authoring.md

Authoring rules

Updated 14 min read source
On this page11
  1. Tiers
  2. The constraint everything else derives from
  3. Code blocks
  4. Tables
  5. Quick-reference blocks
  6. Diagrams
  7. Images
  8. Callouts
  9. Step-by-step flows
  10. Structure
  11. 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 tier a note is written at and the devices — code, tables, diagrams, callouts — it is built from.

The device widths were measured against the rendered site at 360 px, the narrowest phone worth designing for.

Tiers

Not every note should be a deep article, and pretending otherwise is how a rewrite stalls. Three tiers. A note is Reference unless it says otherwise.

Tier What it is
Article One question, answered properly
Reference Lookup material, honest about it
Card A single gotcha

An Article opts in with a > Article. marker on the line below the h1. It is the tier for topics that get interviewed hard, and it carries obligations — see below. Expect 250–400 lines. too-long does not apply.

A Reference note is the default: API surfaces, comparisons, configuration, service maps. There is nothing wrong with reference material and most of the corpus is legitimately this. It should still be accurate, current and cross-linked; it does not have to be an essay.

A Card is a single gotcha in 50–150 lines, the shape of backend/02_python_core/tricky_questions/. Setup, repro, why, fix.

What an Article owes the reader

These are the difference between an article and a longer reference note. check_style.py enforces the mechanical ones.

  1. One question per note. If the title needs “and”, it is two notes. 24_functions_triggers_and_views.md was a survey of four topics at 35 lines each. It has since been deleted: each topic became its own Article, and 24_triggers_in_practice.md shows what one of them looks like done properly.
  2. Show, don’t assert. Any claim that something breaks carries the repro and its output. “Marking a function IMMUTABLE when it reads a table gives a silently wrong index” is the interesting part — so show the wrong answer.
  3. Real code. Real table names, real columns, enough context to paste. Four worked blocks minimum; the checker enforces it as article-thin.
  4. A mechanism section. What the system does underneath, not what the API looks like. The set_config(..., true) transaction-local flag leaking a previous user’s identity across a pooled connection is worth more than three paragraphs describing syntax.
  5. A production failure section. Where it bites, and the shape of the damage — one statement becoming 40,000 function calls inside a transaction that blocks vacuum.
  6. Follow-up chains in ## Interview angle. Not a summary of the body. The question, the answer that lands, and the nested “Follow-up:” the interviewer asks when you get it right. Two minimum, enforced as article-no-followup.
  7. Name the trade you did not take, and why.
  8. Vary the structure. A uniform skeleton across every note is what makes a corpus read as generated. Let the topic choose the shape.

The constraint everything else derives from

Surface Fits at 360 px Measured
Diagram line 60 chars scrolling one destroys its shape
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 code lines at or under 88 characters. That is Black and Ruff’s default line length, so it is the width the code is written at anyway.

This limit was 56, derived from the 360px measurement below. It was wrong, and it was expensive: the site gives every code block overflow-x: auto and a wrap toggle, so a wide line costs a reader almost nothing — while the narrow cap forced six-line toy snippets in place of examples anyone could paste and run. Shallow examples were the single biggest cause of the corpus reading as generated rather than written.

Diagrams keep the tighter limit — 60 characters in a text fence. Scrolling a diagram destroys the shape the diagram exists for, and a diagram cannot wrap.

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 |

Escape a pipe inside a table cell, even in a code span: a \|\| b. GFM splits the cell on every bare |, so a || b renders as a broken row. check_style.py --gate rejects it as table-pipe (found 2026-10-15: eight broken rows across seven notes).

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.

The width caps, and their exceptions

Measured 2026-08-21, because the counter was being read as a defect list when it is a writing guideline with legitimate exceptions.

Code, 88 characters. Black and Ruff’s default, so it is the width Python is already written at. Wrap at commas, attributes or operators. What cannot be wrapped is an atomic token - a URL, an ARN, a hash - and those stay.

Diagrams, 60 characters. check_style.py now classifies a text fence before applying this: a block that draws a shape gets 60, and sample output or a log excerpt gets the code cap, because output scrolls exactly like code. That one fix removed 171 false positives.

A diagram can legitimately exceed 60. The RFC 6455 frame layout in 12_protocols/websockets/01_websocket_protocol.md is 65 characters because a 32-bit ASCII bit diagram is 65 characters. Narrowing it would destroy the thing it depicts. Standard protocol and bit-layout diagrams reproduced from a spec are correct as they are.

So: aim at the caps when writing, fix a diagram whose layout is merely sloppy, and leave the ones whose width is inherent. Neither cap gates, and the reason is this paragraph - a rule with real exceptions cannot be a gate without becoming a rule people disable.

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 section names are fixed

frontend/ was written to a second template and carried it for 157 files. A reader moving from a backend note to a frontend one met a different document, which is exactly the inconsistency CLAUDE.md’s “one style, no exceptions” is about. It was normalized on 2026-08-20 and check_style.py --gate now rejects the old names as legacy-heading.

Do not use Use
## TL;DR no heading; the intro is the prose under the h1
## In depth nothing; its subsections are the sections
## Gotchas / edge cases ## Gotchas
## What a senior is expected to say ## Interview angle, and last
## Cross-references ## Related

No essay ceremony

A note is not a school essay. It opens with prose directly under the h1 and closes with ## Interview angle. Removed corpus-wide 2026-08-20 and now gated as essay-heading:

Do not use Why
## Introduction the intro is the prose under the h1
## Conclusion the note closes with ## Interview angle
## Table of Contents the site renders its own navigation

A table of contents also goes stale the moment a heading is renamed, and all 38 in the corpus were pure anchor-link lists.

## Summary is allowed and was checked rather than assumed: 49 of the 53 carry a comparison table or a rule list, so it is a generic name over real content. Prefer a name that says what it summarises.

Headings carry no bold. A heading is already emphasis, and a trailing colon is label punctuation - ### **Key Concepts:** becomes ### Key concepts. Gated as bold-heading. Partial bold that carries meaning is fine, as in ## **S**: Single Responsibility.

## Related comes before ## Interview angle, which closes the file. Articles additionally use ## Where it breaks in production and ## The trade not taken — see the Tiers section above.

The checklist

Run python tools/check_style.py <path> — it checks every mechanical rule below and reports the line. Then, at 360px:

  • The tier is deliberate, and an Article says > Article.
  • Every fence tagged with a language, balanced, and never nested
  • No code line over 88 characters; no diagram line over 60
  • No table over 3 columns; first column short; no cell over 60 chars
  • An h3 at least every 40 lines
  • Under 250 lines, unless it is an Article or the material needs more
  • Article only: one question, four worked blocks, a mechanism section, a production-failure section, and two follow-up chains
  • 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