Skip to main content
Jason Sonderman, UXMC, CPACCJason Sonderman

The stack

Astro renders every page as static HTML first; React only shows up where something is genuinely interactive — the theme toggle, the contact form, the header nav, the chat assistant — each mounted as its own island rather than one client-side app owning the page. Case studies, articles, and shorter snippets are MDX, checked against a shared schema in content.config.ts so a missing field fails the build instead of failing silently in production. Tailwind v4 handles styling, Radix UI backs the few components that need real interaction primitives, and Netlify builds and serves the result.

One constraint runs underneath all of it: the guided parts of this site have to degrade to the same static content with JavaScript turned off. Not as a nicety — the content lives in the same collection either way, so there’s nothing for a script to gate.

The agent layer

Every case study can carry an optional qaContext field in its frontmatter: a plain-language summary, a short list of labeled facts, and sometimes a scope note about what the piece doesn’t claim. It sits next to the prose it describes rather than in a separate corpus, on the idea this redesign keeps coming back to — treat an agent reading the content the way a design system treats an agent reading its tokens, as a first-class consumer, not an afterthought bolted on later.

At build time, that field gets projected three ways: a short index at /llms.txt, a complete document at /llms-full.txt, and a markdown mirror of each case study rendered from the same source as its HTML — the link at the bottom of every case study, "see how an agent reads this page," points at exactly that file. JSON-LD runs alongside all of it, a CreativeWork block per case study and a Person block site-wide, for crawlers that read structured data rather than prose. None of this is generated once and left to drift: a CI check runs real questions against the generated document over a live model and fails the build if an answer stops being grounded in what’s actually written.

The full account of why this exists and how it’s assembled is its own case study: The Content Is the API.

What got extracted, and what didn’t

The agent layer above turned out to be almost entirely mechanism — nothing in it assumes the content is a case study rather than a blog post or a product doc — so I pulled it into a standalone, public template rather than leaving the pattern implicit in this one repo: github.com/blue148/agent-layer-template (opens in a new tab) .

Two other systems shipped in this same redesign and deliberately stayed put. The lens entry pattern routes visitors by intent, but its routing logic is a hardcoded union of four personas and copy specific to this site — the reusable part is a smaller kernel than the surface around it, and pulling the whole thing out would have meant handing someone else my personas along with the mechanism. The fit-brief generator had a different problem: at the point I was deciding what to extract, it had never produced a real brief against a real job description. Generalizing a pattern that hasn’t been exercised once is a guess dressed up as engineering, not extraction.

The harder decision here wasn’t building the template. It was deciding what didn’t belong in it.

Built with an agent, not just about one

Most of this site’s code, this page included, gets written in collaboration with an AI coding agent working from a CLAUDE.md file checked into the repo — the rules for voice, scope, and the no-JS constraint above live there, not in my head alone. I make the product calls and own the voice; a good amount of the diff itself is drafted by the agent against those rules and then reviewed before it ships. It’s a genuine collaboration, not full autonomy in either direction — which is more or less the argument the rest of this site is making about where that model does and doesn’t work.