Skip to content
AstroSage

Building AstroSage

AstroSage: every element, set in type

A single long post that exercises every feature of the theme: typography, math, code, callouts, tables, figures and footnotes.

On this page
  1. Inline elements
  2. Old-style figures
  3. Headings and rhythm
  4. A third-level heading
  5. Quotations
  6. Callouts
  7. Lists
  8. Mathematics
  9. Code
  10. Diagrams
  11. Wide and full-width diagrams
  12. Tables
  13. Figures
  14. Footnotes
  15. Wrapping up

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:

  1. Write the post in Markdown or MDX.
  2. Run pnpm dev and read it in the browser.
  3. 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 eiπ+1=0e^{i\pi} + 1 = 0 or ∇θL(θ)\nabla_\theta \mathcal{L}(\theta) sits on the baseline with the text around it.

Display math gets its own line:

softmax⁡(z)i=ezi∑j=1Kezj\operatorname{softmax}(z)_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}

Multi-line derivations use aligned:

L(θ)=−1N∑n=1Nlog⁡pθ(yn∣xn)∇θL=−1N∑n=1N∇θlog⁡pθ(yn∣xn)\begin{aligned} \mathcal{L}(\theta) &= -\frac{1}{N} \sum_{n=1}^{N} \log p_\theta(y_n \mid x_n) \\ \nabla_\theta \mathcal{L} &= -\frac{1}{N} \sum_{n=1}^{N} \nabla_\theta \log p_\theta(y_n \mid x_n) \end{aligned}

Matrices work too. Here’s scaled dot-product attention:

Attention⁡(Q,K,V)=softmax⁡ ⁣(QK⊤dk)V,Q=[q11q12q21q22]\operatorname{Attention}(Q, K, V) = \operatorname{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V, \qquad Q = \begin{bmatrix} q_{11} & q_{12} \\ q_{21} & q_{22} \end{bmatrix}

An equation that is too wide for a phone screen scrolls sideways instead of breaking the layout:

p(x1,x2,…,xn)=p(x1) p(x2∣x1) p(x3∣x1,x2)⋯p(xn∣x1,…,xn−1)=∏i=1np(xi∣x<i)p(x_1, x_2, \ldots, x_n) = p(x_1)\, p(x_2 \mid x_1)\, p(x_3 \mid x_1, x_2) \cdots p(x_n \mid x_1, \ldots, x_{n-1}) = \prod_{i=1}^{n} p(x_i \mid x_{<i})

Code

Code blocks use Expressive Code. Each one has a frame, a copy button, and syntax colours tuned to the warm palette.

src/utils/readingTime.ts
/** 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:

Terminal window
pnpm create astro@latest -- --template chintak/astro-sage
pnpm dev

A 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:

How a post is built

A Mermaid sequence diagram:

Leaving a comment

An Excalidraw sketch, drawn on excalidraw.com and saved next to the post:

The rendering pipeline, sketched in Excalidraw

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:

Pasted from Excalidraw's clipboard

Or link a shared drawing with ![caption](https://excalidraw.com/#json=…). 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:

A request through the stack (wide)

Use full to go edge to edge:

The pipeline, full bleed

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:

Stylized hills in warm sepia tones under a pale sun
Figures get a caption set in italic, centred under the image.

Figures come in three sizes. The default stays in the text column. size="wide" spans the column and the table-of-contents rail:

The same hills, wide
A wide figure: column plus rail on desktop.

size="full" bleeds edge to edge. The caption stays on the reading column:

The same hills, full bleed
A full-bleed figure: the whole viewport, edge to edge.

In plain Markdown, use the image title: ![Caption](./hills.jpg "wide") or "full".

The hills once more, set in plain Markdown
The hills once more, set in plain Markdown

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.

Footnotes

  1. Robert Bringhurst suggests 45–75 characters for single-column text, with 66 as a comfortable ideal. ↩

  2. Footnote text is set smaller and in the muted colour so it doesn’t compete with the body. ↩