Agent-Built · Part 2 of 4
The Authoring Surface
Every primitive this blog is written with: static MDX components, build-time diagrams and math, and the one React island that earns its JavaScript.

On this page
This blog is written mostly by coding agents, and that changes what “easy to author” means. The easiest thing for an agent is a small, explicit vocabulary with a validator that says exactly what went wrong, not a big flexible one with conventions you have to remember. Part 1 covered the validators; this part is the vocabulary. Every component the site’s posts are written with appears below, in the order an author reaches for them, doing its actual job; this page is its own demo. Nearly all of them ship no script beyond a few inline lines; the post ends with the one real exception, a React island, and the case a component has to make before it’s allowed to be one: the reader genuinely needs to interact with it.
Why static first#
A blog post is a document. Documents don’t need a JavaScript framework to be read, and every
kilobyte of script is a kilobyte the reader downloads before the text settles. So the rule: a
prose, code, or diagram post ships zero external scripts: no React, no framework runtime,
nothing but a few lines of inline script for the theme toggle, the table of contents, and a
couple of the primitives below. The e2e suite enforces it as a budget: script[src] count on a
prose page must be exactly zero.
Static primitive
- Rendered once, at build time
- Works with JavaScript disabled
- Styled by the same tokens as everything else
- Zero bytes of framework script
Hydrated island
- Ships React plus the component
- Needed for real interactivity (state, input, animation)
- Loaded lazily, usually when scrolled into view
- Justified per post, never by default
Prose, code, and images#
Plain Markdown covers most of a post: paragraphs, links, lists, emphasis, and fenced code with syntax highlighting that switches with the theme.
// Frontmatter dates are UTC calendar dates; render them only through this helper.
export function formatDate(date: Date, style: "medium" | "long" = "medium"): string {
return new Intl.DateTimeFormat("en-US", { dateStyle: style, timeZone: "UTC" }).format(date);
}
Images live next to the post and go through Astro’s image pipeline: resized variants,
srcset, intrinsic dimensions, and lazy loading, from a single line of Markdown.

When an image needs a caption or a wider measure than the text column, it goes in a Figure:

