Authoring guide & block reference
Every block in the Curiosity library, demonstrated. Doubles as a live styleguide and as the prompt context for an agent creating new notes.
On this page
This page exercises every block. Treat it as the canonical reference for an agent (or human) writing a new note.
File layout
- File path:
src/content/notes/{category}/{ID}_{kebab-slug}.mdx {ID}is a zero-padded 4-digit number, immutable once assigned.- Frontmatter is validated against
src/content.config.tsat build time.
Frontmatter — the minimum
---
id: "0042"
title: "A short, declarative title"
category: java
type: pattern # required: concept | decision | debug | comparison | procedure | pattern | antipattern
status: published # verified | archived — defaults to verified
tags: [new, indexing, locking] # unquoted bare words; "new" is the in-progress marker only
difficulty: intermediate # optional
parent: "0005" # optional, id of parent note
relatedTo: ["0006", "0009"] # ids of peer notes (build fails on unknown ids)
summary: "20–240 chars. Goes in OG, RSS, search."
createdAt: 2026-05-01
updatedAt: 2026-05-24 # only stamp on real edits, not at creation
---
Core blocks
<Callout>
Highlighted notes. kind is one of tip, info, warn, danger, gotcha. An optional title overrides the default label.
<Steps> and <Step>
Auto-numbered procedures. Useful for any “do A, then B, then C”.
-
Pick the ID
Look at the highest existing id in the category and add one. Numbers are global, not per-category.
-
Draft the summary
Write the 240-char summary first. If it won’t compress, the note is too broad.
-
Fill the body using blocks
Reach for
<Pattern>,<Decision>, or<Postmortem>when the shape matches. Don’t force them. -
Cross-link with `<Ref>`
Add inline references to related notes; the build will fail if you reference an unknown id.
<Aside>
A margin note. Defaults to side="right". Floats next to body text on wide screens, stacks inline on narrow ones.
The main thread continues here. The aside floats next to the paragraph on lg+ and stacks above on mobile. Keep aside content short — a sentence or two at most.
<Columns>
Two-column layout. ratio accepts 1:1, 2:1, or 1:2.
Left column. Useful for side-by-side prose, like “before vs. after” or “naive vs. correct.”
Right column. Each child of <Columns> is one column. Wrap each in a div so MDX preserves the boundary.
<Ref>
Inline reference to another note, validated at build time. Pass to (the target’s id from frontmatter) and optionally label to override the link text.
A bare reference renders as 0000 Authoring guide & block reference · Meta , or 0000 this guide · Meta for a labelled link.
Workflow blocks
<Pattern>
Reusable solution. Slots: problem, solution, consequences.
Clients retry write requests after network failures. Naive servers process the retry as a new request and double-write.
Require an Idempotency-Key header. Store the response keyed by it. On replay, return the stored response.
One extra table, one extra lookup per write. Eliminates an entire class of double-charge incidents.
<AntiPattern>
What not to do, and why. Slots: smell, whyItHappens, fixInstead.
try { … } catch (e) { log.error(e); } — exception swallowed, caller never told.
The developer wanted the immediate error to stop appearing. The structural problem is unresolved.
Either handle the exception meaningfully or let it propagate. Logging is not handling.
<Decision>
ADR-style. Records a decision with its alternatives.
The query shapes are relational and the dataset stays under 500GB for the foreseeable future. We trade some ops work for query flexibility we'll definitely need.
- Mature
- Strong consistency
- Rich operators
- Vertical scale ceiling
- Ops overhead
- Managed
- Horizontal scale
- Limited query shapes
- Cost grows with scans
<Compare>
Side-by-side comparison. Named slots left and right; pass leftLabel / rightLabel for headers.
List<Order> orders = repo.findAll();
for (Order o : orders) {
// Triggers a query per order
o.getCustomer().getName();
} List<Order> orders = repo.findAllWithCustomer();
for (Order o : orders) {
// Already loaded
o.getCustomer().getName();
} <Tradeoffs>
Explicit axes × options table. Cheaper to scan than two columns of prose.
| Axis | REST | GraphQL |
|---|---|---|
| Discoverability | OpenAPI is mature | Schema introspection built in |
| Over-fetching | Per-endpoint problem | Solved by the client |
| Caching | HTTP cache works | Needs per-query strategy |
| Tooling maturity | Universally supported | Ecosystem newer but solid |
<Postmortem>
War-story / debug write-up. Slots: symptom, investigation, rootCause, fix, prevention.
p99 on /checkout jumped from 180ms to 2.4s starting 14:02 UTC. No deploys in the last 6h.
Slow query log showed a sequential scan on orders that had been indexed yesterday. The index was there but unused.
ANALYZE had never run on the table after a 5x data growth event. Planner stats were stale; it picked the seq scan.
ANALYZE orders; returned latency to baseline within 30s. No code change.
Enabled autovacuum_analyze_scale_factor=0.05 on hot tables. Added a synthetic check that alerts when planner stats are >48h stale.
<Concept>
Definition card for a mental model.
Eventual consistency
Replicas converge on the same value, eventually — but reads in between can disagree.
Useful when your write throughput exceeds what a strongly-consistent store can offer, and when staleness measured in seconds is acceptable to the user-facing experience. The cost is paid by every reader: they must handle disagreement explicitly.
<Glossary>
Short term list. Cheap to maintain inside the note it defines.
- ADR
- Architecture Decision Record — a dated note capturing a decision, its alternatives, and the reasoning.
- Idempotency key
- An opaque client-minted token identifying a write attempt. Reusing it on retry returns the original response instead of executing the write twice.
- N+1 query
- Pattern where a list-loading query is followed by one query per row to resolve a lazy association.
Colors
Notes inherit a single warm/muted palette from src/styles/global.css. Use these tokens — both directly in CSS and as string values inside MDX props (e.g. fill: "var(--accent-soft)") — instead of writing raw hex literals, so dark mode keeps working and the whole site stays visually coherent.
Highlight tokens — for step states
<Flow> and other interactive blocks pick a color automatically from the step’s highlight enum. Use these four for any block that wants to convey informational, cautionary, successful, or failed state. Don’t reach for raw hexes like #16a34a or #dc2626 — those break the warm palette and don’t switch with theme.
Each enum value maps to one semantic token. The active edge, active nodes, and the step-list bullet all pick it up. Steps with no `highlight` property (or `highlight: 'neutral'`) fall back to the brand --accent — that's the orange-on-step you see for unmarked transitions.
Soft semantic tokens — for tinted backgrounds
The four strong colors above are loud — useful for active edges, pass/fail icons, alert dots. When you want to convey the same meaning more quietly — a flow region tinted to say “this whole tier is risky”, a callout that should feel green without being a button, a node fill that hints “this value was extracted” without claiming a hard state — reach for the soft companion. Each *-soft pairs with its strong sibling: use --tip as the icon, bullet, or heading color sitting on --tip-soft, and you get a coherent two-tone duo for free. For body text on a soft background, stay with --text — the contrast against *-soft ranges from borderline (--tip on --tip-soft ≈ 3.6:1) to insufficient (--warn on --warn-soft ≈ 2.5:1) for paragraphs.
Soft companions to the strong semantic colors. Pair --tip-soft + --tip, --warn-soft + --warn, etc.
Brand tokens — for custom fills and accents
Use these when a node, callout, or region needs to stand out without claiming a specific semantic meaning. --accent is the brand orange; --accent-soft is its warm soft companion (ideal for data boxes in diagrams).
Accent is the strong moment; accent-soft is the muted background used for highlighted info boxes.
Surface and text tokens — for everything else
These define the page itself. Reach for them when you need a fill that’s deliberately not a highlight — a neutral inset, a subtle region background, a custom label color.
Backgrounds, borders, and text colors. The flow's default node fill is bg-elevated; default stroke is border-strong; default text is text.
Interactive blocks
These are Svelte islands — full client-hydrated components mounted with client:visible, so they only pay for themselves once the reader scrolls them into view. Reach for them when a static diagram cannot carry the cause-and-effect; otherwise prefer Mermaid (it ships zero JS).
<Flow> — generic step-graph
A reusable node-and-step graph. Provide nodes with absolute coordinates inside the SVG viewBox (default 720 × 240), and an ordered list of steps with from / to / title / optional body / optional highlight. The component renders dashed inert edges for every unique pair, animates the active edge per step, and exposes Play / Pause / Step / Reset controls plus a clickable step list. highlight is one of neutral, info, success, warn, error.
Cache-aside read
What a single read looks like when the application owns the cache, not the database.
Shapes, ports, routing, and regions
Nodes default to a rounded rectangle, but you can pick shape: "rect" | "pill" | "circle" | "diamond" | "cylinder" and override size with size: { w, h } (or size: { r } for a circle). Per-node fill, stroke, and textColor are also supported. Edges anchor to the nearest side by default; to pin an edge to a specific anchor, write from: "id:port" where port is n / s / e / w / c — or a custom port declared on the node via ports: { name: { x, y } }. Each step picks its route: auto (the default quadratic curve), straight, or orthogonal (one elbow). Pass dashed: true for async / best-effort edges. Background layers.regions draw labelled bounding boxes behind the nodes — useful for swimlanes or tier boundaries.
Read path with shape-typed nodes
Same cache-aside, drawn so each node's shape matches what it represents — a pill for the client, a cylinder for the database, a circle for the in-memory cache. Orthogonal edges pin to named ports.
State machines
Use circles and self-loops to model state transitions. Multiple outgoing edges from the same node show alternative outcomes; a backwards edge plus dashed: true reads as a retry. The self-loop is rendered automatically when a step’s from and to reference the same node.
Job lifecycle
A background job's states from the worker's perspective. The self-loop on running is the heartbeat; the dashed edge from failed back to queued is the retry.
Branching workflows
A diamond marks a decision. Wiring both outcome edges from the diamond’s east port via orthogonal routes makes the two branches share an elbow column, so the split point reads at a glance.
Tiny CI pipeline
Five steps from a developer's push to either a deploy or an alert. The diamond is the test gate; the two branches share the same elbow x so the decision point is obvious.
Layered architecture
Stack regions to draw tier boundaries and route requests across them. Orthogonal edges between tiers leave a clean horizontal seam in the gap between regions — useful when the diagram needs to convey both topology and request flow at once.
Read path through an HTTP stack
A user request crosses three tiers — edge, application, data — and returns with the answer. The dashed edges show cache misses; the green return path traverses every tier on the way back.
Icon shapes, external labels, edge labels, and multi-line text
Beyond geometric primitives, <Flow> ships with five icon-style shapes for diagrams whose actors are recognisable things, not just boxes: server (a 3-bar stack with a status LED), envelope, lock, check (a circled tick), and cross (a circled X). Pair these with labelPlacement: "above" | "below" | "left" | "right" to put the label next to the icon. Each step accepts an optional edgeLabel that renders on its inert edge with a halo so it stays readable against any background. Node and edge labels both honour \n for line breaks — useful when a database is labelled example.com\nDNS or a fetch arrow needs Fetch example.com DKIM record\nat 's._domainkey.example.com'.
DKIM signature verification
A replica of the canonical DKIM diagram: the sender signs an outgoing email with its private key; the receiver pulls the DKIM signature out of the headers, fetches the matching public key from DNS, re-derives the digest, and either passes or fails based on whether they agree.
<SqlPipeline> — purpose-built walkthrough
A domain-specific island that animates MySQL’s logical execution order (FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT), tracking row counts as rows are added, filtered, aggregated, and trimmed. No props — the content is baked in. Treat it as the template for any future bespoke interactive: copy SqlPipeline.svelte, replace the data and labels, register the .astro shell in src/components/MdxComponents.ts. Name bespoke islands for what they visualize (SqlPipeline, TcpHandshake, BPlusTreeInsert) — reserve the Flow suffix for instances of the generic <Flow> graph.
How MySQL runs your SELECT
Press ▶ Play to watch the engine execute the query top to bottom, one stage at a time. Each stage processes the rows that survived the stage above it.
The diagram shows the seven stages MySQL runs for every SELECT, in the order
they actually execute — not the order you wrote them in. Press ▶ Play to walk
through one stage at a time, or click any band or clause to jump there.
<Stepper> — step through an algorithm
Use it when a reader must watch state change one step at a time: a sorting pass, a dynamic-programming table, a graph search, a tree traversal. Pick a renderer (array, grid, graph, or tree) and give a list of frames; every frame has a note (what just happened, shown under the stage and announced to screen readers) and a state shaped for the renderer. Up to 500 frames. A frame that points at a missing index, node, or cell fails the build with the frame number.
<Stepper renderer="array" title="Bubble sort pass" frames={[
{ note: "Start with 3, 1, 2.", state: { values: [3, 1, 2], pointers: { i: 0 } } },
{ note: "Compare 3 and 1: swap.", state: { values: [3, 1, 2], highlight: [0, 1], pointers: { i: 0 } } },
{ note: "Sorted.", state: { values: [1, 2, 3], sorted: [0, 1, 2] } },
]} />
Step 1 of 4 Start with 3, 1, 2.
The grid renderer takes cells (a rectangular table of up to 16 by 16) with optional highlight and path lists of [row, column] pairs.
Step 1 of 2 The table starts with its borders filled in.
The graph renderer takes a top-level graph (up to 30 nodes and their edges) and frames with active, visited, and the edges to mark.
Step 1 of 3 Start at a.
The tree renderer takes a top-level tree (nested value and children, up to 63 nodes); frames name nodes by their path of child indices joined with dots, and the root is the empty string.
Step 1 of 3 Go left.
<Scene3D> — a small 3D scene
Use it only when depth carries the idea (a layered architecture, a data structure in space). Describe the scene as data: shapes (box, sphere, torus, cone, cylinder, each with position, size, color of accent, blue, green, or muted, and an optional label), optional links as pairs of shape indexes, and a required alt that says what the scene shows. On a phone or touch screen the block shows a poster until the reader taps it, it stops rendering while offscreen, and it does not rotate for readers who prefer reduced motion. At most one <Scene3D> per note; bun run build warns when a note has more.
<Scene3D alt="A sphere, a box, and a torus joined in a row" shapes={[
{ kind: "sphere", position: [-2, 0, 0], color: "accent", label: "Source" },
{ kind: "box", position: [0, 0, 0], color: "blue" },
{ kind: "torus", position: [2, 0, 0], color: "green", size: 0.8 },
]} links={[[0, 1], [1, 2]]} />
- Source
When the primitives are not enough, write a per-note island under src/components/islands/notes/{ID}/ (plain React, or a scene built on SceneFrame, which already provides the poster, visibility, reduced-motion, and release behaviour). Per-note island code is linted like note text.
<Gsap> — reveal content as it scrolls into view
Wrap a few sibling elements and they fade and slide in one after another when the block scrolls into view. Props: from (up, down, left, right, or none), distance in pixels, duration and stagger in seconds, and once. Without JavaScript, before hydration, and for readers who prefer reduced motion, the content is simply visible. Use it sparingly: it is for pacing a list, not for decoration.
<Gsap from="up" stagger={0.1}>
<p>First point.</p>
<p>Second point.</p>
</Gsap>
Each paragraph in this block fades in as it enters the viewport.
The wrapper only animates its direct children.
Nothing here is hidden when scripts are off.
Code and diagrams
Code
Standard fenced blocks. Five notation features stack on top of Shiki’s two-theme output:
title="…"in the fence meta wraps the block in a<figure>with a caption.- Range highlight:
{3-5}in the meta string highlights those lines. - Inline highlight:
// [!code highlight]on any line. - Diff:
// [!code ++]for adds,// [!code --]for removes. - Focus:
// [!code focus]blurs every other line until the reader hovers the block.
public Order place(Cart cart, String idempotencyKey) {
var existing = idempotencyRepo.find(idempotencyKey);
if (existing != null) {
return existing.response();
}
var order = orderFactory.from(cart);
return orderRepo.save(order);
}function loadUser(id: string) {
const cached = cache.get(id);
if (cached) return cached;
const row = db.query(id);
const user = legacyFetch(id);
cache.set(id, row);
return row;
}Mermaid
Server-rendered to inline SVG by rehype-mermaid (img-svg strategy). Ships zero JS, so reach for it whenever a static diagram is enough — the interactive blocks above are only worth their weight when the reader needs to walk through the diagram step by step.
Supported diagram types: flowchart, sequenceDiagram, stateDiagram-v2, erDiagram, classDiagram, gantt, timeline, journey.
Images
Prefer an interactive block, Mermaid, or ASCII art over a raster image — those stay editable and theme-aware. Reach for an image only when the visual is genuinely a bitmap you cannot reproduce in code (an annotated diagram distilled from an external source, a screenshot). When you do, download the file into the repo and reference it by an absolute path — never hot-link a third-party URL, which rots and breaks the “renders in 10 years” guarantee.
Drop the file under public/notes/ named {ID}-{slug}.png, then reference it with standard markdown. Files in public/ are served from the site root, so the leading / is the public root, not the note folder. Always write descriptive alt text — it is the only thing a screen reader or a future agent reading the source has to go on. Content images (like Mermaid diagrams) open in a zoomable lightbox on click — fit-to-screen by default, with scroll-wheel/pinch zoom, +/− controls, and drag-to-pan — so a dense diagram stays readable without you doing anything extra.

The image above is a placeholder that ships with this guide.
Conventions
- One concept per note. Past ~600 lines, split and cross-link with
<Ref>. - Numbers don’t lie. Include concrete data (latencies, row counts, sizes) when relevant. Vague claims age badly.
- Write so a future-you, cold, can follow it. No “as discussed above” without an anchor. No unexplained jargon.
- Use the
newtag while a note is still settling. Every note ships asstatus: publishedby default — thenewtag is the in-progress marker. Remove it once the note holds up on a cold re-read. Usestatus: archived(never deletion) to retire a note without breaking incoming references.