Good typography is invisible. You notice it only when it fails: when a line runs so long your eye loses its place, when a heading floats closer to the paragraph above than the one it introduces, or when a code block shouts over the prose around it. This post is a field test. It uses every element the theme supports, so each one can be checked at a glance in light and dark, on a phone and on a wide monitor.
The body is set in a variable serif with optical sizing, so small text gets sturdier strokes and large headings get finer detail. Paragraphs use text-wrap: pretty to avoid lonely last words, and the measure stays around 66 characters. That’s near the classic sweet spot of 45 to 75 characters per line.1
Inline elements
Within a paragraph you’ll find strong emphasis, italic emphasis, and both at once. There are links that sit quietly until you hover, inline code for identifiers like useState(), and highlighted passages for the bits worth rereading. Keyboard shortcuts look like ⌘ + K. Chemistry needs subscripts, as in H2O, and footnote-style references need superscripts, as in x2. Abbreviations such as CSS carry their expansion on hover.
Posts link to each other with wiki-links. [[colophon]] becomes Colophon, titled for you; [[colophon#Tools|the toolchain]] reads the toolchain and lands on that heading; [[#Callouts]] jumps to Callouts on this page. Links within the site keep a plain underline, while links that leave it, such as the Astro docs, carry a small arrow.
Smart punctuation turns “straight quotes” into curly ones, -- into an en dash for ranges like 1990–2026, --- into an em dash for asides — like this one — and three dots into an ellipsis…
Old-style figures
Numbers inside prose use old-style figures, which sit in the text like lowercase letters: the build took 1,284 ms across 37 pages in 2026. Tables switch to lining, tabular figures so columns of digits line up.
Headings and rhythm
Headings are tighter than body text and sit closer to the paragraph they introduce than to the one before them. The table of contents on the right is built from the h2 and h3 headings on this page. It stays pinned while you scroll and highlights the section you’re reading.
A third-level heading
Third-level headings are smaller and still bold. They appear indented in the table of contents.
A fourth-level heading
Fourth-level headings switch to italic and are left out of the table of contents to keep it calm.
Quotations
The details are not the details. They make the design.
Charles Eames
Quotes can nest when a conversation needs it:
Someone once wrote to me:
Typography exists to honor content.
I’ve kept that note above my desk ever since.
Callouts
Callouts come in five flavors. You can write them as GitHub alerts, which keeps posts portable:
Or as container directives, which can hold richer content:
Lists
An unordered list:
- Typography first: serif body, generous leading, comfortable measure
- Warm colours
- Paper background in light mode
- Warm ink in dark mode
- Fast pages with almost no JavaScript
An ordered list:
- Write the post in Markdown or MDX.
- Run
pnpm devand read it in the browser. - Publish.
A task list:
- Pinned table of contents
- KaTeX at build time
- Write the next post
Definition lists use HTML:
- Measure
- The length of a line of text, usually counted in characters.
- Leading
- The vertical space between lines, named after the strips of lead in metal type.
Mathematics
KaTeX renders math at build time, so pages ship no math JavaScript. Inline math such as or sits on the baseline with the text around it.
Display math gets its own line:
Multi-line derivations use aligned:
Matrices work too. Here’s scaled dot-product attention:
An equation that is too wide for a phone screen scrolls sideways instead of breaking the layout:
Code
Code blocks use Expressive Code. Each one has a frame, a copy button, and syntax colours tuned to the warm palette.
/** Estimated minutes to read `markdown`, at ~230 words per minute. */export function readingTime(markdown: string): number { const words = markdown.split(/\s+/).filter(Boolean).length; return Math.max(1, Math.round(words / 230));}Line numbers, inserted and deleted lines, and word highlights:
import numpy as np
def softmax(z): return np.exp(z) / np.exp(z).sum() return np.exp(z - z.max()) / np.exp(z - z.max()).sum()A terminal frame for shell sessions:
pnpm create astro@latest -- --template chintak/astro-sagepnpm devA diff:
const theme = "astro-paper";const theme = "astro-sage";Rust, with a collapsed section:
4 collapsed lines
use std::collections::HashMap;use std::fs;use std::io::{self, Read};
fn word_counts(text: &str) -> HashMap<&str, usize> { let mut counts = HashMap::new(); for word in text.split_whitespace() { *counts.entry(word).or_insert(0) += 1; } counts}A very long line scrolls horizontally instead of wrapping:
const url = new URL("https://example.com/api/v1/posts?include=author,tags,series&sort=-pubDatetime&page[size]=20&page[number]=1&filter[draft]=false");Diagrams
Diagrams are rendered at build time to inline SVG, so their text is selectable and searchable, and they follow the light and dark palettes. Hover a diagram and press the expand button (or double-click it) to open the viewer: scroll or pinch to zoom, drag to pan, or switch to text selection.
A Mermaid flowchart:
A Mermaid sequence diagram:
An Excalidraw sketch, drawn on excalidraw.com and saved next to the post:
You can also paste a drawing straight into the post: in Excalidraw, select everything and choose Copy to clipboard as JSON, then paste it into an excalidraw block. Nothing needs downloading:
Or link a shared drawing with . It’s fetched and decrypted once at build time, then cached.
Wide and full-width diagrams
Diagrams use the same two sizes. Add wide to the fence (or "wide" as an Excalidraw image title) to span the column and the table-of-contents rail, which steps aside while it passes:
Use full to go edge to edge:
Tables
A numeric table with tabular figures, so the digits line up:
| Model | Params | Top-1 (%) | Latency (ms) |
|---|---|---|---|
| ResNet-50 | 25.6M | 76.1 | 4.1 |
| ViT-B/16 | 86.6M | 81.1 | 8.7 |
| ConvNeXt-T | 28.6M | 82.1 | 4.9 |
| EfficientNet | 5.3M | 77.1 | 2.6 |
A wide table scrolls inside the column, never the page:
| Feature | AstroPaper | AstroSage | Notes | Since | Status | Owner | Docs |
|---|---|---|---|---|---|---|---|
| Serif body | No | Yes | Source Serif 4 with optical sizing | 0.1 | Shipped | theme | Typography |
| Pinned TOC | No | Yes | Right rail with scroll-spy | 0.1 | Shipped | theme | Layout |
| KaTeX | Guide | Built-in | Rendered at build time | 0.1 | Shipped | markdown | Math |
Figures
An image with a caption:
Figures come in three sizes. The default stays in the text column. size="wide" spans the column and the table-of-contents rail:
size="full" bleeds edge to edge. The caption stays on the reading column:
In plain Markdown, use the image title:  or "full".
Footnotes
Footnotes collect at the end of the post, each with a link back to its reference.2
Wrapping up
That covers the full set. If any of it looks off (a heading too close, a code block too loud, a colour too cold), it’s a bug in the theme, not in your writing.