Static Sites without the Static
A technique to get peak performance without waiting for rebuilds and preview URLs
Static sites, produced with tools like Gatsby, Hugo, and others, are popular for a reason. They deploy almost anywhere, are extremely fast, and don’t compromise on the developer experience. When paired with a CMS though, new sites must be built each time an editor makes an update in the UI.
This isn’t always a problem since static site rebuilds are fast, but it does mean that that tooling needs to be inserted after changes are published to perform the rebuild. Most CMS providers offer webhooks to power this functionality.
Recently, a number of services that have sprung up recently to offer “preview’ or staging versions of a site, and some that attempt to keep the entire preview client-side as part of the editor. But as good as these are, it does mean that an editor (who is often a non-technical user) needs to sit through a rebuild process to see changes they have made.
Recently, I set out to explore a technique to allow instant updates from a CMS-backed site on staging sites, while keeping the production version static. Let’s dive in and see how that technique works.
A look at the tools we’ll use
Next.js
- Supports Server Side Rendering out of the box
- Has simple “API” support via an
/api
folder. While we won't leverage it for this article, most CMS-backed sites will need to process simple forms and this is a big selling point. - Can optionally export a fully static site. More on this at the end of the article.
graphql-request
- A simple wrapper for fetch to make GraphQL queries
Now (optional)
- Provides multi-cloud redundancy out of the box
- Supports a CDN caching layer, which we can intelligently set via Server Side Rendering
Start with the dynamic, Server Side Rendered page
First, you’ll need to setup a Next.js site. If you’re starting from scratch, I’d recommend using create-next-app
.
Then, set up a GraphQL client using graphql-request
:
After that, set up a Next.js page that fetches data from the CMS using GraphQL.
In a staging environment, we need to point graphql-request
to our CMS's "draft" endpoint (most CMS's support this workflow). Whenever an editor saves a change to the CMS, they can reload the page to see the changes.
Remove the need to reload
Reloading the page to see updates works well, but gets tedious for the user that is previewing the changes. What if we could build in a “live reload” feature, that automatically refreshes the staging site after changes are saved? To accomplish a live reload feature, we can:
- leverage a SSR response header to determine if page data has changed
- add a focus hook that will asynchronously fetch the page in the background and compare the new version header
Set the version header
To set the version header, we will create a hash of the data after it is fetched during Server Side Rendering. We’ll create a utility to keep things clean.
Add hooks that implement focus and reload behavior
The first hook we will add will simplify the handling of browser focus events.
And a hook that wraps useFocus
that checks the version header and reloads the page if necessary:
Here’s how to use the hooks we just put in place:
To recap what is going on:
- An
x-version
header will be set during Server Side Render, based on the CMS API response data. - When the browser receives a focus event, the current page is fetched again in the background
- If the header has changed, we reload the page. If not, we do nothing.
Note:
This same technique can be leveraged on any Single Page App to notify users that a new version has been released, and that they should reload the page. In that case, you would typically add a confirmation prompt before reloading on their behalf.
After deploying, you should be able to verify the x-version
header was set in the browser.
Moving to production on Now
Up to this point, we’ve created a Server Rendered Next.js app that will automatically reload on focus if the page data has been updated in our CMS. The final step is to bring the high performance of a static site to our page.
To do that, we will leverage a built in feature built into the Now platform called Serverless Pre-Rendering. Similar to the header approach above, we’re going to set a special cache header to cache our pages at the CDN layer. In practical terms, that means our page will be as fast as a static site for the duration that it is cached.
Now’s CDN will send a cached version of the page to the user. If the cache time has expired, it will asynchronously fetch and re-cache the page using a Serverless lambda in the background. This is awesome, because it avoids the “slow for 1 user every x seconds” problem you encounter with many caching strategies.
Let’s add a util to set the proper cache header during Server Side Rendering.
The important bits here are s-maxage
and stale-while-revalidate
. we set s-maxage
to the number of seconds we'd like the CDN to serve a cached page, and stale-while-revalidate
powers the async cache update strategy mentioned above.
To use the util, we set a cache time on a per-page basis. For example, we might want our homepage to only be cached for a max of one minute before refetching, but our contact page could be cached significantly longer.
Once you deploy this code to staging, you should be able to verify that a cached response was returned:
What happens if you deploy a code update during this time?
Now will intelligently clear caches for you every time you deploy! See https://zeit.co/docs/v2/network/caching/#cache-invalidation for more detail.
Alternate approach: use Next.js’s static export
What if you can’t or don’t want to host on Now? As I mentioned earlier, Next.js also supports exporting a static site. Using this, we can change our build pipeline to export a static version for production.
Unfortunately, this requires adding some configuration to your app, especially if you have dynamic pages. Here’s an example config that filters out dynamic pages and populates sub-pages based on the results of a GraphQL query:
You’ll also want to add a hook from your CMS that triggers an export and deployment. Generally, it’s best to leverage a script in package.json
to keep things clean.
You could also leverage the custom webhook to run a workflow on GitHub Actions that accomplishes the same thing.
Enjoy your non-static static site!
We now have what seems to be the best of both worlds. Our staging site acts like a traditional server that reflects changes as soon as they are saved in the CMS. Due to the CDN caching, our production site is just as fast as a static site, with the added bonus of never being out of date for longer than our cache time. We never need to wait on a site rebuild.
We’re currently rebuilding the Echobind site, which leverages this technique. Look for it to be released soon!
Thanks to ZEIT
Massive props to the team at ZEIT for not only adding this feature to the Now platform, but also for writing a blog post on Serverless Pre-Rendering that demonstrated the focus reload technique.