authoring.md

Authoring rules

Updated 9 min read source
On this page10
  1. The constraint everything else derives from
  2. Code blocks
  3. Tables
  4. Quick-reference blocks
  5. Diagrams
  6. Images
  7. Callouts
  8. Step-by-step flows
  9. Structure
  10. The checklist

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:

python
# 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:

python
# WRONG — look-ahead
returns = signal * price.pct_change()
returns = signal.shift(1) * price.pct_change() # RIGHT — trade next bar

Never 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.py reports over-three as table-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.

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

markdown
| 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.

markdown
```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 ![meaningful alt text](...) — 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.

markdown
> **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:

markdown
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 h3 at 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.py and python tools/check_style.py pass