The Content Is the API
Three systems shipped in the same redesign looked, from a distance, like one reusable idea. Up close they weren’t coupled at all — and the harder, more useful decision was figuring out which one was actually worth pulling out on its own.
3 — agent layer, lens entry system, fit-brief generator
Assessed each against reuse value and coupling, not just whether it shipped1 — the agent layer only
The other two were real engineering, just not reusable in the way the redesign’s own plan assumedStandalone Astro template, GitHub template repo intended
Rejected an npm package and an Astro integration as the wrong shape for what’s actually being sharedPublic on GitHub, marked as a template repo
github.com/blue148/agent-layer-template — CI groundedness eval passes 3/3 against a real Anthropic credentialThree systems, one assumption
This site’s redesign shipped in stages. Cycle 1 built a static, agent-facing content layer — every case study, rendered at build time into a short index (llms.txt), a complete document (llms-full.txt), per-page markdown mirrors, and JSON-LD, all sourced from the same content collections that build the HTML. Cycle 2 built a lens-based entry pattern — a dismissable, persisted prompt that routes a visitor into a curated sequence based on why they’re here, degrading to full static content with JavaScript off. Cycle 3 built an offline fit-brief generator — a two-phase CLI that reads a job description, matches it against case studies using the same agent-facing facts Cycle 1 produced, and writes a static, unlisted page.
The plan’s own scope for this phase was one line: extract the pattern into a reusable, documented codebase. Singular. It read like these three things were one idea that happened to ship across three cycles — which made sense on paper, since all three touch content, and two of them explicitly reuse Cycle 1’s output. The obvious move was to package all three together and call it “the redesign, extracted.”
I didn’t trust that framing enough to start building against it. Before writing any extraction code, I went back through all three systems and asked a narrower question of each one: if I handed just this piece to someone building an unrelated site, how much of it would they actually keep, and how much would they have to tear out?
The diagnosis
The agent layer turned out to be almost entirely mechanism. qaContext — a structured, optional field co-located with the content it describes — has no assumptions about what the content actually is. The generator that projects it into llms.txt/llms-full.txt hardcodes a handful of things specific to this site (a site URL constant, an About/Experience special-case, an exact field list), but the core loop — iterate a collection, render its facts, write a file — doesn’t care whether the collection holds case studies, blog posts, or product docs. The CI groundedness eval that checks the generated document against ground-truth questions is cleaner still: nothing in the harness itself references this site at all, only the ground-truth file does, and replacing that file is the entire point.
The lens system was real engineering — a no-JS-first state machine handling URL params, localStorage persistence, and session-scoped dismissal, plus two genuinely reusable primitives (an escape hatch, a persistence helper). But the part doing the actual routing work is a hardcoded union of four personas and the literal copy for each one, including a line as specific as “I’m evaluating Jason for a role.” Another site adopting this pattern doesn’t want my personas. What’s reusable is a smaller kernel inside a larger, more bespoke surface: a dismissable, persisted, URL-overridable entry choice that degrades to static content. Real, but narrower than it looks from the outside.
The fit-brief generator had a different problem — not coupling, but validation. Its two-phase generate/review/commit flow and its prompt-injection hygiene (a pasted job description is always treated as content to analyze, never as instructions) are ideas worth reusing on their own. But at the point I was investigating this, the script had never actually produced a real fit brief. src/content/fit-briefs/ was empty. Extracting and genericizing a pattern that’s never been exercised against a single real input isn’t extraction — it’s guessing that the current shape is right before anything has tested that assumption.
The decision
Bundling all three into one “extracted pattern” would have manufactured a shared abstraction between things that aren’t actually coupled — they shipped in the same project, not from the same design. The agent layer is a build-time content projection. The lens system is client-side progressive enhancement over static routing. The fit-brief generator is an offline CLI. Nothing in one depends on the other; the PRD driving this redesign says as much explicitly for Cycle 3 and Cycle 2.
I scoped the extraction to the agent layer alone. It has the highest reuse value of the three — portable to any Astro site built on content collections, regardless of what the content actually is — and the lowest genericizing cost: pulling a few hardcoded constants into a config file, not a rearchitecture. It’s also the most differentiated idea on its own. “Treat agents as first-class consumers of your content, the same way a design system treats agents as first-class consumers of design tokens” doesn’t need the lens system or the fit-brief generator to be a complete thesis.
Packaging was its own decision, weighed against three alternatives. An npm package implies a semver and maintenance commitment out of proportion to a portfolio project’s appetite — and half the value here isn’t “install a dependency,” it’s “copy this schema shape into your own content.config.ts and adapt it.” An Astro integration is the wrong API surface entirely; these are page, schema, and script conventions, not renderer-level hooks. A documented reference with no formal packaging was the lowest-effort option, but the weakest forcing function — nothing would actually require the genericizing work to happen; it’s easy for docs to gesture at “swap this for your content” without the swap being clean. A GitHub template repo sits between all three: someone can click “use this template” and get a working site with the agent layer wired up, which forces every site-specific string to actually become a placeholder, without taking on a package’s versioning burden.
What shipped
The template is a standalone Astro project: a generic items content collection standing in for whatever your primary content type is, the qaContext schema copied as-is, a genericized generator reading site name and URL from one config file instead of a hardcoded constant, the same three routes (llms.txt, llms-full.txt, per-item markdown mirrors), parameterized JSON-LD, and the CI eval harness with a small example ground-truth file in place of this site’s actual content.
One piece of the original code got fixed on the way out, not just copied: the generator tracked which published entries were missing qaContext but never surfaced that list anywhere — no build warning, no CI gate, a signal nobody was reading. The template version logs it as a build warning instead. Small, but worth naming, because it’s exactly the kind of dead tracking code a template shouldn’t silently inherit.
I confirmed llms.txt, llms-full.txt, the per-item markdown mirrors, and both levels of JSON-LD all render correctly against two example content entries — that was true before the eval ran once. Running the ported eval harness for real, against a live Anthropic credential, is what actually caught a problem: one ground-truth question required the literal digit ”5”, but the model’s correct answer spelled it out as “Five channels,” so a right answer failed the check. The fix was the same alternate-phrasing pattern this site’s own ground-truth file already used elsewhere for exactly this reason — I just hadn’t applied it to every question. That’s the specific value of actually running an eval instead of trusting that a well-built harness implies a well-built ground-truth file: the harness was fine. The test data wasn’t. With that fixed, the eval passes 3/3, and the template is public at github.com/blue148/agent-layer-template, marked as a GitHub template repo.
Knowing what not to generalize
The lens system and the fit-brief generator aren’t in the template, and that’s stated in its README as plainly as what is included — not as a hedge, but because leaving them out was itself the harder decision to make well. It would have been easy to extract all three and call it thorough. It would have been just as easy to extract nothing and call the whole investigation a wash because it didn’t produce one clean “the pattern.” Neither of those is what the evidence supported. One of the three systems had a real, portable idea at its core; the other two didn’t, for two different reasons — one because its reusable core was smaller than its bespoke surface, the other because it had never been tested against reality yet.
That’s the same instinct this site’s case studies keep returning to elsewhere: a partial result, stated honestly, is more useful than a complete-sounding one that overclaims. This one just happens to be about the site’s own construction instead of a client’s product.
Tools
- Astro content collections
- Zod
- Anthropic API (Claude)
Agent view
This page has a machine-readable twin, generated from the same source rather than written alongside it.
See how an agent reads this page