Figure also takes a zoom flag: it wraps a plain <img> in a button that opens a full-size
lightbox, for screenshots whose fine print matters.
Tables are plain Markdown too. Every table is automatically wrapped so that on narrow screens it scrolls sideways inside its own box instead of pushing the page wider; the author does nothing:
| Check | Where it runs | Fails the build? |
|---|---|---|
| Frontmatter schema | content loader | yes |
| Unknown series or tag | type system | yes |
| Duplicate/reserved slug | validatePosts | yes |
| axe WCAG 2.2 AA | e2e | yes |
| Horizontal overflow @360w | e2e | yes |
When a list is an inventory of short items rather than an argument, ListGrid turns the tall
bullet column into a grid of boxes. It wraps a plain Markdown - list, so like Steps it
degrades to exactly that. Everything that lives in one post’s directory:
index.mdxhero.png- diagram SVGs
- screenshots
components/*.tsx- component tests
Emphasis without shouting#
Two components handle “the reader should notice this”, and they are deliberately different.
Quotations get attribution and, when there is one, a source link:
Plan to throw one away; you will, anyhow.
Procedures and alternatives#
Steps is an ordered list with a connector line. The Markdown stays a plain 1. list, so it
degrades to exactly that anywhere else the text is shown.
- Create
src/content/posts/<slug>/index.mdxwith valid frontmatter (a scaffold script does this). - Write the post using the primitives on this page. Put images and any components next to it.
- Run
npm run validateandnpm run build. If anything is off (an unknown tag, a duplicate series order, a missing alt) the build says what and where. - Set
draft: falseand push. CI builds, runs the accessibility suite, and deploys.
Tabs are for equivalent alternatives, never for hiding content. Arrow keys move between them, Home and End jump to the first and last, and without JavaScript the panels simply stack.
Markdown image
Figure with caption
<Figure caption="What the diagram shows." width="wide">
<Image src={diagram} alt="Alt text that describes the image" />
</Figure>Long, optional material folds away in a native <details> element, no script:
The full frontmatter contract
title(≤ 60 characters; it is the post’s entire<title>)description(40–160 characters, used for search snippets and social previews)publishedAt, optionalupdatedAt(never earlier thanpublishedAt)series(must exist in the registry) andseriesOrder(unique within the series)tags(each must exist in the registry)authority(the repository that owns this post’s domain facts; see part 3)draft(drafts are dev-only: never built, listed, fed, or indexed)hero { src, alt }(required, 1500×600) and optionalogImage
Code that wants a name#
Bare fenced code is the default. When a snippet is a file, or long enough that someone will want
to copy it, CodeBlock adds a header and a copy button: one small script for the whole page,
not per block, and the “Copied” confirmation is announced to screen readers.
/** Build a site-relative href that respects Astro's base. Never hardcode "/blog/". */
export function href(path: string): string {
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
}Heavier media#
Three primitives carry media too heavy to load unasked, and each makes the reader opt in.
Video embeds a local file or a YouTube player, with a required accessible title. Gallery
steps through a set of screenshots one at a time with prev/next arrows and a full-size escape
hatch; the TV data explorers post walks whole interfaces that way.
MuseumEmbed is the click-to-load frame around this site’s preserved live demos, so a
multi-megabyte 2014 page costs nothing until the reader presses the button;
Drag the Timeline, Refilter the Page is built around
one. None of them appear on this page — this post has no video, screenshot set, or museum demo.
That absence is the rule in action: a primitive shows up when its content does, never for
decoration.
Diagrams and math#
Diagrams are hand-drawn SVG files that live next to the post and render inline at build time; the reader gets a picture, not a script that draws one. The house style is pictorial by default: a rail line, a staircase, a magnifier, a drawing of the mechanism rather than a grid of labeled boxes. Every diagram gets a sentence describing it and a name and summary on its figure, because a picture without words is invisible to a screen reader. This one shows the path from an MDX file to a deployed page:
Because each drawing keeps its neutral strokes and text on currentColor, the same file follows
the reader’s theme natively: no dark-mode filter, no second render.
Math uses TeX syntax and renders through KaTeX, also at build time. Inline, reading time is minutes for words; the display form of the same idea:
Neither diagrams nor math adds a byte of JavaScript to the page.
The ladder#
There is no chart library on this site and no third-party widget behind any of the primitives above — even the gallery and the museum embed are a few dozen lines of inline script, not a dependency and a maintenance promise. When a post wants a visual, the house rule is a ladder: stop at the first rung that holds.
A flow, a state machine, a mechanism: that’s a hand-drawn picture. A chart with fixed data is an SVG. A chart the reader can filter is an island. That last rung is the rest of this post.
Islands, not apps#
Astro renders every page to static HTML. A component marked with a client:* directive is an
island: it renders to HTML at build time like everything else, and then, in the browser, only
that component’s JavaScript loads and takes over its own DOM. The rest of the page never
hydrates.
Below is the smallest possible island, a counter, living in this post’s own directory as
components/Counter.tsx. It hydrates when scrolled into view (client:visible), so a reader who
never gets this far never downloads it.
import Counter from "./components/Counter";
<Counter client:visible />
That’s the entire authoring surface for interactivity: import a component that lives next to the post, add a directive. The component is ordinary React built from the same design tokens as the site chrome (an island can’t invent its own look, so demos age with the theme instead of against it), and it has an ordinary unit test next to it.
Good reasons for an island:
- The reader manipulates it: sliders, filters, a search box, a playground.
- It animates in a way CSS cannot express, and the animation carries meaning.
- It fetches or computes something on demand that would be absurd to precompute.
Bad reasons:
- It was easier to write in React. (It usually isn’t, once you count the hydration.)
- A library exists for it. (The library is now the reader’s download.)
- Everything else on the site is React. (Almost nothing on this site is React.)
Post-specific components stay in the post’s directory, with their tests, and are promoted to the shared library only when a second post needs the same thing, which is also when it becomes worth designing an API for. Site chrome (header, footer, theme toggle) is never an island; those are plain Astro with a few lines of inline script. The one exception in the whole site is the archive explorer: full-text search, tag and series filters, and sortable listings genuinely need to respond to the reader, so that page hydrates one island and lazily fetches its search index.
Every primitive on this page was placed by an agent following the same catalogue you have just read. How a post actually gets written, reviewed, and shipped, including the part where other repositories are consulted as domain experts, is part 3.