Skip to content
Always Shippable

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.

9 min read
Stacked authoring blocks (callout, tabs, code) with one island widget marked by a lightning bolt
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.

A schematic card on a grid background, standing in for the screenshots real posts carry

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

A schematic card on a grid background, rendered wider than the text column
The same image, wrapped in a Figure with a caption and a wide breakout.

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:

CheckWhere it runsFails the build?
Frontmatter schemacontent loaderyes
Unknown series or tagtype systemyes
Duplicate/reserved slugvalidatePostsyes
axe WCAG 2.2 AAe2eyes
Horizontal overflow @360we2eyes

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.mdx
  • hero.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.

— Fred Brooks, The Mythical Man-Month

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.

  1. Create src/content/posts/<slug>/index.mdx with valid frontmatter (a scaffold script does this).
  2. Write the post using the primitives on this page. Put images and any components next to it.
  3. Run npm run validate and npm run build. If anything is off (an unknown tag, a duplicate series order, a missing alt) the build says what and where.
  4. Set draft: false and 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

![Alt text that describes the image](./diagram.png)

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, optional updatedAt (never earlier than publishedAt)
  • series (must exist in the registry) and seriesOrder (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 optional ogImage

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.

src/lib/url.ts
/** 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:

From source to deployed pageMDX source is validated, built to static HTML, tested, and deployed to GitHub Pages; a validation error sends the file back to be edited.

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 max⁡(1,round⁡(w/230))\max(1, \operatorname{round}(w / 230)) minutes for ww words; the display form of the same idea:

tread=max⁡(1,round⁡(w230))t_{\text{read}} = \max\left(1, \operatorname{round}\left(\frac{w}{230}\right)\right)

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.

The visual ladderFour rungs in order of preference: hand-drawn pictorial SVG, static SVG chart, HTML and CSS, React island. The first three cost zero script; only the island ships JavaScript, and only when the reader must manipulate the visual.

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.

A page outline with one hydrated islandMost of the page is plain text blocks; a single small block near the bottom is highlighted as the island, so client JavaScript is a small fraction of the document.
One island in a sea of static HTML. Only the highlighted block loads script.

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.

Counter demoA button that counts its own clicks; the count updates without a page reload.
Hydrated React island
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.