tanghoong design
  1. Design system
  2. Content

08 — Content

Content & prose

Long-form typography. Set .prose on the wrapper and author plain HTML inside it — headings, paragraphs, lists, quotes and code all get their rhythm without a class on a single child.

The prose block

Capped at 68 characters. Spacing comes from the owl selector — .prose > * + * — so every sibling gets top space and the first child gets none. That is what keeps the rhythm even when the content is generated from Markdown and you cannot add classes.

Live prose

Why the thin slice comes first

Most integration projects fail in the same place: the second week, when the first real payload arrives and it does not look like the documentation. A thin slice — one record, end to end, into production — surfaces that on day three instead of week six.

It also settles the arguments that otherwise run for a month. You cannot debate whether the queue can handle the volume once there is a queue handling volume, and you cannot debate the schema once a real record has been through it. See the patterns page for how this gets written up.

What goes in the slice

The smallest path that touches every system the final thing will touch. Not the simplest feature — the narrowest complete one. Specifically:

  • One real record from the real source, not a fixture
  • The actual transport, with the actual auth
  • Whatever the destination system does on write, including its side effects
  • One alert that fires when the thing breaks
    • Pointing at a person, not a channel
    • With enough context to act on at 3am
The slice is not a prototype. It is the first ten per cent of the real system, and it stays in production. Delivery note, 2024

What it costs

Roughly a week, sometimes two if credentials are slow. That is real time, and it is the most common thing a client asks to skip. It is also the only week that reliably pays for itself — the alternative is discovering the same facts in week six with a deadline attached.

Run it with npm run slice and check the output against the schema in docs/contract.md before widening.

$ npm run slice -- --source=prod --limit=1
  → fetched 1 record   (312 ms)
  → transformed        (4 ms)
  → wrote to sink      (89 ms)
  ✓ round trip 405 ms

A smaller heading

Used sparingly — a fourth level usually means the section wants splitting.

PhaseDurationOutput
Discovery1–2 weeksWritten architecture
Thin slice1 weekOne path in production
BuildFixed scopeThe rest of the paths
Placeholder illustrating where a diagram sits in prose.
Figures get a hairline border and a 16 px radius. The caption is small, quiet and left-aligned.

Footnotes and asides use <small>, which drops to 15 px and secondary ink.

Note that nothing inside <article class="prose"> carries a class. That is the point — Markdown output drops straight in.

Eyebrow, lead and quote

The three pieces that open a page. An eyebrow labels the section, the heading names it, and the lead makes the case for reading on.

Opening block

Case study — Logistics

A nightly batch that could not finish before morning

Route allocation ran for six hours and had started overlapping with the dispatch window. Replacing it with an event-driven allocator cut it to continuous, sub-second assignment.

We stopped planning around the batch. That changed more than the latency number did.

Head of Operations, on the first week after cutover

<p class="eyebrow">Case study — Logistics</p>
<h2 class="mt-3">A nightly batch that could not finish</h2>
<p class="lede mt-5">Route allocation ran for six hours…</p>

<blockquote class="quote">We stopped planning around the batch.</blockquote>
<p class="quote-note">Head of Operations</p>

Article header

Title capped at 20 characters-per-line for balance, with a metadata row beneath. The dots between metadata items are generated, not typed.

Header

Engineering

Why the thin slice comes first

Delivery Integration

Code and keys

Inline code gets a sunken pill; blocks get a bordered well that scrolls sideways rather than wrapping. Keyboard keys get a physical bottom edge.

Code

Set the region with --region=ap-southeast-1, then press Ctrl C to stop the watcher.

export DATABASE_URL="postgres://localhost:5432/app"
npm run migrate -- --to=latest
npm run slice -- --source=prod --limit=1

Code blocks never wrap. A wrapped shell command cannot be copied reliably, so the block scrolls instead.

Numbered steps

A methodology or a runbook. Numbers are generated with a CSS counter in the accent colour, set in mono with leading zeros so single and double digits align.

Steps
  1. Frame the failure mode

    Write down what breaking looks like before writing what working looks like. It is a faster route to the real constraints.

  2. Ship a thin slice

    One real record through every system the final thing will touch, into production.

  3. Instrument before widening

    If you cannot see the first path, you will not be able to debug the tenth.

  4. Hand over as you go

    Documentation written at the end is documentation nobody trusts.

Measure

Three caps, and the choice matters more than most typographic decisions: a line longer than about 75 characters loses the reader on the return sweep.

ClassCapUse
.measure-tight56chLeads, section intros, quotes
.measure68chBody copy — the default in .prose
.measure-wide80chDense reference text and tables only

Print

A CV and a case study both get printed. content.css ends with a print block that drops the header, footer, nav sheet, toasts and rail, forces black on white, keeps headings with their content and stops cards breaking across pages.

Print rules
@media print {
  body { background: #fff; color: #000; }
  .site-head, .site-foot, .navsheet, .toasts, .rail { display: none !important; }
  a { color: #000; text-decoration: underline; }
  h1, h2, h3 { break-after: avoid; }
  .card, .panel, .table-wrap { break-inside: avoid; box-shadow: none; }
}

Try it: print-preview this page. Nothing should be cut off and no ink should be wasted on chrome.

Do Underline links inside running prose. Hover-only underlines are undiscoverable mid-paragraph — .prose a underlines always, on purpose.
Don't Run body copy at container width. 1120 px of 17 px text is about 130 characters a line, and it is genuinely tiring to read.