ADAM DJ BRETT

Home / Blog / 11tyx5, Reprise: Build Awesome Alpha, a Whitestone Metasearch, and the Quest for Four 100s

Eleventy days ago (literally) I wrote about launching five Whitestone Foundation sites at once and called it 11tyx5. I ended that post with a small tease: a Version 3 redesign of the flagship site was "in active development," and the network would keep moving. I did not expect the next chapter to arrive as one overnight sprint, but that is more or less what happened. Between the evening of July 8th and the morning of July 9th, thewhitestonefoundation.org shipped its v3 rebuild, the whole fleet got a shared search overhaul, jcrt.org's search quietly broke and got rebuilt on a sturdier foundation, and I learned a genuinely new lesson about Cloudflare's edge cache that I am still a little annoyed I didn't know already.

This post is the field report. It covers the Build Awesome alpha upgrade, why I tore out the old cross-site search pipeline, how the new [/metadata/search/(https://thewhitestonefoundation.org/metadata/search/)] page merges five independent Pagefind indexes in the browser with no server involved, what it took to get all five sites onto one build pipeline and one Netlify setup, where the build-time savings actually came from this time, the still-unfinished chase for four 100s in PageSpeed Insights, and the Cloudflare caching bug that made a fixed problem look unfixed for the better part of a day.

Upgrading to Build Awesome v4.0.0-alpha.10 #

I wrote a whole post on July 5th about moving a smaller podcast site onto Build Awesome — the new package identity for Eleventy — and I won't repeat that here except to say the pattern held: npm install @awesome.me/buildawesome --save-dev, pin the alpha exactly in package.json, swap the build script from eleventy to buildawesome, and let Build Awesome carry its own matching Eleventy alpha instead of listing @11ty/eleventy as a direct dependency yourself.

The Whitestone v3 rebuild went a step further and adopted v4.0.0-alpha.10 specifically. I went and read the actual release notes before writing this, because "alpha.10" as Bob noted in the 11tybundle.dev is not where the exciting new changes happened that is actually in alpha.09. Reading the realse notes for alpha.09 helped me realize i need to keep my output file the same and not be a mad man and switch it to dist, so I am trying to aspire to Zach's levels of consistency in my code. The Whitestone config keeps the same explicit dir block it always has.

From allsites.json to metasearch: what I tore out first #

As previously mentioned in 11tyx5 I tried to use a shared data patterns like allsites.json, an append-only canonical record, and an intentionally opinionated cross-site ranking where JCRT's archive outranked the magazine site, which outranked the org page. That system worked, in the sense that it produced ranked results. But it worked the way a lot of first systems work: by being the most obvious thing to build, not the thing you'd choose if you started over.

Under the hood it was a scheduled ingestion job — ingest-allsites.yml — that crawled the other four Whitestone sites on a cron, ran an embedding model over what it found, and committed the resulting JSON straight into the flagship repo. That bought semantic-ish ranking, at the cost of a genuinely awkward set of tradeoffs: a cron job that could silently stop working, a growing pile of committed JSON that had nothing to do with the site's own content, an embedding step that had to run somewhere, and search results that were only ever as fresh as whenever the job last succeeded. If JCRT published a new archive issue on Tuesday, the org-wide search wouldn't know about it until the next scheduled crawl.

The v3 rebuild deleted the whole pipeline — the workflow file, the ingestion script, the committed JSON — in the same pass that removed the old semantic-vector search. That's a strange thing to be proud of, ripping out a feature you built on purpose a year earlier, but it's the right kind of strange. The replacement needed to answer the same question — "search everything Whitestone publishes, from one box" — without a cron job, without an embedding model, and without ever being staler than the last time each individual site deployed. That's the metasite work below.

Building a metasite: metadata as infrastructure, Pagefind as an API #

What replaced the deleted ingestion pipeline is smaller than what it replaced, which is generally the sign you did something right. Here's the architecture, in the order I'd explain it to someone rebuilding this from scratch.

Metadata as the control plane #

The rebuild's actual thesis is that most of what makes a multi-site network annoying isn't content, it's facts about the sites — titles, taglines, canonical URLs, OG images, footer links — repeated with slight drift across templates until nobody trusts any of them. So v3 puts all of that in one file, src/_data/metadata.yaml, per site:

site_title: "The Whitestone Foundation"
tagline: "Fostering critical thought for the public interest."
url: "https://thewhitestonefoundation.org"
routes:
  contact: "/contact/"
  news: "/news/"
seo:
  ogImage:
    url: "/images/whitestone-logo.webp"
    width: 1200
    height: 630
  openGraph:
    type: "website"
  twitter:
    card: "summary_large_image"

A handful of Nunjucks filters — headTitle, headDescription, headImage, canonicalUrl, absUrl — read from this at build time and populate the <head> of every template. Per-post pages get BlogPosting JSON-LD, and a no-JS breadcrumb component emits its own BreadcrumbList JSON-LD alongside the visible trail. None of this involves a runtime; it's Eleventy resolving data into HTML once, at build time, the same way it's always worked. The point isn't novelty, it's that "site metadata" became a data problem instead of a templating-discipline problem. Once you frame it that way, the search feature falls out almost naturally, because a Pagefind index is really just another artifact built from the same finished HTML.

Pagefind as an API, not a widget #

Each site's build ends the same way: eleventy builds _site/, esbuild minifies CSS/JS, and then pagefind --site _site --output-subdir pagefind runs last, crawling the already-rendered production HTML and writing a WASM search engine plus index chunks under /pagefind/. There's no server component — a visitor's browser downloads the WASM module and runs the query locally against static files, which is either delightfully retro or the logical endpoint of "static site," depending on your mood.

Locally, each site uses Pagefind's newer Component UI rather than the older PagefindUI bundle, lazy-loaded off the critical path:

<pagefind-modal-trigger role="search" aria-label="Open search" compact hide-shortcut>...</pagefind-modal-trigger>
<pagefind-modal></pagefind-modal>

The script only loads on idle, on first hover/focus of the trigger, or on Cmd/Ctrl-K — nobody pays the WASM tax until they've expressed intent to search.

The interesting part, though, isn't the per-site widget. It's that Pagefind's index is addressable and mergeable across origins, which means you can treat it less like a search plugin and more like a read API.

A Pagefind bundle is a public, versioned, cacheable, CORS-enabled read API for "what does this site contain" — it just happens to be made of static files instead of JSON endpoints.

The metasearch page #

/metadata/search/ on thewhitestonefoundation.org is the site that actually exercises that idea. It's a plain Eleventy template — no special backend, no aggregation job — that uses Pagefind's documented multisite API to merge four other sites' indexes into its own, live, in the visitor's browser:

await import('/pagefind/pagefind-component-ui.js');
const { configureInstance } = window.PagefindComponents;

const remotes = [
  { bundlePath: 'https://jcrt.org/pagefind/', mergeFilter: { Site: 'JCRT' } },
  { bundlePath: 'https://thenewpolis.com/pagefind/', mergeFilter: { Site: 'The New Polis' } },
  { bundlePath: 'https://journal.thenewpolis.com/pagefind/', mergeFilter: { Site: 'The New Polis Journal' } },
  { bundlePath: 'https://esthesis.org/pagefind/', mergeFilter: { Site: 'Esthesis' } }
];

const probes = await Promise.allSettled(
  remotes.map((r) => fetch(`${r.bundlePath}pagefind-entry.json`, { method: 'HEAD', mode: 'cors' }))
);
const reachable = remotes.filter((r, i) => probes[i].status === 'fulfilled' && probes[i].value.ok);
const unreachable = remotes.filter((r) => !reachable.includes(r));
if (unreachable.length) {
  console.warn('Metasearch: skipping unreachable indexes:', unreachable.map((r) => r.bundlePath));
}

configureInstance('allsites', {
  bundlePath: '/pagefind/',
  mergeFilter: { Site: 'Whitestone Foundation' },
  mergeIndex: reachable
});

document.getElementById('allsites-search').innerHTML = `
  <pagefind-input instance="allsites" placeholder="Search all Whitestone sites"></pagefind-input>
  <pagefind-filter-dropdown instance="allsites" filter="Site"></pagefind-filter-dropdown>
  <pagefind-summary instance="allsites"></pagefind-summary>
  <pagefind-results instance="allsites"></pagefind-results>
`;

configureInstance creates a named search instance; mergeIndex tells it to pull in other Pagefind bundles alongside its own local one; mergeFilter tags results from each merged index with a Site value so a pagefind-filter-dropdown can let visitors narrow to one publication after the fact — without needing to tag every individual page on every individual site. For any of this to work, each remote's /pagefind/* path needs Access-Control-Allow-Origin: *, since the browser is fetching another origin's index files directly.

The Promise.allSettled probe block exists because of a failure mode I hit almost immediately: if one remote site is down, misconfigured, or its CORS headers are wrong, mergeIndex doesn't fail cleanly — the whole merged instance can just hang, and the visitor sees a permanent "Connecting to all Whitestone sites…" with nothing ever resolving. Probing each remote's pagefind-entry.json with a HEAD request before merging means one publication having a bad day degrades gracefully — its results are silently absent — instead of taking down search for the other four. There's also a <noscript> fallback linking to each site's own /search/ page, since the entire mechanism is inert without JavaScript.

The gotcha: version skew is a silent failure, not a loud one #

Once this was wired up, I noticed JCRT results in the merged search were thin — a couple of weak matches where there should have been dozens. The cause wasn't CORS, and it wasn't the filter logic. It was that jcrt.org's Pagefind index had been built with Pagefind 1.4.0, while the metasearch page and every other site were on 1.5.x. A 1.5 engine merging an index built by 1.4 doesn't error — it just quietly returns partial or degraded results. No exception, no console warning, just a search box that seemed to work while being wrong.

The fix was to bump jcrt-v2's pagefind devDependency to ^1.5.2, and, more importantly, to fold the installed Pagefind version into the build's cache-invalidation signature — so a future version bump anywhere in the network can't silently reuse an index built by a stale binary. After the fix, a search for "Christian Discovery" went from 1 result to 132, with the JCRT 25.1 archive article landing at #3, right where it belonged.

The lesson generalizes past Pagefind: treating static files as an API doesn't exempt you from API compatibility contracts. If five independently-deployed sites are going to have their indexes merged by a shared client, those indexes need to come from compatible versions of the same tool — and because the failure mode is silent degradation rather than a thrown error, you need to go looking for it rather than wait for it to announce itself.

What I like about the end state is how small the integration surface is. Each site stays independently deployable and owns its own index; the flagship site is just the client that composes them. Adding a sixth Whitestone publication later means: publish its Pagefind bundle with CORS enabled, add one object to the remotes array. That's the whole contract — which is a nice place to be after ripping out a whole embeddings pipeline, and sets up the next problem, which is keeping five separately-deployed repos honest about the shared metadata and build conventions that make this composability possible in the first place.

Getting five sites to share one build, one 11ty config shape, and one Netlify setup #

None of the metasearch work above means anything if the other four sites can't produce a Pagefind bundle that's reachable, current, and CORS-friendly. So the second half of the July 8–9 sprint was fleet work: bringing jcrt.org, thenewpolis.com, journal.thenewpolis.com, and esthesis.org up to a shared baseline, one site at a time, mostly by spawning one subagent per repository and giving each the same short brief: adopt Pagefind's Component UI, add the CORS headers the org search needs, and make sure the local search on that site actually works while you're in there.

The rollout looked like this across the fleet:

  • thenewpolis.com on github/search/ converted from the legacy PagefindUI widget to Component UI (the rest of the site's search plumbing was already there). The site indexes 1,914 pages.
  • journal.thenewpolis.com on github — legacy /search/ converted the same way, and a leftover hack that had excluded Component UI assets from the search page (to stop two search widgets from colliding) got deleted now that only one widget exists. data-pagefind-body was added to <main> so indexing scopes to actual content instead of the whole page shell.
  • thewhitestonefoundation.org on github — Pagefind bumped 1.4.0 → 1.5.2, /search/ converted, a dead processResult handler removed.
  • esthesis-v2 on github — bumped to Pagefind 1.5 and a stale pagefind-ui passthrough copy removed. A user report ("results just show the site title and 'skip to main'") turned into a real quality pass: data-pagefind-body on <main>, data-pagefind-ignore="all" on the hero, nav, sidebar, and listing grids, a broken tag-page <h1> fixed, explicit title <meta> tags added, the /search/?q= deep link preserved, and Pagefind's --pf-* CSS custom properties mapped onto the site's dark mode. Verified live afterward: searching "beauty" returned 42 real, correctly-titled results instead of noise.
  • jcrt-v2 on github

Each of those is a small diff on its own. Together they're the thing that actually matters here: every site in the network now emits a Pagefind bundle built the same way, scoped to <main> the same way, versioned the same way, and CORS-enabled the same way. That consistency is what makes the merged metasearch page trustworthy instead of a demo that happens to work for two of the five sites on a good day.

I got stuck several times late at night and as the repositories will show I did use Claude and Github Co-pilot for assistance in troubleshooting. I did check, verify, and modify their code. They did not ship any code, I made many of the changes they recommended and gave credit to them for clarity. Github Co-pilot and Claude really do not like the 11ty/nunjucks fork but I do so I'm keeping it.

The Netlify side of this stayed almost boring by comparison, which is the goal. Each repo's public/_headers file grew the same shape of rule:

/pagefind/*
  Access-Control-Allow-Origin: *
  Access-Control-Allow-Methods: GET, HEAD, OPTIONS

with jcrt-v2 carrying a slightly richer version to satisfy Range-request preflights (more on why in the Cloudflare section below):

/pagefind/*
  Cache-Control: public, max-age=172800, stale-while-revalidate=604800
  Access-Control-Allow-Origin: *
  Access-Control-Allow-Methods: GET, HEAD, OPTIONS
  Access-Control-Allow-Headers: Content-Type, Accept, Range
  Access-Control-Expose-Headers: Content-Length, Content-Range, ETag

Each site still deploys independently, on its own Netlify project, on its own schedule. Nothing about the metasearch work required a shared deploy pipeline or a monorepo. It required a shared contract: build Pagefind the same way, expose it the same way, and let the flagship site do the composing. That's the "one build pipeline" in the section title — not one repo, one pipeline shape, repeated five times.

While I had the fleet open, I also spent a morning on the boring-but-necessary chore of auditing pull requests across all six repositories under the-Whitestone-Foundation organization on GitHub. Five had zero open PRs. journal.thenewpolis.com had five open Dependabot PRs, all conflicting against main because of the same session's work. Two (a Pagefind bump and an Eleventy bump) were superseded by changes already on main and got closed. Three more — an eleventy-plugin-rss major version bump, a markdown-it-table-of-contents bump, and a slugify bump — got rebased, conflict-resolved, built, and diffed against baseline feed output to confirm nothing broke, then left pushed and mergeable for me to click through by hand. Merging dependency bumps without a human actually looking at the diff is exactly the kind of thing I don't want automated away, even when the automation could technically do it.

Reducing build times, again: PurgeCSS, dead Bootstrap JS, and lighter thumbnails #

I have written about build times on this blog before, and not because I've solved the problem once and for all. The December post about a seven-minute build turning into 1.6 seconds locally taught me the first round of lessons: cache RSS fetches with Eleventy Fetch, persist .cache/ across Netlify deploys, and get purgeCSS and Pagefind running in the right order relative to each other. That post ended with a "next steps" line admitting I wasn't sure I had purgeCSS and Pagefind sequenced correctly, and that I'd need to explore running them in parallel once heavy builds were re-enabled. The July 9th sprint is where some of that debt came due, on jcrt.org specifically.

jcrt.org was shipping all of Bootstrap 5.3.8 as its only render-blocking stylesheet — 263 KB raw, for a site that, once you actually measured it, was using roughly 8% of Bootstrap's selectors. A PurgeCSS pass now runs after Eleventy in every build:

"css:purge": "purgecss --css _site/css/bs.css --content \"_site/**/*.html\" --output _site/css/"

That shrinks the served bs.css from 263 KB raw to 38 KB raw — about 7.6 KB compressed — while the full, unpurged Bootstrap build stays in source control so nothing about the underlying framework choice is lost, just the dead weight that was being shipped to every visitor. I deliberately did not defer that stylesheet even at 7.6 KB compressed; deferring layout CSS risks a flash of unstyled content, and at that size there wasn't enough left to defer to make the risk worth it.

The JavaScript side had an even sillier discovery waiting in it. public/js/bs.js was the entire 81 KB Bootstrap JS bundle, shipped for exactly thirty lines of custom sidebar-toggle code appended to the end of it. A search for data-bs- attributes across the built site turned up zero matches. The Bootstrap JS bundle was doing nothing — every interactive behavior it powers requires those data attributes, and the site had none. The 30 live lines moved into a new 1 KB js/menu.js, and the 81 KB bundle (plus its sourcemap, plus an orphaned pagefind-ui.css nobody was linking to anymore) got deleted outright. This friends is why you dont import the whole framework. Lesson learned.

The last piece was images, not code. The homepage's "Recent Journal Issues" section was loading full-size cover scans — roughly 380 KB of images above the fold, on a homepage, for thumbnails. Each recent issue (25.1, 24.2, 24.1, 23.2) now carries a proper thumbnail: field in its front matter — a 480px-wide *_tn.webp render, generated at twice the actual 240px display size for crisp high-DPI screens — sourced from the jcrt-files CDN repo that already exists for exactly this kind of asset. The homepage template now prefers issue.data.thumbnail, falling back to the full image if a thumbnail isn't set. Homepage cover payload: roughly 380 KB down to roughly 87 KB.

The one change major change I made that changed pagefind for that better was refining jcrt-v2's custom Pagefind index cache wrapper in _config/run-pagefind.js — 199 lines down to 75. The deleted half implemented a signature-based cache that could restore a previously-built Pagefind index from .cache/pagefind-index, persisted across Netlify builds, rather than re-indexing from scratch every time. It sounded like a reasonable optimization when I wrote it but it was overly complicated. It is also, per Pagefind's own docs (I had a Claude Sonnet 5 read through pagefind.app's llms.txt to confirm this rather than trust my memory), not a supported mode — there is no ideal way to restore an index and have it guaranteed to match freshly-built HTML from a different Eleventy run. A restored index can drift from the site it's supposedly indexing. Indexing jcrt.org's roughly 4,900 pages from scratch takes seconds, not minutes. The staleness risk of the cache wrapper was never worth the seconds it saved. I deleted my own cleverness and the site got both simpler and more correct at the same time, which is a good trade whenever you can get it.

The quest for four 100s in PageSpeed Insights #

I have not gotten there yet but I will. The clean, headless Lighthouse run against the v3 production build of thewhitestonefoundation.org currently looks like this:

Page Performance Accessibility Best Practices SEO
Home 79 100 100 100
/ai/ 100 96 100 100
/panelists/ 100 94 100 92

Every page is three-for-four. None of them is four-for-four.

The Home page's Performance score is the most legible gap, and most of this post has already explained what's chasing it down: preloading the hero image explicitly (since it's a CSS background-image, and browsers can't discover the priority of an image they can't see in the markup) —

{% if hero and hero.image %}
{# Hero is a CSS background-image, so the browser can't discover its own priority; hint it explicitly since it's the page's LCP element. #}
<link rel="preload" as="image" href="{{ hero.image }}" fetchpriority="high">
{% endif %}

— minifying CSS and JS at build time with esbuild, converting panelist photos to WebP and sweeping roughly 8 MB of unused PNGs out of the repo, and adopting a hard rule going forward: non-favicon images are SVG or WebP, no exceptions. The /ai/ and /panelists/ pages are closer, and their gap sits in Accessibility rather than Performance — which is a different kind of debt, usually contrast ratios, missing labels, or heading order, and one I'd rather fix by reading the Lighthouse audit line by line than by guessing.

What's already paying off, even without four green 100s yet: the native popover mobile menu (no custom JS, no layout-shift risk) —

<button class="nav-toggle" type="button" popovertarget="nav-menu" data-nav-toggle>
  <span class="nav-toggle__open">{{ icons.icon("menu") }}</span>
  <span class="nav-toggle__close" hidden>{{ icons.icon("x") }}</span>
</button>
<div id="nav-menu" class="nav-popover" popover data-nav-popover>...</div>

— the no-JS breadcrumb component emitting BreadcrumbList JSON-LD alongside visible navigation, button contrast brought up to AA in both filled and outline states, explicit width/height on every image so nothing shifts while loading, and the lazy-loaded Pagefind search bundle staying entirely off the critical path until someone actually wants to search. Every one of those is a page not fighting the visitor for their own bandwidth and attention. I'd still like the fourth 100. I'm not going to pretend three isn't real progress in the meantime.

When Cloudflare cached the outage: three layers, one poisoned 404, and a versioned URL #

This is the section I'd have written differently if you'd asked me on the morning of July 9th, because at the time I was fairly sure something was wrong with Pagefind itself.

It started with the simplest possible bug report: search on jcrt.org had stopped working, every query hanging forever on "Searching…". The browser console said Pagefind JS: 1.4.0. Pagefind index: 1.5.2 … you likely have a cached pagefind.js file, alongside a WASM instantiation failure. That message is more honest than most error messages get to be — it was telling me exactly what was wrong on the first try — and I still spent an embarrassing amount of time looking elsewhere first.

jcrt.org is hosted on Netlify, but the domain is proxied through Cloudflare, which also runs a small WAF worker in front of it. That means every request actually passes through three caches, not one: the visitor's own browser HTTP cache, Cloudflare's edge cache, and Netlify's own origin. Only the third one resets when you deploy. The first two keep whatever they cached for as long as the response's headers said to, and — this is the part that actually got me — Cloudflare caches by file extension, independent of what your own Cache-Control headers say the file's actual freshness policy should be. pagefind.js has a stable filename and a long default TTL from the repo's _headers file, so Cloudflare happily served the old cached copy on every request. pagefind-entry.json, sitting right next to it, isn't in Cloudflare's cache-by-extension list, so it was always fetched fresh from origin. The index (fresh, 1.5.2) and the engine loading it (stale, 1.4.0) had drifted apart from two files sitting in the same folder, deployed at the same time, purely because one had a .js extension and the other had .json. Redeploying didn't fix it, because redeploying only purges Netlify — layer three — and does nothing to layers one or two. There was no Cloudflare purge access from inside the repo to reach for.

It got one layer weirder. A Cloudflare zone-level dashboard setting called "Browser Cache TTL" was silently rewriting every cacheable response's max-age to 691200 seconds — eight days — regardless of what the origin's own headers said. So even a correct, intentional Cache-Control header from the repo was getting overridden in flight, at the edge, by a setting nobody on the engineering side of this project could see or change from GitHub.

The fix that actually worked was not "set better cache headers" — headers are policy for future responses; they cannot evict what a cache already has sitting at an old URL under an old policy. The fix was to stop fighting the cache and change the address instead. pagefindVersion is now real global data pulled from node_modules/pagefind/package.json at build time, and every template loads the bundle from a versioned path:

<pagefind-config bundle-path="/pagefind/v/{{ pagefindVersion }}/">

with a Netlify rewrite mapping the versioned path back to the real, unversioned bundle on disk (/pagefind/v/:version/* /pagefind/:splat 200), and _headers split by actual file semantics instead of one blanket rule for everything under /pagefind/:

Cache-Control: public, max-age=0, must-revalidate   # unversioned entry points — always revalidate
Cache-Control: public, max-age=31536000, immutable   # content-hashed index/fragment chunks — cache forever

A brand-new URL has never been cached anywhere, by definition — not in a visitor's browser, not at Cloudflare's edge, not anywhere. Bumping Pagefind's version now rotates the URL automatically, which means this class of bug can't recur silently the next time the site upgrades.

Two footnotes, because both bit me in the same 24 hours. First, while deploying this fix, a pre-deploy probe request hit the new versioned URL before that deploy was live, got a 404, and Cloudflare cached that 404 under its usual long-TTL rules — a poisoned negative cache, at a URL that was correct three minutes later. The fix there was to salt the path once (1.5.2-1) to sidestep the poisoned address, then remove the salt after I asked to have Cloudflare's cache purged manually. Lesson recorded plainly for future-me: never probe a URL before the deploy that creates it is actually live. Second, jcrt.org's Cloudflare zone doesn't live under my personal account — it's a separate login — so anything requiring dashboard access (the cache purges, the Browser Cache TTL change to "Respect Existing Headers") had to go through me directly rather than through whatever automation was doing the code-side work. That's a small organizational fact about who owns what, but it mattered a lot for how quickly this got resolved.

The whole episode is now saved to memory under one name — jcrt-pagefind-cdn-caching — because I do not trust myself to remember it unprompted the next time a stable-filename asset starts feeling haunted. The short version, if you only take one thing from this section: cache lifetime should mirror the identity contract of the URL. A stable name whose content changes needs to revalidate. A name that changes whenever its content changes can be cached forever. Mixing those two contracts on two files in the same folder is exactly how you get a search engine that's stuck talking to yesterday's index.

Where this leaves the network #

Zoom back out and the shape of the last eleven days is fairly simple, even if the individual bugs weren't. 11tyx5 was about proving that five sites could share an architecture without losing their individual identities. This sprint was about proving that shared architecture could also mean shared infrastructure — one metadata model, one search technology, one CORS contract, one Netlify header policy — without turning five independently-edited publications into a single fragile monorepo pretending to be five sites.

The metasearch page is live. The fleet is on a consistent Pagefind Component UI baseline. jcrt.org's search is not just fixed but rebuilt on URLs that can't silently go stale the same way again. Build times keep coming down, for genuinely different reasons each time I write about them, which I think means I'm still finding real waste rather than repeating the same fix. PageSpeed is not four green 100s yet, on any page, and I'd rather tell you that honestly than round up.

There's a smaller, quieter line item buried in the middle of all this that I want to name before closing: jcrt-v2's public/_redirects file has a duplicated block of five rules, repeated at two different line ranges, and it's been failing Netlify's "Redirect rules" check on unrelated pull requests ever since. It's flagged, it's spawned as its own task, and it is exactly the kind of small, unglamorous thing that piles up around an archive this old if nobody goes looking for it on purpose. Consider this me going looking for it on purpose, in public, so it's harder to quietly forget.

The DOI work with KC Commons that I mentioned at the end of 11tyx5 is still the next big milestone, and it's still coming. Everything in this post — the metadata layer, the shared search contract, the version-pinned build tools — is the kind of infrastructure that a persistent-identifier rollout is going to need underneath it anyway. I'd rather have the plumbing sorted out before I ask five sites to also start minting DOIs. One overnight sprint at a time.

Tags : 11ty build-awesome pagefind whitestone-foundation cloudflare web-development static-sites

Webmentions

No webmentions yet.

Previous

Easier Font Awesome SVGs in 11ty

Notes from using the @11ty/font-awesome plugin while migrating a site from Jekyll to Build Awesome, including the small CSS and icon-class fixes that made the SVG output behave.