Skip to content

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.

Updated May 24, 2026

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.ts at 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”.

  1. Pick the ID

    Look at the highest existing id in the category and add one. Numbers are global, not per-category.

  2. Draft the summary

    Write the 240-char summary first. If it won’t compress, the note is too broad.

  3. Fill the body using blocks

    Reach for <Pattern>, <Decision>, or <Postmortem> when the shape matches. Don’t force them.

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

Pattern
Idempotency key
Problem

Clients retry write requests after network failures. Naive servers process the retry as a new request and double-write.

Solution

Require an Idempotency-Key header. Store the response keyed by it. On replay, return the stored response.

Consequences

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.

Anti-pattern
Catch-and-log without rethrow
Smell

try { … } catch (e) { log.error(e); } — exception swallowed, caller never told.

Why it happens

The developer wanted the immediate error to stop appearing. The structural problem is unresolved.

Fix instead

Either handle the exception meaningfully or let it propagate. Logging is not handling.

<Decision>

ADR-style. Records a decision with its alternatives.

Decision
Chose: PostgreSQL
2026-04-12

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.

✓ PostgreSQL
Pros
  • Mature
  • Strong consistency
  • Rich operators
Cons
  • Vertical scale ceiling
  • Ops overhead
DynamoDB
Pros
  • Managed
  • Horizontal scale
Cons
  • Limited query shapes
  • Cost grows with scans

<Compare>

Side-by-side comparison. Named slots left and right; pass leftLabel / rightLabel for headers.

Naive
List<Order> orders = repo.findAll();
for (Order o : orders) {
  // Triggers a query per order
  o.getCustomer().getName();
}
With join fetch
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.

Postmortem
Friday checkout latency spike
2026-03-04
Symptom

p99 on /checkout jumped from 180ms to 2.4s starting 14:02 UTC. No deploys in the last 6h.

Investigation

Slow query log showed a sequential scan on orders that had been indexed yesterday. The index was there but unused.

Root cause

ANALYZE had never run on the table after a 5x data growth event. Planner stats were stale; it picked the seq scan.

Fix

ANALYZE orders; returned latency to baseline within 30s. No code change.

Prevention

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.

Concept

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.

info --info Informational moments — opening request, headers, descriptive callouts.
warn --warn Cautionary moments — cache miss, retry, dashed async edges.
success --tip Successful completion — row returned, DKIM Pass, deploy succeeded.
error --danger Failure state — 5xx, DKIM Fail, tests failed.
neutral --accent Default when no highlight is set — also the brand colour for non-semantic emphasis.

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.

Info (soft) --info-soft Tinted bg for informational regions, descriptive node fills, info-coded callouts.
Warn (soft) --warn-soft Tinted bg for cautionary regions or nodes — a step that's recoverable but worth flagging.
Success (soft) --tip-soft Tinted bg for verified zones, completed states, success-coded callouts.
Error (soft) --danger-soft Tinted bg for error regions, failure rows, problem-coded callouts.

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.

Accent --accent Default active highlight on a step with no explicit highlight; primary buttons.
Accent (soft) --accent-soft Highlighted-data backgrounds: signed-message envelopes, extracted values, computation boxes.
Accent hover --accent-hover Hover state for accent buttons and links.

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.

Background --bg Page bg. Edge-label halo uses this so labels always sit on a clean background.
Surface elevated --bg-elevated Default node fill in <Flow> and the surface for callouts, code blocks, and chips.
Surface sunken --bg-sunken Inset surfaces — kbd chip, region background, deep panels.
Border --border Default border. Outlines for callouts, inputs, table cells.
Border (strong) --border-strong Default node stroke in <Flow>; emphasized table dividers.
Text --text Primary body text. Default text color for node labels and inside-shape text.
Text (muted) --text-muted Captions, secondary metadata, edge labels' surrounding text.
Text (subtle) --text-subtle Tertiary metadata — step counts, timestamps, deemphasized references.

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.

0 / 7
ClientAppCacheDB

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.

0 / 7
Storage tierClientAppRedisMySQL

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.

0 / 5
queuedrunningsucceededfailed

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.

0 / 5
commitbuildtesttests pass?deployalert

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.

0 / 7
EdgeApplicationDataBrowserCDNAPIRedisMySQL

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.

0 / 8
Send DKIM-signed EmailRecieve EmailExtract dkim-signaturefrom email headersFetch example.com DKIM recordat 's._domainkey.example.com'Return example.comDKIM recordYesNoSending MailServerPrivate DKIM keyDKIM-SIGNATUREincluded in headersReceiving Mail Serverd-example.coms=s.domainkeyexample.comDNSValidate message using publickey from DKIM DNS entryWas messageunchanged?DKIM PassDKIM Fail

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

— / 7
customers (500 rows)orders (800 rows)800rows600rows500rows400rows400rows400rows10rowsresultFROM customersJOIN orders ON c.id = o.cidWHERE order_date >= '2023-01-01'GROUP BY customer_idHAVING total_spent >= 1000SELECT customer_id, total_orders, total_spentORDER BY total_spent DESCLIMIT 10

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] } },
]} />
301122i

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.

abcd

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.

132

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
A sphere, a box, and a torus joined in a row

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:

  1. title="…" in the fence meta wraps the block in a <figure> with a caption.
  2. Range highlight: {3-5} in the meta string highlights those lines.
  3. Inline highlight: // [!code highlight] on any line.
  4. Diff: // [!code ++] for adds, // [!code --] for removes.
  5. Focus: // [!code focus] blurs every other line until the reader hovers the block.
OrderService.java
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);
}
user.ts
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.

![Two boxes connected by an arrow, a placeholder diagram showing how a content image is referenced from the public folder.](/notes/0000-example.svg)

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 new tag while a note is still settling. Every note ships as status: published by default — the new tag is the in-progress marker. Remove it once the note holds up on a cold re-read. Use status: archived (never deletion) to retire a note without breaking incoming references.