Authoring rules
How a note is built, not what it says. CLAUDE.md has the voice and currency rules; this file covers the devices — code, tables, diagrams, callouts — and the widths they have to survive.
Every number here was measured against the rendered site at 360 px, the narrowest phone worth designing for. They are not preferences.
The constraint everything else derives from
| Surface | Fits at 360 px | Measured |
|---|---|---|
| Code line | 49 chars | 13.12px monospace in a 316px prose column |
| Table columns | 3 | a 4-column table overflows by 123px |
| Prose line | ~46 chars | wraps, so unconstrained |
The page itself no longer scrolls sideways. What still scrolls is inside a code block or a table, and that is where reading breaks down: a table scrolled right loses its first column, which is the label — the thing that makes the row mean anything.
The design system already says this: “stack, never scroll sideways. A table that scrolls horizontally hides its most important column.”
Code blocks
Always tag the language. ```python, never a bare ```. Untagged
fences get no highlighting and no copy affordance. Diagrams and program output
are tagged text. All 7,390 fences in the corpus are tagged; check_style.py
fails on a new one that is not.
Keep lines at or under 56 characters. Not 49 — a little horizontal scroll on one line is tolerable; a block where every line scrolls is not. Currently 18% of 76,000 code lines exceed 49 chars, and 10% exceed 60.
When a line does not fit, the fix is almost never to let it scroll:
# Too wide — 96 chars, scrolls on every phone
result = await client.post(url, json=payload, timeout=httpx.Timeout(5.0, read=30.0), headers=headers)
# Fits — the same code, and the shape is now readable
timeout = httpx.Timeout(5.0, read=30.0)
result = await client.post(
url, json=payload, timeout=timeout, headers=headers
)Show the effect, not the setup. 5–15 lines. Imports only when the import
is the point. No if __name__ == "__main__", no argument parsing, no
scaffolding the reader already knows.
Comment the line that matters, not every line. One # WRONG / # RIGHT
pair beats a paragraph:
# WRONG — look-ahead
returns = signal * price.pct_change()
returns = signal.shift(1) * price.pct_change() # RIGHT — trade next barNever leave a fence unclosed, and never open one inside another. Both are
silent: an unclosed fence swallows the rest of the note, and a stray ```
pair dumps the code that follows into prose, where markdown renders every
# comment as a heading. tools/check_links.py and tools/check_style.py
both fail on these now; the corpus is clean.
Shell blocks: one command per line, no && chains longer than the width.
Split with a comment instead of a backslash continuation — backslashes break
when copied out of a rendered page.
Tables
Comparison tables are the default whenever 2+ options are in play — X vs Y, tiers, modes, strategies, “when to use which”. They are denser than prose and scan better under time pressure. There are already 1,436 of them; the rules are about width, not whether to use them.
Three columns is the target, and the reason is scannability, not layout. The original justification here — “a 4-column table overflows a 360px screen” — is obsolete: the site card-stacks every table below 700px, so column count cannot break a phone at all.
Measured 2026-08-12 by rendering every table in the corpus at 701px, the narrowest width where tables still render as tables: of the 150 tables with four or more columns, six actually overflowed, and one of those had five columns while most seven-column tables fit. All six were split; none overflow now.
So the honest rule:
- Column count is a poor predictor. What overflows is the sum of each column’s longest unbreakable word plus its padding, and padding is where many columns hurt: eight columns spend ~176px on padding alone.
- Keep to three anyway, because a reader scans a three-column table and
parses a seven-column one.
check_style.pyreports over-three astable-wide; treat it as an editorial signal, not a layout bug. - When you exceed it, split by the decision the reader is making. The AWS compute matrix went from one seven-column table to “you manage nothing” and “you manage nodes”, which is both narrower and a better argument.
The first column is the label, and it stays short — 1–3 words. It is the column a reader scans, and the one lost first when a table scrolls.
Cells are fragments, not sentences. Anything over ~60 characters belongs in the prose under the table. The worst cell in the corpus is 269 characters.
| | Vectorized | Event-driven |
|---|---|---|
| Speed | whole universe in one pass | orders of magnitude slower |
| Fidelity | crude fills | realistic order lifecycle |
| Use for | screening parameters | validating survivors |Two-column tables stack into cards on a phone and are always safe. Prefer them for question/answer, term/definition, and setting/value pairs.
Quick-reference blocks
Exact values an interviewer expects you to know — limits, defaults, thresholds, percentages — go in a compact table, never buried mid-sentence:
| Limit | Value | Note |
|---|--:|---|
| Files per Pages deployment | 20,000 | free plan |
| Max file size | 25 MiB | larger goes to R2 |Right-align the value column with --:. Numbers that line up are numbers you
can compare.
Diagrams
The corpus has 699 text blocks — a mix of real ASCII diagrams and pasted
program output — and one image. Several notes still describe a request path,
a fan-out or a decision ladder entirely in prose, which is the weakest form for
something inherently spatial.
Two mechanisms, and the choice is about what the diagram has to do:
1. ASCII flow — the default. Fenced with text so it renders everywhere,
including GitHub and an editor. Must fit 49 characters wide.
```text
client ──▶ ALB ──▶ app ──▶ Postgres
│ │
└─▶ cache ◀────┘
```Use for: linear request paths, simple fan-out, before/after states, layer stacks. If it fits in 49 columns and 8 rows, ASCII wins — it costs nothing and never breaks.
2. SVG from a theme-aware kit — for anything with real structure: a
sequence with timing, a state machine, a decision tree with more than three
branches. Add the component to site/src/components/diagrams/, embed it from
the note with a paragraph containing exactly [diagram:name], and the site
swaps it in at build time.
Rules for the kit: colours come from design-system tokens only (so both themes
work), text is real <text> (so it scales and is searchable), and the whole
thing must be legible at 320px — which in practice means at most 5 boxes
and 2 levels of nesting.
On GitHub a [diagram:name] paragraph reads as a placeholder. That is the cost
of the mechanism; it is why ASCII is the default and SVG is the exception you
justify.
No Mermaid. It would add a client-side renderer to a site that currently ships almost no JavaScript, and Mermaid’s default output is not legible at 320px without configuration per diagram.
Images
Default: don’t. A screenshot of a dashboard dates faster than the note around it, cannot be searched, and is dead weight on a phone connection.
When an image is genuinely the only way — a real profiler flame graph, a chart whose shape is the argument — then:
- SVG if it is a chart or diagram; WebP/AVIF if it is a photograph or capture
- Live in
site/public/img/<domain>/, referenced as/img/... - Always
— the alt text carries the point for a screen reader and when the image fails - Max 800px wide; the site caps rendered width at 100%
- Never an image of text or of a code sample — that is a code block
Callouts
For the trap, not for emphasis. Overused, they stop being read.
> **Gotcha:** patch where the name is looked up, not where it is defined.
> **Note:** as of 2026-08, 3.6 is still in RC — do not say it shipped.Rendered with an accent left border via the design system’s .callout. Green
and red stay reserved for correct/incorrect feedback — never use them to
decorate a callout.
One per section, at most. A note with five callouts has no callouts.
Step-by-step flows
Any process, chain or sequence gets a numbered list, not a paragraph:
1. `beforeEach` (global)
2. `beforeEnter` (per route)
3. `beforeRouteEnter` (in-component, before the instance exists)
4. `beforeResolve` (after async components resolve)
5. `afterEach` (cannot cancel — analytics goes here)If the order matters, number it. If it does not, use bullets — a numbered list implies a sequence and misleads when there isn’t one.
Structure
Break every ~40 lines with an h3. 62 notes are over 250 lines with no
h3 at all — a wall of text on a screen 780px tall. The corpus has 9,193 h2
against 4,710 h3; the ratio should be closer to even.
Target 250 lines — but completeness outranks it. The cap exists because a note nobody finishes teaches nothing, and 188 files still exceed it. It is a target, not a gate: if the material genuinely needs the length, take it. A half-explained topic is a worse failure than a long note, and trimming past the point of redundancy to satisfy a number produces exactly that.
So when a note is over: first look for the real defect the length is usually hiding — a restated summary table, the same idea twice, setup code that isn’t the point. Remove that. If what remains is all load-bearing, either split by topic and cross-link, or leave it long and move on. Reference material that is looked up rather than read start-to-finish — question banks, API tables — is measured by whether each entry earns its place, not by total length.
One h1, first line, no emoji. The site renders the title itself and hides this duplicate.
Close with ## Interview angle — 2–4 questions an interviewer would
actually ask, each with the gotcha follow-up. This is the repo’s signature and
the highest-value part of every note.
No numbered Q&A sections. A run of ### Q1: / ### Q2: headings is a quiz
transcript, not a note — it repeats the body in a worse format and always
carries the stalest content in the file. Write prose and code, and put the
questions in the closing section where they belong.
The checklist
Run python tools/check_style.py <path> — it checks every mechanical rule
below and reports the line. Then, at 360px:
- Every fence tagged with a language, balanced, and never nested
- No code line over 56 characters
- No table over 3 columns; first column short; no cell over 60 chars
- An
h3at least every 40 lines - Under 250 lines, unless the material needs more
- Any sequence is a numbered list
- Anything spatial is a diagram, not a paragraph
- No numbered Q&A section
- Closes with
## Interview angle -
python tools/check_links.pyandpython tools/check_style.pypass