Semi-Static Websites

Image by Annie Ruygt

Fly makes it easy to deploy server-side rendered (SSR) content sites to a global fleet of servers close to your readers without a content distribution network (CDN). We’ll look at how to accomplish that with Sitepress, a Ruby site generator that can ran as a stand-alone SSR, static website, or be embedded inside of a Rails app.

Static websites have exploded in popularity over the past few years. What is it that people like so much about static site generators?

  • Low operational complexity - Static websites can be deployed to a production environment without the need for a database, caching server, or other service dependencies. All that’s needed is a pile of HTML, CSS, and JavaScript files and a fast server, like nginx.
  • Things break before they’re deployed - If there’s an error, its usually caught during the compilation phase by the site compiler. The only exception to this are static sites that include tons of JavaScript.
  • Pretty darn fast - Static websites that keep an eye on asset sizes are generally pretty fast around the world when deployed behind a CDN, but only if those caches are warmed up. More on that later.
  • Content is files - Site generators typically manage content as a bunch of text, image, JavaScript, and CSS files in folders. This means they can be stored on a file system and tracked with amazing tools like git, which bills itself as the stupid content tracker. Dynamic systems, like Wordpress, use databases to store content, which requires versioning schemes that are implemented at the application level.

Despite these dreamy characteristics of running static websites in production, it does come with a few trade-offs, especially if there’s a few dynamic things that are needed, like publishing content with a future publish date.

Let’s look at a few approaches for how one might accomplish such a feat so we can better understand these trade-offs.

Problem: How Do I Publish Future Content?

A common use case when dealing with content, like a blog, is scheduling a post to be published in the future. It’s a simple task for a dynamic content management system, but for static site generators, it requires some work. Here’s a few different approaches to the problem.

The Static Way: Use cron to Build and Publish The Site

One way to publish future content with a static website is to schedule an hourly or daily task in cron that builds all the HTML pages in the website and uploads them to a production environment. This works fine for a small website that’s not updated frequently, but it falls over for larger websites that need to publish content at more fine-grained intervals. Imagine wiring up a cronjob that runs every minute to compile and upload a website with a few thousand pages?

The Database Way: Run a CMS like Refinery or Wordpress

On the other end of the spectrum there’s the Content Management System. The most popular CMS in the world is WordPress. For Rails, the most popular CMS is Refinery according to the Ruby Toolbox.

This approach to managing content comes with a lot of complexity. For starters, these approaches require databases. Databases are hard to sync between production, staging, and development environments. Databases break. It’s difficult to version and merge conflicting data in the database if people are editing the same content. Usually some sort of caching layer needs to be built above the content generated by the database. It’s a lot of extra complexity that might not be worth it.

The Middle Road: Deploy a Semi-Static Website to Fly

Semi-static websites, also known as server-side rendered (SSR) sites, run a small server in production that renders content per request. For a blog, that means the server could check a publish_at frontmatter key on a markdown file like this:

---
title: My First Blog Post
publish_at: January 1, 2042
---

Hello! I hope 2042 is a great year.

And quickly figure out whether or not to display the post based on the server’s current time.

We’re going to use a Ruby site generator called Sitepress to deploy a semi-static website to Fly’s global infrastructure. Why not Bridgetown, Jekyll, and Middleman? Those are all really great site generators, but they’re focused on generating static HTML, CSS, and JavaScript assets, which won’t work that great for server-side rendered sites. Outside of Ruby there’s tons of site generators, but this is written for Ruby Dispatch where we talk about all things Ruby.

How to build and deploy a semi-static website to Fly

It’s pretty quick! Here’s how to do it:

  1. Clone the repo https://github.com/sitepress/standalone-starter.
git clone git@github.com:sitepress/standalone-starter.git
  1. Install the Fly CLI and signup for an account.
  2. Now we’re going to run a command that provisions the app and deploys it:
fly launch --copy-config --dockerfile Dockerfile --now
  1. Once that finishes, run fly open and you should see a website that looks like this:

Hooray! You’ve published your first semi-static website. It’s running a webrick server that renders everything per-request.

How fast is the website we just deployed?

Now let’s see what things look like for people on the other side of the world by running fly curl, a nifty little tool that loads your website from various Fly outposts.

fly curl https://$YOUR_SITE_NAME.fly.dev
REGION  STATUS  DNS   CONNECT TLS     TTFB    TOTAL
ams     200     0.7ms 0.8ms   29ms    193.5ms 195.1ms
cdg     200     0.7ms 0.9ms   29.1ms  179.5ms 181.1ms
dfw     200     0.7ms 0.9ms   29.5ms  71.3ms  73.5ms
ewr     200     0.5ms 0.7ms   25.1ms  90.6ms  91.8ms
fra     200     0.6ms 0.8ms   30.5ms  204.7ms 205.9ms
hkg     200     0.6ms 0.7ms   21.7ms  186.9ms 188.5ms
lax     200     0.5ms 0.7ms   21.9ms  50.5ms  51.2ms
lhr     200     0.8ms 1ms     29.2ms  174.3ms 175.9ms
mia     200     0.8ms 1.1ms   33ms    116.4ms 160.6ms
nrt     200     0.4ms 0.5ms   17.4ms  135.8ms 136.5ms
ord     200     0.6ms 0.8ms   22.8ms  93.3ms  95.2ms
scl     200     1.1ms 1.5ms   48.3ms  211.3ms 213.3ms
sea     200     0.4ms 0.5ms   16.3ms  57.4ms  57.9ms
sin     200     0.5ms 0.6ms   17.5ms  204.6ms 205.2ms
sjc     200     3.3ms 3.4ms   34.9ms  46.1ms  46.4ms
syd     200     0.7ms 0.9ms   24.9ms  192.9ms 193.6ms
yyz     200     0.6ms 0.8ms   31.6ms  99ms    99.9ms

