This is a technology spike — nothing here is a committed HDC deployment.Learn more →

← Back to all entries

Spike evaluation: Astro + content collections + Pagefind + Decap CMS

First full vertical slice through SPECIFICATION.md’s open questions, covering one option from each category (SSG, search, CMS, styling) end-to-end rather than a shallow scaffold of everything. Verified by running the actual build/dev/preview flows locally and querying the built Pagefind index directly in Node, not just by reading code — see “What was actually run” below.

What this spike proves works

Friction encountered (and fixed)

Second layout, images, and environment rendering (SPECIFICATION.md, “Demonstrate alternative layouts and images” / “Individual environments/rendering”)

Search-index-driven listing and pagination (SPECIFICATION.md, “Search-index-driven listings with pagination”)

Open questions for comparing against Next.js / Eleventy

What was actually run (for reproducibility)

npm create astro@latest -- astro --template minimal --no-install --no-git --typescript strict --yes
npm install
npm install -D pagefind decap-server
npm run build         # astro build && pagefind --site dist
npm run preview        # served /, /entries/*, /search/ — verified via curl
npm run cms            # decap-server from repo root; queried its API directly to confirm it lists data/entries

Also run directly against the built output, not inferred from code:

Post-spike code review (senior developer/architect pass)

An independent review of this spike’s code, conducted after completion, from the perspective of a senior developer/architect deciding what carries into the next stage of development. The spike passed its functional bar; the findings below are the patterns that must not be copied verbatim into a product, plus the questions to answer before the first production sprint.

AI declaration: original implementation by Claude Sonnet 5 (claude-sonnet-5); this review by Claude Fable 5 (claude-fable-5), based on a full read of every source file in this spike plus the spec, report, and canonical schema for cross-checking. Every finding below was verified against the actual code, not inferred.

Code-level improvements

Architectural concerns

Security considerations

Questions for the next stage

  1. What is the single source of truth for the data model, and what generates the Zod schema, Decap config, and filter facets from it — codegen, a CI drift check, or accepted manual sync?
  2. Zero automated tests and no CI exist. What’s the minimum harness before iteration two — ajv schema validation as a build gate, a Playwright “Sangya” smoke test, a deploy workflow?
  3. Sveltia (the other CMS candidate) was never trialled despite being in scope, and Decap’s real GitHub backend (OAuth app, PR-based editorial workflow, per-environment config) is untested — when do these get decided?
  4. HDR UK convention gap: no MIT/copyright headers exist in any new source file, per AGENTS.md — sweep before this goes public.

Addressed since this review

The “Search-index-driven listing and pagination” round (see that section above) resolved several of the findings listed here, verified by rebuild: the stored-XSS innerHTML interpolation in searchDemo.ts (all client-rendered card fields now pass through an escapeHtml helper), the duplicated entry-card markup (one shared entryCardHtml template now), the fragile |-delimited data-* corpus encoding (removed with the server-rendered cards), the weaker dev-mode fallback search (now substring-matches the corpus’s full body, so “Sangya” passes pre-build too), the missing aria-live on the result count (added), and the review’s original question about render-everything-and-hide at scale (answered — the listing is now index-driven and paginated, per the spec’s new section). Separately, the 3-facet /search/ page was removed outright after user testing found it non-functional and redundant against the now-complete homepage search. Still open from this review: the schema-sync strategy, tests/CI, CMS backend decisions, Infinity in computeFilterOptions, and URL-reflected filter state.