Daniel D. Beck

Scheduling content with static site generators and automatic deployments

Using a static site generator, it’s not always obvious how to publish future-dated content or content that’s derived from data pulled in at build time. Static site generators have lots of benefits for managing and hosting websites, but sometimes going static makes previously simple tasks more complex. This is one of those tasks.

Continue reading to learn why post-dated content and static sites aren’t exactly made for each other, and a pattern for overcoming this obstacle. I’ll illustrate an implementation of the pattern with Jekyll, GitHub Actions, and Netlify.

The problem: static sites can tightly couple publication to deployment

If you’re used to a traditional content management system (CMS), then you might not think much of post-dating content. For example, if you’re using WordPress to publish a new blog post and you set a future date and time for the post, then it does not appear on your site until that date and time. The content appears at the appointed time without intervention because your site effectively regenerates on every page load.

With a static site generator like Jekyll or Hugo, only the content which is current at the time of deployment appears on your site. To get a future-dated page to appear, you must regenerate and deploy on or after the publication date and time. Absent some automation, it’s inconvenient to publish something at a specific time and adds a new source of risk. If your site only deploys when you trigger a deployment—by pushing a commit or running a script—then it’s easy for your site to serve up stale content because you forgot to deploy at the right time.

In other words, static sites can needlessly bind your publication process to your version control or continuous integration tools. This binding is avoidable, however. There are ways to schedule publication that don’t require you to go back to a traditional CMS or introduce a heavyweight dependency, such as a headless CMS.

A pattern for scheduled content

My preferred approach to this problem follows this general pattern:

  1. Post-date content (like you would with a traditional CMS)
  2. For other immediate changes, deploy as usual
  3. Automate a standing daily (or more frequent) deployment

Step 1: Post-date content

Use your static site generator’s publication date field. Many static site generators provide partial tools to post date content like you would with a traditional CMS. For example, if you’re using Hugo, there’s a publishDate field. Set it to the earliest time you want that content to be available.

Until the publication date, use your static site generator’s preview options for local development (and continuous integration on development branches). For example, Jekyll’s --future command-line switch generates everything, including post-dated pages.

But don’t use this feature for production deployments. This way, you can continue to check in content following a workflow that’s indifferent to what day it is without fear that future-dated content will appear prematurely.

Step 2: Deploy as usual

If you already use a continuous deployment mechanism, then continue to do so. If you use, for example, Netlify’s Git integrations, then you’ll still get near-immediate deployments for content that’s ready for production now.

If you deploy manually, via script or user interface, then now might be a good time to consider trying a continuous deployment workflow. You’ll get something like it in the next step anyway; you might as well see what it’s like to always be deploying. In any case, you can continue to trigger deployments manually by whatever means you already have.

Step 3: Automate recurring deployments

On top of your existing deployments, schedule deployments to run automatically, at a frequency that matches the pace at which you schedule content. For example, if you want to publish one item each day, you can schedule deployments once or twice a day. If you want to schedule publication several times a day, you may need to deploy hourly or more often.

There are lots of tools to carry out this last bit: cron, CI/CD webhooks, and, what I’ll be illustrating below, GitHub Actions.

Scheduling deployments with Jekyll, GitHub Actions, and Netlify

Now that you’ve got the general pattern, it’s time for a concrete example. My own site consists of Markdown-formatted sources in a GitHub repository, generated into a site with Jekyll, which is, in turn, deployed with Netlify.

Suppose today is July 6 and I’m planning to publish a new page in one week, on July 13. The front matter for the new page looks something like this:

layout: post
title: "Another new post"
date: 2021-07-13 08:30:00 +0100

The key line is the date field, which shows this page is scheduled for future publication on July 13 at 8:30 BST. If I run jekyll build now, this page is not generated. If I commit and push this change to my default branch, then Netlify would automatically deploy the site, but this page would not be part of that deployment. But if I run jekyll build --future, I can preview the page locally.

How do I make sure this page appears on July 13 at 8:30 BST? One option is to get an early start that Monday morning and be sure to run git push at precisely 8:30. But let’s just say I have a relaxed relationship with mornings and that is not going to happen.

Instead, I’ll push this change now and create a GitHub Actions workflow that triggers the Netlify build every morning, unattended. To set up the workflow, I’ll follow these steps:

  1. Add a build hook—a unique URL that triggers a build—to the site on Netlify. See the Netlify docs for build hooks to learn how to do this. Keep the URL secret.
  2. Store the build hook URL as a repository secret. See the GitHub docs for secrets to learn how to do this. I call my secret NETLIFY_BUILD_HOOK.
  3. Commit a workflow configuration file in .github/workflows that makes a POST request to your secret build hook URL.

The last part is the tricky bit. GitHub Actions are a little intimidating at first. If you create the proper configuration file, you can run commands on push, pull request creation, and other events, or on a schedule. My .github/workflows/deploy.yaml file looks like this:

name: Deploy ddbeck.com every weekday
    - cron: "31 8 * * 1-5"
  runs-on: ubuntu-latest
    - name: Trigger Netlify build
      run: curl --silent --show-error -X POST -d {} "$NETLIFY_BUILD_HOOK"

There are three key sections here.

  1. The schedule option: it uses a cron notation to specify that this workflow runs every weekday morning at 8:31.
  2. The run step: it runs a shell command. In this example, it triggers a build by making a request with curl to the Netlify build hook URL.
  3. The env option: it sets an environment variable for the run command. In this example, it sets an environment variable from the repository secret I made earlier.

Once this file is checked into my repository’s default branch, GitHub automatically triggers the deployment each morning. Now, on July 13 at 8:31, the site redeploys on Netlify; the previously post-dated content becomes current and is included in the generated output and deployment.

Variations on the pattern

This isn’t the only way to schedule content with static sites. A number of components can be substituted, depending on your needs.

Narrowly, there are many ways to configure your GitHub Actions workflow. For example, you could increase the frequency of deployments (e.g., to handle several post-dated items per day). Or you can vary the deployments themselves, by using Netlify’s own (more sophisticated) library of actions for triggering builds, or adding additional checks and conditions before deploying.

Beyond that, you can substitute individual tools in the overall pattern. Some examples:

However the pieces come together, you don’t have to tie yourself to your editorial calendar just because you’re using a static site generator. With a little automation, you can post-date your content as well as (or better than) a traditional CMS.

If you’re thinking about migrating to a static site generator, then I suggest pruning your content before moving. Check out my guide to content audits, Delete Your Content, to start your migration right.