skip to note
back to writing

wfd / Jul 21, 2026 / 9 min

writings for discussion

writing things down is important. it forces you to think clearly about what you're building, what you've learned, and what you still don't understand. a half-formed idea in your head stays half-formed until you try to put it into sentences.

this WFD supersedes WFD 12. WFD 12 remains in the archive as a legacy document. WFDs are never deleted, even when superseded, because they're a record of how things were at the time they were written.

WFD (Writing for Discussion) is how i capture that process. it's a document format inspired by Oxide's RFD system, which itself draws from the original spirit of the IETF Request for Comments:

Notes are encouraged to be timely rather than polished. Philosophical positions without examples or other specifics, specific suggestions or implementation techniques without introductory or background explication, and explicit questions without any attempted answers are all acceptable. The minimum length for a note is one sentence.

the bar for writing a WFD is intentionally low. if you can explain it in a sentence, that's enough to start. polish comes later, or not at all.

when to write a wfd

this document says "you" a lot. the WFD format isn't proprietary or exclusive to this site. if the structure works for you, take it. adapt it. make it yours. that's the whole point.

anything worth remembering is worth writing down. some examples:

  • debugging a problem that took more than an hour
  • learning something that surprised you
  • building something and wanting to document the decisions
  • an opinion about tools, patterns, or process
  • something you'd want to reference later

there's no approval process. no review board. no minimum length beyond one sentence. if the idea matters to you, write it down.

writing conventions

don't repeat what another WFD already said

if something has been covered in a previous WFD, reference it instead of restating it. a link to the relevant WFD is always better than a paraphrase. this keeps documents focused and avoids content that drifts out of sync when the original gets updated.

drafts should use arguments for/against

when a WFD is in draft and hasn't reached a decision yet, lay out the tension explicitly:

arguments against:

- reason one
- reason two

arguments for:

- reason one
- reason two

this makes the open question visible. the reader can see exactly what's unresolved and why a decision hasn't been made yet. see WFD 18 and WFD 19 for examples.

wfd metadata and state

every WFD starts with YAML frontmatter:

---
number: 17
title: "writings for discussion"
updated: "2026-02-11T00:35:00"
state: "published"
labels: ["process"]
excerpt: "short description of what this document covers."
---

the fields:

fieldrequireddescription
numberyessequential WFD number. WFD 1, WFD 2, WFD 17. no gaps required.
titleyeslowercase. short. descriptive.
updatedyesISO 8601 datetime with time. the only timestamp that matters.
stateyescurrent lifecycle state (see below).
labelsyesarray of tags. keep it to three or fewer. use [] for none.
excerptyesone or two sentences. shows up in search results and the listing page.

states

a WFD can be in any of these states:

statemeaning
draftplaceholder. not ready for anyone to read.
discussionactively being written or revised. feedback welcome.
publishedthe idea is formed and the document says what it means to say.
committedi'm committing to this post. it's not going to change significantly.
livingactively maintained. will be kept up to date on a best effort basis.
legacywritten before the WFD system existed. migrated from the old blog format. may not follow current conventions.
abandonedthe idea didn't pan out. kept for the record.

states are freeform text. these are suggestions. i might not even follow them myself because i felt like a word meant something different that day, or because i wanted a state that doesn't exist yet. type whatever makes sense.

unlike Oxide's RFD process, there's no branch-per-document workflow, no pull request for discussion, and no formal review. documents are markdown files in a content directory. state changes are just a frontmatter edit.

numbering

WFDs are numbered sequentially. WFD 1, WFD 2, WFD 17. no dashes, no leading zeros in the display. the number is permanent. if a WFD is abandoned, the number stays taken.

updated, not created

there is no creation date. only updated. if you come back six months later and rewrite half the document, the timestamp reflects that. the creation date is noise. what matters is when the document was last touched.

the markdown engine

the rendering pipeline is custom. i didn't want to use an off-the-shelf markdown engine like MDX, Contentlayer, or Markdoc because they all impose opinions about how content should be structured, and i wanted full control over what syntax is available and how it renders.

the engine is built on remark and rehype for the base markdown-to-HTML conversion, with shiki for syntax highlighting. everything else is custom parsing on top.

the parser works in a single pass over the raw markdown lines. it maintains state machines for code blocks, admonitions, HTML blocks, and media groups. each line is checked against a series of regex patterns in order:

  1. if we're inside a fenced code block, accumulate lines until the closing fence.
  2. if we're inside an HTML block (<details>, <div>, etc.), pass lines through to remark as-is.
  3. if we're inside an admonition (> [!NOTE]), accumulate continuation lines that start with >.
  4. check for code block openings, heading patterns (for TOC extraction), admonition starts, component directives, and media lines.
  5. if a line matches ![alt](url), @gif[alt](url), @video[caption](url), or @embed[title](url), it's collected into a media group.
  6. consecutive media lines without a non-empty separator become a single carousel.
  7. everything else is accumulated as text and flushed through remark when a non-text block is encountered.

the output is an array of typed content blocks (text, code, media, mermaid, admonition, component) that React components consume directly. there's no intermediate AST transformation or plugin chain beyond what remark and rehype provide for the text blocks.

this means adding new syntax is straightforward: add a regex to parseMediaLine or a new block type to the line scanner, add a corresponding React component, and it's done. the @gif, @video, @embed, and @component syntaxes were all added this way.

