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 wayThemeToggleandCodeCopydo it. - Colours come from the tokens. No hex value in a class.
- Images go through
<Image>fromastro:assets, out ofsrc/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.