loading
On this page
  1. Headings breathe differently at each level
  2. Section heading, slightly quieter
  3. Inline formatting you’ll actually see
  4. Blockquotes carry weight
  5. Lists are where rhythm matters most
  6. Code, because this is a developer’s site
  7. Tables for structured data
  8. Horizontal rules and breathing room
  9. Interactive islands
  10. When to reach for an island
  11. Admonitions for inline explanations
  12. A footnote, for completeness
  13. Closing
  14. Footnotes
playbook

Markdown & MDX reference

Sample content, not real writing. Every markdown element plus a live React island and a live Vue island — rendered in one place so the whole content pipeline stays visible to itself.

Good design is as little design as possible. Less, but better — because it concentrates on the essential aspects, and the products are not burdened with non-essentials.

— Dieter Rams, 1976

This is a reference post, not real writing. Its job is to render every markdown element the prose styles need to handle, plus prove that MDX can embed interactive islands from any framework. When I tune the theme I can see everything at once and catch anything that looks wrong. The companion piece — Design tokens — does the same for colors and the semantic typography scale.

The text is meaningful rather than lorem ipsum because lorem ipsum reads like static and it’s hard to judge rhythm on noise. Treat the quotes and the commentary as placeholder content that happens to be worth reading — swap either out when real writing arrives.

Headings breathe differently at each level

Headings aren’t just about being big. Each level needs enough room above it to feel like the start of a new idea, and just enough below it to belong to the text that follows. When that rhythm breaks, the page feels stacked instead of flowing.

Section heading, slightly quieter

The H3 does the real work on most articles. It’s where I group sub-ideas without needing to escalate the visual weight.

Fourth-level, for tight subdivisions

Below this the hierarchy usually stops mattering.

Five deep is almost always a sign the structure is wrong
And six is basically never used

Inline formatting you’ll actually see

Paragraph text mixes a bunch of small things: bold for the word you want the eye to catch, italic for emphasis that shouldn’t interrupt, inline code for names of things, and hyperlinks for places you can go next. Occasionally there’s a struck-through correction that someone thought about, then changed their mind.

The real test is whether all of those can live in the same sentence: I wrote const count = items.length while thinking “there must be a better name”, and a colleague linked me to an article about naming conventions that was honestly kind of overengineered thoughtful.

Blockquotes carry weight

A blockquote is the page saying “pause here; someone else said this better.”

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.

— Antoine de Saint-Exupéry

Inside a quote, normal styles still apply — you can have bold, italic, code, or a link. What changes is the weight: the text is indented, the left edge is marked, and the whole block asks the reader to slow down.

Sometimes a quote spans multiple paragraphs.

The second paragraph keeps the indentation and the border. It should feel like one continuous passage, not two separate fragments sitting next to each other.

Lists are where rhythm matters most

An unordered list for things that don’t need an order:

  • Items can be short.
  • Or they can run longer, wrapping onto a second line to test that the bullet alignment stays correct and the leading doesn’t collapse when a single entry becomes a paragraph on its own.
  • Nested lists exist for when the outer idea has sub-ideas:
    • like this one
    • and this one
    • which can go as deep as you want, though usually you only want one or two
  • And then back to the outer level.

An ordered list, for steps that need to happen in order:

  1. Understand the problem before touching code.
  2. Find the smallest change that makes the failing test pass.
  3. Refactor only after you’re sure what “right” looks like.

Make it work, make it right, make it fast.

— Kent Beck

Task lists track things you can actually finish:

  • Write the first post
  • Make sure headings work at every level
  • Add an illustration when I find one worth using
  • Convince myself this is finished

Code, because this is a developer’s site

Inline code like git rebase -i HEAD~3 lives comfortably inside a sentence. For anything more than a few characters, fenced blocks earn their space:

