Components

Every component in src/components/ takes props and prints. None of them fetches content: the page gathers the data and hands it over. That is what keeps a page readable as a page — you can see what is on it by reading one file.

Three groups: common/ (the chrome and the plate), article/ (listing and reading) and mdx/ (what you can put in the body of a piece).

The plate

The cover plate is the theme. It is two components, not one, so the same photograph can carry different type: a photograph with Plate, the type on it with PlateHead, and — on the home page and the desk fronts — a column of teasers with CoverLines.

Component Props What it is
Plate image alt credit? focus? priority? class? The photograph, its crop, its credit, and whether it loads eagerly. Whatever sits on it is passed as children
PlateHead kicker? spot? title deck? href? as? id? class? The type laid on a plate: label box, headline, deck. spot puts the label in the spot colour
CoverLines items omitKicker? as? class? The signature column of teasers. How it is set comes from coverLines in config.ts, not from props

Plate only handles the photograph. The scrim and the type colours are the .plate block in global.css, and the column on a plate stays inside 32rem — the comment there says why the scrim depends on it.

priority belongs to exactly one plate per page, the one above the fold. On the home page that is the cover; everywhere else it waits.

The cover-line layouts

CoverLines reads coverLines.layout rather than taking it as a prop, because the arrangement is a property of the magazine and not of the page. Three values, all in src/styles/cover-lines.css:

layout How the column is set
stack The entries run down the plate. The default
numbered The same, with a mono ordinal in the margin. Reads as a contents page; pair with divider: 'rule'
columns The entries run across the foot of the plate, and stack again below 48rem

columns stacks on a phone on purpose: three headlines side by side at 360px are three headlines two words wide.

The chrome

Component Props What it is
PageHead kicker title lede folio? class? The standing head on an index page. The colophon, the series index, tags, the archive and search all open with it, so the back-of-book pages cannot drift into four different openings
SEO title? description? image? type? jsonLd? noindex? All <head> meta. The only file that writes it. Pages pass it through Base
Author author role? links? href? A contributor: portrait, name, role, bio. At the foot of an article and down the colophon masthead
ThemeToggle – Light/dark. The value lands on <html data-theme>, which flips color-scheme
Analytics – The analytics tag. Emits nothing until analytics.provider is named
Newsletter – The subscribe plate. A plain form POST, no script
ContactForm – Pro. The letters box on the colophon, over Web3Forms
LocaleSwitch – Pro. Two short codes beside search. Only languages something is filed in

PageHead’s folio is the mono count line under the lede — “18 stories”, “6 parts”. Omit it where there is nothing to count.

Author takes role rather than reading it off the entry, because the colophon masthead already groups by title and prints only the qualifier after it.

The article

Component Props What it is
Card item layout? size? kicker? deck? priority? class? One article with its photograph. layout: 'row' sets the picture beside the type; size: 'lead' gives it the weight of a lead
Rundown items numbered? desk? class? A column of small pictures and headlines. Related reading, and a series’ parts
Rail blocks? headings? more desk deskHref beside? The column beside the body: the blocks the piece asked for, then what else the desk has filed
RailToc headings label? Pro. The toc rail block, built from the piece’s own h2 and h3
SeriesNav series number total previous? next? Pro. Where this part sits, and the two pieces either side of it
Comments – Pro. The giscus thread. Loads nothing unless comments is wired
CodeCopy – Pro. Puts a copy button on every code block in the body

Card always prints in the same order — picture, label, headline, deck, folio — because that is the order a reader scans them. kicker: false and deck: false are for the places where the label would only repeat the desk bar or the heading above it.

Rail is a column beside the body only from 64rem up, and only where the page asked for one with beside. Everywhere else it prints under the body, which is why the figures are a description list and not a boxed sidebar.

CodeCopy is driven from a script rather than from a component you wrap each block in. A fenced block is the natural way to write code in Markdown, and nobody should have to stop writing Markdown to get a copy button.

In the body (MDX)

Import these into a .mdx article. They are Pro.

Component Props What it is
Callout label? tone? An editor’s note, a method box, a definition. tone: 'spot' puts the rule and label in the spot colour — one box per piece, at most
Pullquote cite? A line lifted out of the column and set beside it from 48rem up
Figures caption rows source? A table of figures with the proportions drawn in. rows is { label, value, display? }[]

A Callout is a component and not a blockquote because a blockquote in this theme is a pull quote, and giving it a second job would flatten both.

Figures reads its proportions against the largest row, not against a total: the sets that go in here are usually not parts of one whole — years, departments, vendors. For three headline numbers, use the rail’s stat blocks instead.

Share cards

src/lib/og.ts is not a component, but it is the other place a plate is drawn. An article’s card is its own photograph, cropped to 1200×630 and baked to JPEG at build. The pages with no photograph of their own get a drawn plate instead, and the whole list of them is the cards array:

export const cards: Card[] = [
  { path: '/colophon', kicker: 'About', title: 'Colophon' },
  { path: '/archive', kicker: 'Record', title: 'Archive' },
  // ...
];

path is the pathname with no trailing slash; kicker and title are written for the card rather than read off the page, because the wording that works on a page is often too long for a card. A page not listed here keeps site.defaultOgImage, so removing a row is a downgrade, never a broken card.

Two things to know before you edit it. The plate constant at the top of the file mirrors the plate tokens — satori reads no stylesheet, so a colour preset has to be repeated there to reach the cards. And the OG fonts are latin subsets, like the webfonts: a title that needs another script should keep the photograph.

Fonts

src/fonts.ts is the third file that is not a component and still changes every page. It holds two whole alternatives to the faces the theme ships with, kept as data so that a swap is one word rather than nine edits:

export const pairing = pairings.quarto;

quarto is Source Serif 4 with Archivo; wire is Inter with Libre Franklin. null keeps the faces written out in astro.config.mjs. Each pairing keeps the same three cssVariable names, so nothing else in the theme changes. Writing your own is in customization.md.

Writing a component of your own

  • Props only. If it needs content, the page fetches it and passes it down.
  • No client-side framework. A <script> tag and CSS, the way ThemeToggle and CodeCopy do it.
  • Colours come from the tokens. No hex value in a class.
  • Images go through <Image> from astro:assets, out of src/assets/. Never a raw <img>.
  • Words the theme prints belong in src/i18n/<locale>.ts, not in the component.
  • One component per file. No barrel files.