The time-to-first-byte (TTFB) times on the other side of the world probably aren’t all that great, which means people trying to read your website are sitting there waiting. TTFB is the amount of time people have to wait after typing your website.com into their browser and receiving the first bytes of HTML.

Provision and deploy to servers around the world

Let’s fix that problem by deploying this website closer to them, without a CDN, by telling Fly we’re cool running our site in these regions:

fly regions set sin fra ord
Region Pool:
fra
ord
sin
Backup Region:

This command tells Fly the parts of the world you want to deploy the website, but the servers aren’t running there yet. Let’s spin them up.

fly scale count 3 --max-per-region=1

Fly will scale up your semi-static site in each of these regions so they’re ready to respond to requests as they come in.

Let’s see how those regions scaled up.

fly status
App
  Name     = $YOUR_SITE_NAME
  Owner    = personal
  Version  = 2
  Status   = running
  Hostname = $YOUR_SITE_NAME.fly.dev
  Platform = nomad

Instances
ID        PROCESS VERSION REGION  DESIRED STATUS  HEALTH CHECKS       RESTARTS  CREATED
394677de  app     2       ord     run     running 1 total, 1 passing  0         1m6s ago
4de70730  app     2       sin     run     running 1 total, 1 passing  0         1m6s ago
8f6646aa  app     2       fra     run     running 1 total, 1 passing  0         2m21s ago

Now how fast is our website?

Let’s run fly curl again and see what happened to the latency:

fly curl https://$YOUR_SITE_NAME.fly.dev
REGION  STATUS  DNS   CONNECT TLS     TTFB    TOTAL
ams     200     0.7ms 1ms     29ms    37.7ms  39.4ms
cdg     200     0.9ms 1.1ms   22.9ms  44.5ms  45.9ms
dfw     200     0.8ms 1.2ms   25.9ms  68.1ms  69.3ms
ewr     200     6.8ms 7.1ms   85.5ms  70.3ms  71.3ms
fra     200     0.8ms 1.1ms   26.9ms  33ms    34.4ms
hkg     200     0.9ms 1.1ms   22.7ms  70ms    71.7ms
lax     200     0.5ms 0.7ms   21.7ms  86.4ms  87.1ms
lhr     200     0.8ms 1.1ms   24.5ms  36.4ms  38ms
mia     200     0.8ms 1.2ms   25.3ms  69ms    70.8ms
nrt     200     0.4ms 0.6ms   18.1ms  94.1ms  94.6ms
ord     200     0.4ms 0.6ms   21.8ms  51.9ms  53ms
scl     200     1.1ms 1.4ms   39.8ms  168.1ms 170.3ms
sea     200     0.4ms 0.5ms   16.1ms  83.2ms  91.6ms
sin     200     0.4ms 0.5ms   17.4ms  26.3ms  26.9ms
sjc     200     0.4ms 0.5ms   28.1ms  83.6ms  83.7ms
syd     200     0.9ms 1.1ms   23.4ms  113.7ms 115.1ms
yyz     200     1ms   1.5ms   41.8ms  60.8ms  62.3ms

Much better! The TTFB time decreased, which means people who are reading the website see the content instantly (under 250ms) as far as they’re concerned.

Fly, Content Distribution Networks, Your Customers, and You

With Fly, it is possible to cut out the middleman CDN and simply run content servers closer to your customers. CDNs are great, but they do add latency to your application for a few reasons:

  1. A CDN with a cold cache has to fetch the content from the origin. This adds latency to the initial request.
  2. When the cache from a CDN expires, it has to check the origin for a fresh resource which again, adds latency to the request. Some CDNs can be configured to serve up the stale content while requesting the new stuff from the origin, but nobody wants something that’s stale. If people like stale stuff bakeries wouldn’t mark down day-old-bread.

For static websites, this isn’t a huge deal, but for mixed websites where all application requests go through the CDN and the cache times are low like a blog or news website, its at least an extra hop that simply isn’t necessary when running servers close to your users.

There’s lots of reasons to deploy semi-static websites

Here’s a few that you could try for your own projects:

  • Always up-to-date project README files - Lots of open source projects have a website that repeat content from the README.md file at the root of their project. With a semi-static site, its easy to fire off an HTTP request for an open-source projects latest README.md file and render it within the project website. The starter app shows how a Github README could be rendered on a project page.
  • Localization - A semi-static site could localize content depending on the FLY_REGION or users IP address.
  • Treat Your Content As Data - Imagine a world where you could get a list of relevant help articles to your customers from within your application? It’s possible with Sitepress via a line of code that looks like HelpPages.tags(:login, :security, :account_management). Treating content as data opens up a lot of interesting use cases for tightly integrating content with your app, which can make for a great experience for your customers.
  • Community Websites - Git is the ultimate content management system for communities, especially when it’s backed with workflows like “Open a PR to edit content”. It’s how Fly’s docs are managed. Be sure to include an “Edit with Github” link on the page that opens the content up in Github’s edit view so people can make contributions for quick edits, like typos.
  • Run it in your Rails Apps - If you’re a small team or solo developer, Sitepress can be embedded in your Rails apps and integrate directly with your routes files. If the Rails application has a database, you’ll want to read about how to run ordinary rails apps globally.