// The semantic typography scale this site uses.
// Every token maps to both a font-size and a matching line-height,
// so there's a single source of truth — no magic numbers anywhere.
export const typography = {
  micro: { size: 10, lineHeight: 1.4 }, // tag pills, type badges
  meta: { size: 11, lineHeight: 1.5 }, // dates, mono meta labels
  caption: { size: 14, lineHeight: 1.55 }, // card descriptions
  body: { size: 16, lineHeight: 1.65 }, // main reading register
  card: { size: 17, lineHeight: 1.35 }, // card titles
  heading: { size: 18, lineHeight: 1.3 }, // section headings
  name: { size: 20, lineHeight: 1.15 }, // hero name
  lead: { size: 22, lineHeight: 1.5 }, // hero tagline
} as const;

Shell commands in their own language:

pnpm build
pnpm lint
pnpm format:check

CSS, since half of this post is about CSS:

@theme {
  --text-body: 16px;
  --text-body--line-height: 1.65;
}

Programs are meant to be read by humans and only incidentally for computers to execute.

— Donald Knuth

Tables for structured data

Tables are tricky to get right on the web. A good one stays readable at narrow widths and doesn’t fight with the prose around it.

TokenSizeLine heightUsed for
text-micro10px1.4Tag pills, type badges
text-meta11px1.5Dates, mono meta labels
text-caption14px1.55Card descriptions
text-body16px1.65Main reading register
text-card17px1.35Post and project card titles
text-heading18px1.3Section headings
text-name20px1.15Hero name
text-lead22px1.5Hero tagline

That’s the semantic scale. Every text-[Npx] in the codebase was replaced with one of these eight tokens, so tuning typography is a one-file change.

Horizontal rules and breathing room

A horizontal rule is the page saying “this idea is finished; something different is coming.”


After a rule, the next section feels like a new beat. Which is the whole point.

Interactive islands

The difference between .md and .mdx is that MDX lets you import components straight from any framework Astro supports, drop them into the middle of prose, and hydrate them on the client with a directive. Below are two identical counters — one built in React, one built in Vue — both rendered inside this post and both fully interactive. They use the same CSS variables as the rest of the site, so they flip with the theme toggle.

react · useState
count: 0
vue · ref
count: 0

Notice the uppercase eyebrow label inside each counter. React shows react · useState; Vue shows vue · ref. Both islands are lazily hydrated via client:visible, so they only boot when they scroll into view.

The MDX source for this section looks like this:

import CounterReact from "@/components/demo/CounterReact.tsx";
import CounterVue from "@/components/demo/CounterVue.vue";

<CounterReact client:visible />
<CounterVue client:visible />

That’s it — no route setup, no hydration boilerplate, no per-post build config. The @astrojs/mdx, @astrojs/react, and @astrojs/vue integrations registered in astro.config.ts handle the rest.

When to reach for an island

Most posts don’t need this. Prose, code blocks, and tables cover 95% of writing. Reach for an island when:

  • A concept is genuinely clearer when the reader can poke at it (sliders, toggles, live previews).
  • Static text would require three paragraphs to describe what one small interactive component shows in one beat.
  • You need a demo of a component you’re also shipping in the app — reusing the real thing is cheaper than mocking screenshots.

And when in doubt, stay with prose. An island that isn’t earning its hydration cost is a worse version of a paragraph.

Admonitions for inline explanations

Sometimes a term or phrase deserves a short explanation without breaking the reader’s flow. The A popover that appears on click, giving extra context without leaving the page. Uses the native Popover API — no JavaScript positioning library needed. component handles this: click the dotted-underline trigger and a popover appears with the explanation.

When a concept has both an explanation and a reusable prompt behind it, the admonition shows tabs. For example, Semantic tokens like text-body or text-heading map to a specific font-size and line-height pair. They replace magic numbers so the entire typography scale can be tuned from one file. in this site uses eight named tokens instead of raw pixel values.

A footnote, for completeness

Sometimes a small point wants a small escape hatch.1 A good footnote is quiet — it doesn’t interrupt the main line of thought, but it’s there for the reader who wants more.

Closing

Simplicity is prerequisite for reliability.

— Edsger Dijkstra

That’s every element I can think of, rendered in context. If something here looks wrong in either light or dark mode, the fix goes into src/styles/global.css — and this post will show the result immediately on reload. That’s the whole job of a validation document: make the system visible to itself.

Footnotes

  1. Like this one. Footnotes are rendered at the bottom of the article with a return link back to where they were referenced.

← Back to all content