Using Cloudflare workers to publish your scheduled 11ty posts

Intro

With my switch to 11ty away from a PHP based CMS I was struggling to see how I could schedule posts. When 11ty builds, it builds static HTML files - with no way of "show this post when this date has passed" functionality.

I like to write my posts and schedule them - I find it gives me more motivation to write, but I digress. I wanted a way to rebuild my site when a post was due to go live.

The straightforward approach is to trigger a re-build manually in Netlify on or after the day the post was scheduled. My "if" blocks would pick up the post in the "past" and populate the listing, RSS and sitemap pages with the new URL.

However, this required me to remember and be near a device that can do it (their interface is surprisingly mobile friendly).

The next approach was to use the Netlify build hook to trigger a rebuild every night. This would tick the automation box, but would quickly eat up my free Netlify minutes, along with doing several unnecessary builds (I publish every other week, generally).

I could schedule a build every fortnight, but for those times I have a Notes post scheduled or similar, it would potentially be 2 weeks before it appears (unless I publish there and then).

What I wanted is for a system to know when my next scheduled post is and regularly check for that date. It can then trigger a build if required.

Table of Contents

Objectives

With the preamble out the way we are going to

  1. Build a page with 11ty which lists the next scheduled post
  2. Check this date regularly, if it matches today (or is in the past) trigger a rebuild

TL:DR;

  1. Create an upcoming.json or similar file to list your next post (Example on Gitlab)
  2. Host your site with Netlify and get the build hook
  3. Set up a scheduled Cloudflare Worker (Gitlab repo) to check the file regularly

Steps

Create an "upcoming" endpoint

The first step is to create an endpoint (page) somewhere with the date of your next post in it. For me, I found it the least path of resistance for this to be a json file.

Using Remy Sharp's Post I created a blog collection, which is posts that are not a draft neither in the future

const now = new Date();
	const livePosts = p => p.date <= now && !p.data.draft;

	config.addCollection('blog', collection => {
		return collection
			.getFilteredByGlob('./app/content/blog/*.md')
			.filter(livePosts)
			.reverse();
	});

Using the same technique, I have also made a drafts collection in my .eleventy.js

config.addCollection('drafts', collection => {
		return collection
			.getFilteredByGlob('./app/content/{blog,drafts}/*.md')
			.filter(p => !livePosts(p))
			.reverse();
	});

The drafts collection contains both posts that are due to go live or are marked with draft: true in the front matter.

One method here would be to make a upcoming/scheduled/pending collection which doesn't include draft: true.

As I only needed this function in one place, I decided to use JavaScript front matter to create a "collection" on the fly.

Create a file called "upcoming.json.njk" with the following front matter

---js
{
	permalink: "upcoming.json",
	post: function() {
		// Get the drafts collection
		let pending = this.ctx.collections['drafts']
			.reverse()
			// Remove everything that is marked as draft
			.filter(p => !p.data.draft);

		// Return the title and date of the next post
		return {
			title: pending[0].data.title,
			date: pending[0].date
		};
	}
}
---

The first line sets a nice permalink, while the following lines load the drafts collection, reverse and filter out the posts marked as a draft.

Lastly, we return just the first blog post - there is no need to include all of them as we only care about the next one.

As our function returns an object, we can use Nunjucks built in functions to output as valid JSON

{{ post() | dump | safe }}

This results in what you see on upcoming.json.

Get the build hook

Netlify have some fantastic documentation on how to find your build hook.

Make a new hook (I called mine "worker" but you could be more specific) and make a note of the URL it gives you.

We'll store that as an environment variable in our Cloudflare Worker settings.

Make the Worker

This uses Cloudflare Workers, but anywhere that can run JavaScript on a cron/scheduled task should work

The next step is to make a Cloudflare Worker to run as often as you wish to check the date of your "pending" post.

Note: For this step, I am assuming you have a Cloudflare account and a basic understanding of Cloudflare Workers.

You can either make a new worker through the user interface on the CLoudflare website or you can use the Cloudflare Wrangler CLI.

Using the UI

You can make a Cloudflare worker using their website with the "Quick Edit" button.

  1. Go to the Workers page and click "Create a Worker"
  2. Paste in the Javascript below (the same file is on Gitlab)
  3. Navigate to the setting page and under the "Triggers" tab click "Add Cron Trigger" and input the pattern for how often you want it to trigger
  4. Navigate to the "Settings" tab and create 2 environment variables
  • BUILD_HOOK - This is your netlify build hook URL
  • UPCOMING_JSON - This is the full path to your /upcoming.json made in step one (e.g. https://www.mikestreety.co.uk/upcoming.json)

Worker JavaScript

/**
 * Fire the check on both the schedule and when accessed via the URL
 */
 addEventListener('scheduled', event => {
	event.waitUntil(checkBuildRequirement())
});
addEventListener('fetch', async event => {
	event.respondWith(checkBuildRequirement())
});

/**
* Shall we build it?
*/
async function checkBuildRequirement() {

		// Get the pending posts and parse as JSON
	let pending = await fetch(UPCOMING_JSON)
			.then(data => data.json()),

		// Should the build be triggered?
		rebuild = 'No build needed';

	// If we have an item and the date is in the "past"
	if(
		pending &&
		pending.date &&
		(new Date(pending.date) < new Date())
	) {
		rebuild = `Building ${pending.title}`;
		await fetch(BUILD_HOOK, {
			method: 'POST',
		});
	}

	// Return some text
	return new Response(rebuild, {
		headers: {
			'content-type': 'text/plain'
		}
	});
}

Cloudflare Wrangler CLI

If you chose to use the CLI, you can use my git repo as a base. Rename wrangler.toml.example to wrangler.toml and fill in the following:

  • name - lowercase, hyphenated
  • account_id - found on the workers overview page
  • zone_id - found on the right hand side of your main Cloudflare dashboard (as this worker is scheduled, it doesn't need to be the domain of your 11ty site - but it helps)
  • UPCOMING_JSON - This is the full path to your /upcoming.json made in step one (e.g. https://www.mikestreety.co.uk/upcoming.json)

Lastly, review and adjust, if required, the schedule for your worker. The notation is that of a standard crontab.

The last thing you need to do is set you build hook path. From your worker project directory, run the following:

wrangler secret put BUILD_HOOK

It will prompt for your URL and store it on Cloudflare.

The last thing is to run the following to get your worker live.

wrangler publish

Your output should look something like

✨  JavaScript project found. Skipping unnecessary build!
✨  Successfully published your script to
 https://your-worker-name.account.workers.dev
with this schedule
 0 9,15 * * *

Conclusion

You should now have your 11ty site rebuilding on Netlify only when you have a new post scheduled to go live.

Let me know if you have an questions or feedback.

View this post on Github

You might also enjoy…

Mike Street

Written by Mike Street

Mike is a CTO and Lead Developer from Brighton, UK. He spends his time writing, cycling and coding. You can find Mike on Mastodon.