ADAM DJ BRETT

Home / Blog / How Build Awesome alpha, async Nunjucks, and smaller Cloudflare checks fixed my podcast build

I spent part of this week cleaning up the build and deploy pipeline for the Mapping the Doctrine of Discovery Podcast. It started as a simple GitHub Actions failure and turned into a useful reminder: the fastest static site is not just the one with the fastest generator. It is the one with the fewest unnecessary moving parts.

The site was already static, already running on Cloudflare Workers, and already in decent shape. But the deployment workflow had accumulated enough checks, helper installs, metadata files, and generated headers that the system was starting to work harder than it needed to. The main problem was not one dramatic bug. It was several small sources of friction stacked together:

  • Eleventy was still pinned directly as @11ty/eleventy.
  • The site generated too many Cloudflare _headers rules.
  • The deployment workflow installed PDF tools on every deploy.
  • The deploy script ran the full npm test, including checks that did not need to block every publish.
  • GitHub Actions upgraded npm on every run even though the bundled runner version was good enough.

The result was a successful but heavier-than-necessary pipeline. After the cleanup, the deploy path is smaller, the package graph is leaner, the _headers file is dramatically simpler, and the Cloudflare deployment runs in about half the time it did before.

Moving from Eleventy to Build Awesome alpha #

The first major change was moving the site from a direct Eleventy dependency to Build Awesome, the new package identity for Eleventy. The install command is straightforward:

npm install @awesome.me/buildawesome --save-dev

In this repository I pinned the alpha exactly:

"@awesome.me/buildawesome": "4.0.0-alpha.10"

and changed the build command from:

"build": "npm run clean && eleventy && npm run generate:postbuild"

to:

"build": "npm run clean && buildawesome && npm run generate:postbuild"

That did two things. First, it put the site on the current Build Awesome alpha line. Second, it let me remove the direct @11ty/eleventy dependency from my own package.json. Build Awesome brings its matching Eleventy alpha internally, so my top-level dependency list no longer had to carry both the old direct generator dependency and the new package.

That made package.json easier to understand. The build tool now reads like the tool I am actually using:

"dependencies": {
  "@11ty/nunjucks": "4.0.0-alpha.3",
  "@awesome.me/buildawesome": "4.0.0-alpha.10"
}

The upgrade commit changed the lockfile substantially too: 591 insertions and 937 deletions. That is not a perfect measurement of complexity, but it is a useful sign that the dependency graph changed meaningfully. After removing the old direct Eleventy dependency, npm uninstall @11ty/eleventy removed 49 packages from the local install.

The migration did surface one real assumption in my own code. A global data helper still tried to read:

packageJson.devDependencies["@11ty/eleventy"]

That worked only while @11ty/eleventy was a direct dependency. Once Build Awesome became the build tool, that value was gone. The fix was simple: read the Build Awesome package version instead. This is the kind of small migration bug I actually like. It shows exactly where my code was coupled to the old package shape.

Nunjucks v4 full async #

The second important change was using @11ty/[email protected], the full-async Nunjucks alpha.

This release matters because it moves Nunjucks internals much more fully into async/await territory. For this site, I used it most directly in the post-build _headers generator:

import { Environment } from "@11ty/nunjucks";

const env = new Environment(null, { autoescape: false });
const headers = await env.renderString(template, {
  siteUrl: SITE_URL,
  seasonCategories,
  standaloneCategories,
});

That may look like a small thing, but it made the headers script feel like the rest of the modern build pipeline: async data in, rendered text out, no awkward sync/async boundary. It also let me write the Cloudflare header rules as a real template instead of manually pushing strings into an array.

The larger lesson is not that every file needs Nunjucks. It is that a static site build becomes easier to reason about when the templating layer and the scripting layer both handle async work naturally. Podcast sites are full of derived artifacts: feeds, metadata, citations, headers, redirects, and search indexes. The fewer special cases in the build model, the better.

The Cloudflare _headers problem #

The actual build failure came from Cloudflare's _headers limit.

The site emits FAIR-style metadata for each podcast episode. Each episode gets a nearby metadata.json file and HTTP Link headers that point to that structured metadata. That is good for machine readability, citation, and long-term discovery. The problem was how I represented those headers.

The original generator created per-episode rules. With 54 podcast posts, that pushed _site/_headers to 111 rules. Cloudflare Workers allows at most 100. The check failed with:

_site/_headers has 111 rules; Cloudflare Workers allows at most 100

The first fix collapsed the per-episode rules into wildcard rules by content group. That brought the file down to 9 rules. Then I pushed it further by recognizing that all season paths shared the same structure. Instead of separate rules for season1, season2, season3, and so on, Cloudflare placeholders could express the whole family:

/season:season/*
  Link: <https://podcast.doctrineofdiscovery.org/season:season/:splatmetadata.json>; rel="describedby"; type="application/ld+json"; profile="https://schema.org/"
  Link: <https://podcast.doctrineofdiscovery.org/season:season/:splat>; rel="cite-as"
  Link: <https://schema.org/PodcastEpisode>; rel="type"

The final _headers file has only four rules:

/*.xml
/*/metadata.json
/season:season/*
/special/*

That is the kind of optimization I like. It did not remove metadata. It removed repetition. The site still advertises machine-readable episode metadata, canonical feed URLs, JSON content types, and podcast episode type links. It just does so with patterns instead of one rule per page.

I also tightened the local check so this does not regress. The header test now enforces 5 rules or fewer, not Cloudflare's looser limit of 100. If I accidentally go back to per-page header rules, the build will complain immediately.

Splitting deploy checks from archival checks #

The next set of improvements came from reading the GitHub Actions logs.

The old run showed this shape:

StepApproximate time
Setup Node.js5.2s
Update npm3.3s
Install dependencies3.5s
Install PDF verification tools13.4s
Build and check site with npm test6.8s
Deploy to Cloudflare Workers9.1s

The biggest waste was obvious: installing PDF tools during every deploy. The check:pdfs script uses pdfinfo and qpdf to verify PDF metadata, rights text, and outline/bookmark data. Those checks are valuable. They are also not necessary for every Cloudflare deployment.

So I split the test paths.

The full test remains:

"test": "npm run build && npm run check:esm && npm run check:build && npm run check:headers && npm run check:feeds && npm run check:assets && npm run check:links && npm run check:redirects && npm run check:metadata && npm run check:citations && npm run check:pdfs"

The deploy test skips PDF verification:

"test:deploy": "npm run build && npm run check:esm && npm run check:build && npm run check:headers && npm run check:feeds && npm run check:assets && npm run check:links && npm run check:redirects && npm run check:metadata && npm run check:citations"

Then the Cloudflare workflow changed from npm test to:

- name: Build and check Build Awesome site
  run: npm run test:deploy

That let me delete the entire apt install step from deploy:

sudo apt-get update && sudo apt-get install -y ghostscript poppler-utils qpdf

It also let me remove a redundant shell check that counted files in _site, because npm run check:build already validates the expected output files and checks that the build is not suspiciously small.

But I did not delete PDF validation. I moved it into its own workflow:

name: Check PDF Metadata

That workflow runs manually and when PDF-related paths change. It installs only what the script actually uses:

sudo apt-get update && sudo apt-get install -y poppler-utils qpdf

No Ghostscript. No deploy blocking. No loss of metadata discipline.

Relaxing npm without giving up Node discipline #

The final small speed win came from npm.

The workflow was installing npm 11.18.0 globally on every run because package.json required:

"npm": ">=11.18.0"

But the GitHub runner already had npm 11.9.0 with Node 24.14.0, and the project builds cleanly with that. Updating npm cost about 3.3 seconds in the old log and about 2 seconds in the later optimized run.

So I relaxed the npm engine:

"engines": {
  "node": ">=24.14.0",
  "npm": ">=11.9.0"
}

and set:

"packageManager": "[email protected]"

Then I removed the workflow step that upgraded npm. Node stays pinned. The build still uses a modern npm. The deployment no longer pays a repeated tax for a version difference that was not buying me anything.

The timing change #

The deploy timeline now looks much better.

Before these changes, the Cloudflare deploy run was about 59 seconds. After removing PDF tooling from deploy and using test:deploy, it dropped to about 33 seconds. After relaxing npm and removing the npm upgrade step, it landed around 31 seconds.

In practical terms:

VersionTime
Before PDF/workflow cleanup~59s
After deploy-only test path~33s
After npm engine relaxation~31s

That is not just a percentage improvement. It changes how the workflow feels. A half-minute deploy is short enough that I can push a content or metadata fix and stay in context while it runs.

Local build/check time is also comfortably small. The deploy test runs in about 4 seconds on my machine:

npm run test:deploy

The Build Awesome part itself wrote 215 files and copied 265 files in about 1.1 seconds locally during the most recent run. For a podcast site with feeds, metadata files, citations, redirects, asset checks, link checks, and Cloudflare headers, that is a good place to be.

What this solved #

The biggest visible problem was the Cloudflare header limit, but the cleanup solved more than that.

It reduced site complexity. _headers went from 111 rules to 4. That is not a cosmetic change. It means the metadata strategy now scales with the URL structure instead of the number of episodes.

It reduced deploy complexity. The Cloudflare workflow no longer installs unrelated PDF tools or runs a full archival validation suite on every publish.

It preserved quality checks. PDF metadata validation still exists, but it runs where it belongs: in a focused workflow that responds to PDF-related changes or manual review.

It shrank the package surface. The site no longer carries direct @11ty/eleventy as its own top-level build dependency. Build Awesome is the build tool, and the package file says so.

It clarified responsibility. npm test is the full quality gate. npm run test:deploy is the deploy gate. check:pdfs is archival/document quality. The workflow names now match their jobs.

The broader lesson #

This is the kind of maintenance that rarely looks impressive from the outside. There is no redesign screenshot. No shiny homepage. No dramatic feature announcement.

But it matters.

Static sites get their power from being understandable. If the build process becomes a pile of inherited assumptions, the site slowly stops being minimal even if the output is static HTML. I do not want a "simple" site with a complicated deploy ritual. I want a publishing system where each check earns its place.

Moving to Build Awesome alpha, using async Nunjucks, collapsing Cloudflare headers, and splitting PDF validation into its own workflow all moved the site in that direction. The site is still doing the important scholarly and archival work: structured metadata, citation files, podcast feeds, redirects, PDF metadata checks, and durable URLs. It is just doing less unnecessary work on the way to deployment.

That is the real optimization: not making the computer race through a messy process, but making the process smaller, clearer, and easier to trust.

Tags : 11ty build-awesome cloudflare performance web-development static-sites

Webmentions

No webmentions yet.

Previous

PNG to PDF to OCR: A Small Command Line Skill for Digital Humanities Work

A practical Mac command line workflow for turning high-resolution PNG scans into searchable OCR PDFs with img2pdf and OCRmyPDF.

Next

Making Static Sites FAIR with metadata.json, Zotero, Eleventy, and Jekyll

A detailed implementation note on adding FAIR Signposting, per-page Schema.org metadata.json files, Zotero metadata, and citation automation across Eleventy and Jekyll sites.