the tradeoff is that the parser is imperative and stateful rather than declarative. it's not as elegant as a proper AST visitor pattern, but it's simple to debug and easy to extend. every feature in this document was added in under an hour.

writing format

WFDs are written in markdown with some extensions. everything below is available in any WFD.

text

standard markdown: **bold**, *italic*, ~~strikethrough~~, `inline code`, [links](url).

bold text, italic text, bold and italic, strikethrough, inline code, a link

headings

use ## through ####. these are extracted for the table of contents sidebar on desktop and the bottom sheet on mobile. # is reserved for the document title.

code blocks

fenced with triple backticks. specify the language for syntax highlighting.

```typescript
const x: number = 42;
```
const x: number = 42;

tables

standard GFM tables. they render with rounded borders and a semi-transparent background.

| header | header |
|--------|--------|
| cell   | cell   |
featuresyntaxexample
bold**text**bold
italic*text*italic
code`code`code

blockquotes

> quoted text goes here.

this is a blockquote. it can span multiple lines.

admonitions

GitHub-style alerts with five types:

> [!NOTE]
> useful information.

> [!TIP]
> helpful advice.

> [!IMPORTANT]
> key information.

> [!WARNING]
> urgent information.

> [!CAUTION]
> risk of negative outcome.

Custom titles:

> [!NOTE/Custom Title]
> content with a custom title.

useful information that readers should know, even when skimming.

helpful advice for doing things better or more easily.

key information that readers need to achieve their goal.

urgent information that needs immediate attention to avoid problems.

advises about risks or negative outcomes of certain actions.

Custom Title Here admonitions can have custom titles instead of the default.

images

![alt text](url)

consecutive images on adjacent lines automatically group into a scrollable carousel. images display at their natural aspect ratio with a max-height constraint.

mountain vista at dawn forest path in autumn coastal cliffs at sunset

gifs

@gif[description](url)

videos

@video[caption](url)

link embeds

@embed[title](url)

wfd embeds

reference another WFD by slug or number to render an inline preview card with its title, excerpt, state, labels, and reading time.

@wfd[exploring-liquid-glass]
@wfd[14]

exploring liquid glass

related writing: 14

mermaid diagrams

```mermaid
graph TD
    A[Start] --> B{Decision}
    B -->|Yes| C[Done]
    B -->|No| D[Retry]
```

supports flowcharts, sequence diagrams, entity relationship diagrams, and anything else mermaid supports.

graph TD
    A[Start] --> B{Is it working?}
    B -->|Yes| C[Great!]
    B -->|No| D[Debug]
    D --> B
sequenceDiagram
    participant User
    participant Server
    User->>Server: Request
    Server-->>User: Response

collapsible sections

<details>
<summary>click to expand</summary>

hidden content here. supports any markdown.

</details>

hidden content goes here. supports any markdown:

  • lists work fine
  • bold text renders correctly
  • even code: const x = 1

react components

@component[ComponentName]
@component[ComponentName]({"prop": "value"})

embeds interactive React components directly in the document. components are registered in BlogComponents.tsx and render inside a macOS-style frame.

available components: PixelLoader, PixelLoaderDemo, PixelLoaderAdjacencyDemo, ThreeDemo

three.js scenes

Three.js scenes are a special case of react components. they render interactive 3D scenes inline using @react-three/fiber and @react-three/drei, dynamically imported with SSR disabled since WebGL requires a browser context.

@component[ThreeDemo]
@component[ThreeDemo]({"height": 400})

the ThreeDemo component is a showcase. for actual WFD content, create purpose-built scene components in src/components/blog-demos/, register them in BlogComponents.tsx with next/dynamic and { ssr: false }, and reference them with @component[YourScene].

the pattern for adding a new Three.js component:

  1. create the component in src/components/blog-demos/YourScene.tsx with "use client"
  2. use @react-three/fiber's Canvas and @react-three/drei for helpers (controls, materials, environments)
  3. register in BlogComponents.tsx:
const YourScene = dynamic(() => import("./blog-demos/YourScene"), {
  ssr: false,
  loading: () => <ComponentFrame title="scene"><LoadingFallback /></ComponentFrame>,
});
  1. use in markdown: @component[YourScene]({"height": 400})

Three.js components are heavy. keep scenes focused and avoid loading external models unless necessary. the dynamic import with ssr: false ensures the Three.js bundle only loads when the component scrolls into view on client.

lists

unordered (-), ordered (1.), and task lists (- [ ] / - [x]).

  • first item
  • second item
    • nested item
  • third item
  1. first step
  2. second step
  3. third step
  • uncompleted task
  • completed task
  • another pending item

spoiler tags

discord-style spoiler syntax. text is hidden behind a blurred overlay and revealed on hover.

||spoiler text here||

this is a spoiler: ||the cake is a lie||. hover to reveal it.

spoiler tags work inline with other text and respect inline code, so ||this is not a spoiler|| inside backticks renders as-is. added in WFD 27 because some posts need to discuss plot details without ruining things for people who haven't watched yet.

quick reference

elementsyntax
bold**text**
italic*text*
strikethrough~~text~~
code`code`
link[text](url)
image![alt](url)
heading## heading
quote> quote
list- item
ordered list1. item
task list- [ ] task
table| col |
code block```
mermaid```mermaid
note> [!NOTE]
video@video[caption](url)
gif@gif[caption](url)
embed@embed[title](url)
wfd embed@wfd[slug-or-number]
component@component[Name]
three.js scene@component[ThreeDemo]
collapsible<details>
spoiler||text||