Vol. 01 · Issue 01 · Spring 2026 · gf.cx specimen
knowledge base · gf.cx portfolio patterns

Patterns that travel

Operating patterns surfaced session-by-session from building the gf.cx portfolio — recipes, traps, design choices, disciplines. Each entry is a pattern that has earned its keep on one surface and is documented so the next developer (future-Dan, or anyone else building in this style) can recognise + reuse it without rediscovering the friction.
New here? Start with Why patterns — what a pattern is, from Alexander to the Gang of Four to directing AI agents.

Read-me · status of the stack

Source · Dan, 2026-05-27

"Can't tell you how important a pattern like 'Read-me · status of the stack' is to the growth. It's super logical and shows foresight and helps frame the interdependencies between entities and records/bindings and such like."

"This is a GOLD standard set of operating principles."

The problem

A system grows by accretion: new endpoints, new bindings, new vendor accounts, half-done refactors, parked sketches. Without a self-state-tracking surface, two things rot:

The shape

Production-blocking3 items+
Data lifecycle4 items+
Workflow / UX3 items+
Cross-surface integration3 items+
Operational hygiene4 items+
Tag-physical workflow3 items+
People / governance3 items+

An H2 "Read-me · status of the stack" near the bottom of an operator dashboard or category landing. Below it: a stack of collapsible <details> panels — one per priority tier, with:

Why R/Y/G works (and a flat checklist doesn't)

ChoiceWhy
Color via dot, not text-colorDot is at-a-glance even at peripheral vision. The dot does the work before any reading happens.
Categories named in operator's language"Tag-physical workflow" instead of "Hardware" — names the operator already uses internally. Eye finds the right bucket without translation.
Collapsible per-category23 items in a flat list is intimidating + scroll-heavy. 7 collapsed panels fit one phone screen; the eye picks where to dive in.
Count in summaryShows scope without opening. After completions: "2 of 3 done · 1 open" makes velocity visible.
✅ stays + datesVelocity is visible. Future-you knows what was resolved + when + by which session.
How-to-fix sub-line per open itemRe-entering work next session = one-line briefing. The dashboard is actionable, not just descriptive.
Lede summarizes velocity"5 of 23 resolved this session" primes the reader to see the page as alive + moving, not a static gripe list.

Where it applies

Strong candidates — surfaces where the page is both documentation AND a TODO:

Weak candidates — pages whose audience is external:

The portable shape

<h2 class="section-heading">Read-me · <em>status of the stack</em></h2>
<p class="lede">[snapshot date · N of M resolved · brief tier framing]</p>

<details class="gap">
  <summary>
    <span class="gap__pri">🔴</span>
    Category name
    <span class="gap__count">2 of 3 done · 1 open</span>
  </summary>
  <ul>
    <li><span class="gap__title">✅ item title</span> (done YYYY-MM-DD). Brief.</li>
    <li><span class="gap__title">item title.</span> Why it matters.
      <span class="gap__sub">How to close it · effort · who/what blocks.</span></li>
  </ul>
</details>

When it breaks

Trigger to add it to a page

When the page starts having tracked-but-unresolved decisions, bindings, or dependencies that span ≥ 3 categories. Below that, a single TODO list or a Cross-references block is enough. Above that, R/Y/G + collapsible structure starts paying off because the eye needs help finding what matters.

Reference implementation

Canonical exemplar as of 2026-05-27: home.gf.cx/service-records/ → Read-me · status of the stack. The dashboard tracks the cross-Worker service-records stack (svc.gf.cx + io.gf.cx/found). After 5 items flipped ✅ in one session, Dan named the underlying pattern — this patterns.gf.cx entry codifies it.

BASE_URL · one-line swap

The problem

A subdomain spins up on a Pages preview URL (foo-gf-cx.pages.dev) and its custom domain (foo.gf.cx) binds days later. In between, Worker code, tag-previews, OG images, analytics, README copy — every surface composing an absolute URL — references the wrong place. Hand-find-replace at swap time always misses one.

The shape

Export one canonical BASE_URL constant from one shared module. Every other surface imports it. Binding day = one-line edit + redeploy flips everything.

// src/config.ts
export const BASE_URL = "https://foo.gf.cx";

// everywhere else:
import { BASE_URL } from "./config";
const previewUrl = `${BASE_URL}/preview/${id}`;

When it breaks

Reference

The found.gf.cxio.gf.cx/found migration (2026-05-28) used sed across 4 external files for retroactive cleanup of hardcoded URLs. Codified into this pattern so the next subdomain starts correct rather than getting fixed at swap time.

Sandbox-first · discipline

The problem

Wiring production vendor credentials into a customer-facing surface before the integration is proven is how secrets leak, callback paths misfire, and the integration's true edge-cases ambush the live order path. The opposing risk — building a sandbox surface that no one ever wires up — wastes the dry-run.

The shape

Every vendor integration (Prodigi / Pwinty, Resend, Twilio, Stripe, Canva Connect) gets a self-contained surface at sandbox.gf.cx/<vendor>/ first. That surface holds the sandbox credential, does the full round-trip (OAuth, API call, callback, response inspection), and proves the path end-to-end. Only then does the same code path get promoted to the production surface with the production credential swapped in.

Why a dedicated subdomain

Reference implementations

Workers-with-Assets · run_worker_first

Source · Dan, 2026-05-28

"Default routes browser navigations through the assets-layer FIRST, bypassing the Worker. curl works, browser 404s. Should bake into gfcx_subdomain_new.py template."

The problem

A Cloudflare Pages project with a _worker.ts + assets binding looks like it should serve dynamic routes from the Worker and static files from the asset layer. In practice, browser navigations (requests with Sec-Fetch-Mode: navigate) route through the asset layer first, returning a 404 for routes the Worker would have handled. The symptom: curl returns 200, the browser returns 404. Easy to misdiagnose as DNS, cache, or routing.

The fix

# wrangler.toml — every new gf.cx subdomain serving both static + API
[assets]
directory = "./dist"
binding = "ASSETS"
run_worker_first = true   # ← Worker gets first crack at every request

Where it applies

How to recognise

If curl https://name.gf.cx/dynamic-route returns 200 and the same URL in a browser returns 404, with the response showing cf-cache-status: MISS or no Worker invocation log: this is the bug.

Reference

Cost 90 minutes on dare.co.uk/ip before the cf-cache-status HIT/MISS signal broke the diagnosis open. Captured 2026-05-28 after the same trap reappeared on sandbox.gf.cx's Workers-with-Assets migration — proof the pattern travels.

Visual verification · external probe

The problem

"Looks good from here" is not deploy verification. The local browser cache, the local NextDNS resolver, the CF edge cache for your IP, the SSL cert that resolved an hour ago — all of these can show a working site while a real user on a different network sees a 404, a bad cert, or an asset that hasn't propagated. The class of bug this misses: curl works, browser fails (see run_worker_first). An independent witness is the only reliable check.

The shape

After any non-trivial deploy of a public surface, hit it from an external visual probe — a screenshot service that renders the page from a third-party network, with no shared cache and no DNS overlap. The probe answers two questions in one shot:

Useful probes

Where it applies

If the local browser says "looks good" but no external probe has confirmed it, the deploy is unverified.

Tag-router · numeric + slug peers

The problem

QR-encoded asset tags need to be dense (a phone camera reads /1042 faster than /sennheiser-hd-560s-headphones) and re-assignable (the physical tag number outlives the asset description). But humans also need friendly URLs they can type, share, and remember. Choosing one over the other forces a compromise on the side you didn't pick.

The shape

Both numeric and slug forms are first-class peers in the Worker — neither is a redirect to the other. Both resolve directly to the same record:

// io.gf.cx/found Worker
GET /1042                    → render record #1042
GET /sennheiser-hd560s       → render record (slug-looked-up) #1042
GET /1042.json               → JSON projection of same record

The numeric ID carries class-prefixed structure for cheap human grouping (1xxx = vehicles, 2xxx = tools, 5xxx = electronics) without leaking the actual category into URL semantics — the prefix is a hint, not a contract.

Why both peers, not redirect

Reference implementations

gfcx_subdomain_new · scaffold

The problem

Each new <name>.gf.cx subdomain shares the same first hour of setup work: Pages project create, custom-domain binding, fonts, primitives (cards.css, media.css), declarative landing, 404 page, wrangler.toml, _redirects, deploy command, the dashboard URL for the custom-domain pane. Hand-rolling each one rediscovers the same friction every time — and forgets the small things (Workers-with-Assets run_worker_first, Rocket Loader data-cfasync, BASE_URL constant) the previous one taught.

The shape

One script — ~/bin/gfcx_subdomain_new.py — encodes the current state of "what a fresh gf.cx subdomain looks like." Run it with a name; it generates the repo, the Pages project, the deploy command line, and prints the dashboard URL where the custom-domain CNAME needs to bind. Every new subdomain starts from the latest learnings, not a blank slate.

The discipline

Each new subdomain that surfaces a friction (a missing field, a forgotten CF setting, a default that was wrong) feeds back into the scaffold itself before the next subdomain spins up. This is what makes the scaffold compound: the third subdomain inherits the second's edge cases, the tenth inherits all nine.

What the scaffold bakes in today (v0.3.0)

A fresh subdomain is no longer just a landing page — the generator stamps the full belts-and-braces baseline, so a new surface ships finished, not minimal. A surface missing any of these is under-finished:

The principle

Don't hand-roll a new gf.cx subdomain. The wrappers encode prior friction.

The same principle applies to the wider ~/bin/ portfolio: wrangler-deploy > raw npx wrangler, verify-cf-token > raw curl, gfcx_status_hub_render > hand-templated dashboard. Reach for the wrapper first — it has already paid the cost of every gotcha you haven't hit yet.

Pulse on icons · staggered wink

Source · Dan, 2026-05-29

"I keep wanting pulses on these icons too, like an easter egg."

Validated on data.gf.cx after the same session locked in the active-dot infinite pulse (the "you are here" marker) and the on-hover pulse on the gate-detail icons. The third move — a permanent, gentle pulse across the source-type glyphs on the active gate card — gave the surface a quiet pulse-of-life that reads more as character than as chrome.

The problem

A row of icons next to a label reads as static decoration. The eye logs them once and moves on. Without motion, you lose two things:

The shape

Each glyph wraps in a span that carries a class. The animation runs on every glyph, but nth-child selectors offset the phase. The result is a ripple — left to right, or whatever sequence the DOM order encodes.

<div class="icon-row">
  <span class="icon-row__glyph"><svg>...</svg></span>
  <span class="icon-row__glyph"><svg>...</svg></span>
  <span class="icon-row__glyph"><svg>...</svg></span>
  <span class="icon-row__glyph"><svg>...</svg></span>
</div>
.icon-row__glyph {
  transform-origin: center;
  animation: icon-wink 5.2s ease-in-out infinite;
}
.icon-row__glyph:nth-child(2) { animation-delay: 0.9s; }
.icon-row__glyph:nth-child(3) { animation-delay: 1.8s; }
.icon-row__glyph:nth-child(4) { animation-delay: 2.7s; }

@keyframes icon-wink {
  0%, 88%, 100% { transform: scale(1);    opacity: 0.82; }
  4%, 12%       { transform: scale(1.18); opacity: 1;    }
}

Live demo

Four glyphs, ~5.2s cycle, 0.9s phase offset between siblings — a quiet ripple left-to-right. Sit with it for 10 seconds to catch the full cycle.

The tuning

Where it belongs

This is a character animation, not a status indicator. Reach for it when:

It does NOT belong on form fields, navigation, primary CTAs, error states, or anywhere accessibility-critical. Pair with @media (prefers-reduced-motion: reduce) { .icon-row__glyph { animation: none; } } to respect user settings.

The principle

Static icons are decoration. Staggered icons are alive.

This pattern is the third in a family of pulse-as-affordance moves: infinite pulse = "you are here" (active marker, always running), hover pulse = "you can engage with this" (one-shot on rollover), and now staggered ambient pulse = "this surface has a heartbeat" (decorative, low-energy, sequential). Same CSS technique (scale + opacity keyframes), three semantic registers. Reach for whichever matches the meaning you're encoding.

Jargon tooltip · data-tip pattern

Source · Dan, 2026-05-31

"Nit-picking — can you add the tooltip pattern from assets.gf.cx to the metrics? FCP are human-readable. Audit/apply across all reports — anytime we use jargon-heavy, put its explainable tooltip in."

The problem

Technical reports and dashboards accumulate acronyms — FCP, LCP, CLS, INP, TTFB, GSC, CrUX. Readers who know them are fine; readers who don't can't act on the data. Adding a glossary page adds friction. Inline prose definitions bloat the table. The browser's native title="" tooltip solves it but looks awful: grey system font, 500ms hover delay, no viewport-clamping, no theme cohesion.

The shape

Two pieces: a CSS primitive from assets.gf.cx and a server-side term-annotator.

LayerWhat it does
assets.gf.cx/tooltip/tooltip.cssPure-CSS [data-tip] selector — dark card, caret arrow, viewport-clamped max-width: min(320px, calc(100vw - 32px)). No JS.
_annotate_jargon(html) in seo_render_html.pyRegex pass over rendered HTML — wraps known GLOSSARY terms in <abbr> with data-tip. Skips <code>, <pre>, <a>, <abbr>, <svg> blocks to avoid rewriting paths or SVG text nodes.

The traptitle= fires twice

The instinct is to set both title="…" (accessibility) and data-tip="…" (styled tooltip). Don't. The browser fires its own native gray box from title= ~500ms after hover — you get two overlapping tooltips simultaneously.

Fix: use aria-label="…" instead of title="…". Screen readers read it; the browser does not show a native tooltip.

<!-- ❌ Double tooltip — browser shows gray box + styled card -->
<abbr title="First Contentful Paint" data-tip="First Contentful Paint — …">FCP</abbr>

<!-- ✅ Single styled tooltip — aria-label for screen readers only -->
<abbr aria-label="First Contentful Paint — …" data-tip="First Contentful Paint — …">FCP</abbr>

Import

<link rel="stylesheet" href="https://assets.gf.cx/tooltip/tooltip.css">

Live demo — CSS-only variant

Hover the dotted term. Pure CSS, no JS — note that aria-label is used in place of title to suppress the browser's native gray box.

Server-side annotation

GLOSSARY = {
    "FCP": "First Contentful Paint — time until the first text or image is painted on screen (good ≤ 1,800 ms).",
    "LCP": "Largest Contentful Paint — time until the largest image or text block is visible (good ≤ 2,500 ms). A Core Web Vital.",
    # ... more terms
}

def _annotate_jargon(html: str) -> str:
    # stash <code>, <pre>, <a>, <abbr>, <svg> blocks first
    for term, expansion in GLOSSARY.items():
        safe = expansion.replace('"', "&quot;")
        html = re.sub(
            r"\b(" + re.escape(term) + r")\b",
            rf'<abbr aria-label="{safe}" data-tip="{safe}">\1</abbr>',
            html
        )
    # restore stashed blocks
    return html

Interactive variant — data-tip-href

When a tooltip needs a clickable "Learn more ↗" link, the CSS-only card can't deliver it (content: attr() is text-only, pointer-events: none). The interactive variant replaces the CSS card with a real DOM element for those terms.

AttributeRole
data-tip="…"Tooltip body text (same as base pattern)
data-tip-href="https://…"Triggers JS card instead of CSS card; "Learn more ↗" links here
aria-label="…"Screen-reader text (same value as data-tip)

CSS suppresses the ::after/::before pseudo-elements for these elements; the JS card takes over with pointer-events: auto. A 120ms hide delay lets the cursor travel from the trigger across the gap into the card without it closing.

<!-- CSS-only (no link) -->
<abbr aria-label="First Contentful Paint — …" data-tip="First Contentful Paint — …">FCP</abbr>

<!-- Interactive (with Learn more link) -->
<abbr aria-label="First Contentful Paint — …"
      data-tip="First Contentful Paint — …"
      data-tip-href="https://web.dev/articles/fcp">FCP</abbr>
<!-- Import both files; JS self-injects its card CSS -->
<link rel="stylesheet" href="https://assets.gf.cx/tooltip/tooltip.css">
<script src="https://assets.gf.cx/tooltip/tooltip-interactive.js" defer></script>

Limitations

Where it applies

Interactive tooltip · data-tip-href

Source · Dan, 2026-06-01

"Lets A/B test — for selector-area tooltips that have an embedded link that lights up red. This involved smart demarcation of the zone, to allow the user to 'click it' without it disappearing."

The problem

The CSS-only data-tip tooltip is text-only. content: attr(data-tip) renders as a plain string — no HTML, no links, no interactive elements inside the card. The pseudo-element also carries pointer-events: none, so even if you could inject a link, the cursor would fall straight through it. For glossary terms that have canonical reference pages (FCP → web.dev/articles/fcp), a "Learn more ↗" link inside the tooltip is genuinely useful — but CSS can't deliver it.

The shape

PieceWhat it does
data-tip-href="https://…"Signals that this element needs the JS card — the CSS card is suppressed for these elements via an explicit override rule in tooltip.css
tooltip-interactive.jsSelf-contained IIFE. Queries all [data-tip][data-tip-href] elements, injects a single shared DOM card (.tip-interactive) into document.body, positions it above each trigger on mouseenter
120ms hide delayThe guard that makes the pattern work. On mouseleave a timer schedules hide rather than hiding immediately — the cursor can travel from the trigger across the gap into the card. mouseenter on the card cancels the timer
Red "Learn more ↗" link.tip-interactive__linkcolor: #c8364c, bold, opens in new tab with rel="noopener"

The trap — hover-zone gap

If you hide the card on mouseleave of the trigger immediately, there is a pixel gap between the trigger element and the card (the caret arrow space). The cursor momentarily has no target — the card vanishes before the user reaches the link. The 120ms delay is the fix: small enough to feel instant, large enough for a deliberate move from trigger to card.

Live demo

Hover the dotted term below. When the card appears, slide your cursor across the gap to click the red "Learn more" link — the 120ms delay keeps the card open while you travel.

CSS suppression rule

Already baked into tooltip.css v0.2.0. Prevents the CSS card from doubling up with the JS card:

abbr[data-tip][data-tip-href]:hover::after,
abbr[data-tip][data-tip-href]:hover::before { display: none !important; }

Where it applies

Context within the field · inline origin chips

Source · Dan, 2026-06-04

"Not sure how this was encoded into the json, BUT it's a worthy pattern — you added where the traffic is originating. Very nice touch — helpful, and it's context-specific, within the field."

The problem

A diagnostic table row that shows what happened (path · count · status codes) leaves the next obvious question — where from? — to be answered by a click, a panel switch, or a fresh query. By the time the reader has navigated to a separate "by country" or "by user-agent" view, they've lost the row that prompted the question. The cost is small per row and large across a dashboard that has dozens of rows.

The shape

Earn the row another row. Under each diagnostic record, render a thin sub-row of origin chips — country codes + non-browser UA labels — scoped to that record's data. No new section, no drill-down: the qualifier sits in the field with the field it qualifies.

/ip                                                          44 ████████ 200 301
  ┌── chips sub-row ────────────────────────────────────┐
  │  CN 25  US 9  SG 6  HK 5  ·  headless-chrome 1  nginx-ssl 1  │
  └─────────────────────────────────────────────────────┘

Encoding · the three layers

LayerWhat it does
Fetch · scoped group-byOne GraphQL call groups by clientCountryName + userAgent, filtered to the probe paths you care about (/ip, /cf, /trace). Keeps the payload small and the chips per-row.
Classify · UA → short labelA small dict (_SCANNER_UA_PATTERNS) maps noisy UAs to dense labels: HeadlessChrome → headless-chrome, python-requests → python-requests, curl/ → curl. Browsers (no match) drop out — chips show only the interesting non-browser callers.
Render · attached sub-rowA second <tr> with colspan="4" immediately follows each parent row. Country chips first, then a · separator, then scanner chips in a different chip class. Empty origins → no sub-row (graceful degradation).

Why the chips belong in the field

Where it applies

Reference implementation

Don't break out to a panel what fits in a row. The qualifier belongs in the field with the field it qualifies.

Pattern preview · live demo + tabbed code

Source · Dan, 2026-06-08

"How could we put in real-code examples of the elements — so that we can see it, like how CodePen does it? Visual + code to the side. It's a preview pattern, for pattern library."

The problem

A pattern entry that documents interactive behaviour — a tooltip, a pulse, a hover-zone guard — earns one <pre><code> block in the prose and asks the reader to imagine what it looks like. The reader can either trust the description or open a new browser tab and rebuild it themselves. Either way, the learning loop is broken: the page tells you about the behaviour without letting you feel it.

The shape

A reusable block that pairs an isolated live demo with the source that produced it. Demo runs in an iframe sandbox so its styles never leak into the host page. Source is shown in a tabbed dark panel (HTML / CSS / JS) with a Copy button. Side-by-side at desktop, stacked at mobile.

PieceWhat it does
<template data-tab="html|css|js">Author writes raw source inside <template> elements. data-tab names the language for the tab label + iframe assembly.
data-external-css / data-external-jsComma-separated URLs of external assets to load inside the iframe head (e.g. assets.gf.cx/tooltip/tooltip.css). Lets a demo depend on the portfolio's primitive CSS without redefining it.
pattern-preview.js assembles srcdocReads each template, dedents the source, builds a single <iframe srcdoc="…"> with external assets in <head> + inline code in <body>. Iframe is sandbox="allow-scripts allow-popups".
Tabs + Copy buttonClick a tab to switch the source pane (HTML default). Copy button copies the currently-shown source to clipboard with a 1.6s "Copied ✓" confirmation.

Live demo — meta

This block is itself a pattern-preview, demoing the simplest possible interactive case: a button that increments a counter. Inspect the three tabs to see how html / css / js compose into the live iframe on the left.

The trap — host-page style leak

The naive version inlines the demo HTML directly into the patterns.gf.cx body. The host page's serif fonts, link styles, code styles, and CSS variables apply to the demo — the demo no longer looks like the surface where it'll actually run, and any aggressive demo CSS (e.g. body { background: black }) leaks back into the host page. The iframe sandbox is non-negotiable: each demo gets its own document scope, its own root element, its own stylesheet origin.

Why srcdoc over a separate file

Where it applies

Reference implementation

A pattern library that only describes its patterns is a glossary. A pattern library that lets you feel them is a workbook.

Source · Dan, 2026-06-18

"'Built with' can be added to patterns as a footer behavior … can we reveal/hide it, especially when the script list is long — do any of the scripts have references back to pattern/memory that can be linked programmatically?"

The problem

A composed page — a voiced article, an ingested clip, a generated dashboard — is the visible output of a pipeline the reader never sees: which script built it, which model transcribed it, when it ran. That provenance is real and worth keeping, but pasted raw at the foot of every page it's clutter, and it goes stale silently. Once the build list grows past three or four tools it dominates the footer it was meant to quietly annotate.

The shape

A single collapsible block: a <details> whose <summary> stays compact — label, tool count, an ingest <time> stamp visible while collapsed — and reveals the full per-tool list on click. Provenance-at-a-glance closed; full ledger open. No JS — <details> carries the reveal/hide for free; the disclosure triangle is a CSS ::before on the summary that rotates 90° on [open], with the native marker hidden.

<details class="provenance">
  <summary><span class="t">Built with</span><span class="meta">4 tools · ingested
    <time datetime="2026-06-18T12:50:15Z">18 Jun 2026</time></span></summary>
  <ul>
    <li><a href="https://kb.gf.cx/<slug>/"><code>~/bin/happiness_video_ingest.py</code></a> — ffprobe · poster · encode · R2</li>
    <li><code>gemini-2.5-flash</code> — verbatim transcription</li>
  </ul>
</details>

.provenance > summary::before { content: "\25B8"; transition: transform .15s; }
.provenance[open] > summary::before { transform: rotate(90deg); }
.provenance > summary::-webkit-details-marker { display: none; }

Linking back · the registry convention

The payoff is the second half of the question: when a listed tool has a canonical home — its governing pattern on this site, or the memory note that documents it (published at kb.gf.cx/<slug>/) — its row links there. The reader goes from what built this to why it's built this way in one click. The link targets already exist; what's missing is a mapping, not new infrastructure.

Keep the map in one place — the footer / OG helper that emits the block — rather than hand-authoring anchors per page:

BUILT_WITH = {
  "happiness_video_ingest.py": {
    "label": "~/bin/happiness_video_ingest.py",
    "href":  "https://kb.gf.cx/feedback_video_ingest_composable_transcripts_2026-06-18/",
  },
  # external tools: link to their own home, or omit href
  "ffmpeg": {"label": "ffmpeg / ffprobe"},
}

A script that wants to own its own provenance can instead declare it in its docstring — a # pattern: <slug> / # kb: <slug> tag the builder harvests — but the central registry is the floor: it works today with no edits to the tools themselves.

When it breaks

Reference

First live on happiness.gf.cx/behaviour/control-is-not-regulation/ (2026-06-18) — a three-clip ingest whose footer lists the ingest script, ffmpeg, the gemini-2.5-flash transcription step and wrangler, with the ingest script linking back to its composable-transcript directive on kb.gf.cx. Generalises toward the shared footer / ~/bin/og_capture.py helper so new surfaces inherit linkable provenance rather than a static list — same scaffold-it-as-the-floor logic as the scaffold baseline.

Editorial article · micro-decision grouping

Source · Dan, 2026-06-18

"a grouping of 3-4 micro-decisions that rolls up nicely."

The problem

An advice or how-to article written as flowing prose buries its useful parts. The reader has to read the whole thing to extract the moves, the writer has to find a single narrative thread to thread them on, and nothing in the piece is independently quotable or skimmable. The structure fights both the writing and the reading.

The shape

Compose the article as a thesis plus a small grouping of self-contained moves. Each move is a micro-decision: an imperative sub-heading — a single concrete instruction — followed by one to three tight sentences. Ideally three or four moves (the set is small enough to hold in the head); list pieces can scale to about a dozen. The grouping "rolls up" into one takeaway stated under the title.

  1. Title + a one-line thesis — the rollup; what it all amounts to.
  2. 3–4 labeled moves. Each is an imperative subhead + 1–3 sentences. Real subheads from the live exemplars: "Slow down by about twenty percent." · "Ask the second question." · "Match the greeting to the situation" · "Take control of the greeting."
  3. Each move is self-contained and skimmable — the set rolls up under the thesis, but any one move stands alone.
  4. Optional tail — crisis/footer lines, or the built-with provenance footer.

Why it works

When it breaks

Reference

The authoritative editorial pattern for articles is happiness.gf.cx. Live exemplars, from the ideal count outward:

Diagnostic probes · quarantine panel

Source · Dan, 2026-06-19

"This diagnostic pattern should be preserved … it's a good one. It should be a composable script that can be used easily."

The problem

Every gf.cx surface ships "where am I?" diagnostic endpoints — /ip, /cf, /trace (the functions/ip.ts · cf.ts · trace.ts that the scaffold stamps into each subdomain). Almost nobody hitting them is a reader: they're pinged by uptime checks, port scanners, headless bots, and your own probes. Left in the analytics, that traffic inflates the top-paths table and buries real reader behaviour. But just deleting it throws away a genuinely useful signal — who is poking the perimeter, and from where.

The shape

Don't delete the noise — quarantine it. Filter the probe paths out of the reader-facing tables, and surface them in their own collapsed <details> panel that answers the different question: per-path request count + bar + status codes, a country-of-origin breakdown, and a scanner-UA classification (headless-chrome, nginx-ssl, bastion, python-requests…). Collapsed by default — it's infrastructure noise, one click away, out of the reader's path — and rendered in an olive/brown that quietly says "not reader traffic." No JS: <details> carries the reveal for free.

Diagnostic probes — /ip · /cf · /trace (excluded from reader view)
/ip70200301
CN 24SG 18HK 18MX 12US 12·headless-chrome 2nginx-ssl 1bastion 1
Live — the panel rendered by diagnostic_probes_panel.py --demo. Click the summary to collapse.

Composable · two calls

The whole pattern is extracted into one dependency-free module — ~/bin/diagnostic_probes_panel.py — so any surface drops the panel in with aggregate() then render_panel(), or a single call if you have a Cloudflare token:

from diagnostic_probes_panel import aggregate, render_panel

# from your own Cloudflare httpRequestsAdaptiveGroups rows:
panel = render_panel(aggregate(rows))      # → self-contained HTML, own CSS, no JS

# or end-to-end (needs an Analytics:Read token):
panel = render_panel(fetch(token, zone_id))

# or just look:  diagnostic_probes_panel.py --demo

The shape passed between the two halves is a stable contract — aggregate the noise once, render it anywhere:

{ "/ip": { "requests": 70, "statuses": [200, 301],
           "countries": [["CN", 24], ["SG", 18], ...],
           "scanners":  [["headless-chrome", 2], ["nginx-ssl", 1], ...] } }

When it breaks

Reference

Extracted from dare_cf_analytics.py's probe section — the dare.co.uk dashboard, where the panel first earned its keep filtering /ip · /cf · /trace out of the reader tables — and generalised into ~/bin/diagnostic_probes_panel.py on 2026-06-19. The probe endpoints themselves are part of the scaffold baseline (every surface gets /ip · /cf · /trace); this panel is their natural back-of-house companion — same scaffold-it-as-the-floor logic.

The footnote pattern

Source · Dan, 2026-06-22

"let this be the 'footnote pattern' as it also highlights in red, and it picks out highlight bold for particular words or phrases."

The problem

A long-read accretes reader questions and replies in the margins. Dumped inline they swamp the spine of the piece; given long descriptive titles they turn the footer into a wall of competing headlines. You want the appendix dense and scannable when closed, and fully self-announcing when opened — without the collapsed list shouting over the article above it.

The shape

Each footnote is a collapsible <details> whose closed row is a single word, and whose open body leads with the full title carried inside. Two type signatures do the emphasis: a red highlight for the key term, and a bold pick-out for the particular word or phrase under examination.

  1. Summary = one word. The collapsed row is just a muted footnote label and a single italic keyword, right-aligned into a clean column. Every keyword on the page is unique — it is the footnote's handle.
  2. Title carried into the body. The full descriptive title moves to the first line of the opened body (.fn-headline) and ends on that same emphasized keyword — so closed stays a one-word column, open announces itself.
  3. Two signatures. Red <em> (the accent colour) carries the running emphasis and the keyword; bold <strong> picks out the one word or phrase being examined. Nest <em><strong>…</strong></em> for bold-red.
  4. Body convention. The prompt — the reader's own words — opens as a single red-italic paragraph; the reply follows, picking out terms in red and bold as it lands on them.
<details class="context-note">
  <summary><span class="fn-kind">footnote</span>
    <em style="color:inherit;text-transform:none;letter-spacing:0;">glance</em></summary>
  <div class="context-note__body">
    <p class="fn-headline">Silence over complaint, the choice that was yours — the backward <em>glance</em></p>
    <p><em>…the prompt, in the reader's own words…</em></p>
    <p>…the reply, picking out a <em><strong>word</strong></em> where it lands…</p>
  </div>
</details>

The collapsed row right-aligns the keyword to a column, and the carried-in headline sits at the top of the opened body:

.footnotes .context-note summary em { margin-left: auto; }
.fn-headline { font-family: var(--serif); font-size: 19px; line-height: 1.3;
               color: var(--ink); font-weight: 600; margin: 0 0 16px;
               padding-bottom: 13px; border-bottom: 1px solid var(--line); }
.fn-headline em { font-style: italic; color: var(--accent-aches); }

Why it works

When it breaks

Reference

Origin and authoritative exemplar is happiness.gf.cx's behaviour/ long-reads:

Named by Dan on 2026-06-22. Pairs with the micro-decision article grouping — that pattern shapes the spine, this one shapes the margins.

Memorable URL · canonical redirect

Source · Dan, 2026-06-24

"Rather than writing vault.gf.cx/software/tailscale — do vault.gf.cx/tailscale and have a redirect manage the forwarding, with the correct canonical URL. It demonstrates ease-of-use, memorable URL structures with the correct canonical."

The problem

Good taxonomy buries good URLs. A page earns its place deep in a section — vault.gf.cx/software/tailscale/ is correct: it lives under /software/ with its siblings, and that path is what crawlers and the sitemap should treat as the one true home. But nobody types or remembers a three-segment path. Ask someone to "go to the tailscale page" and they'll try vault.gf.cx/tailscale first. Flatten the structure to win the memorable URL and you lose the taxonomy; keep the taxonomy and you lose the door people actually knock on.

The shape

You don't choose. Keep the content at its canonical deep path, and add a short, memorable vanity URL that 301-redirects to it. Two artefacts, one source of truth:

# _redirects — memorable vanity → canonical deep path
/tailscale     /software/tailscale/          301
/tailscale/*   /software/tailscale/:splat    301
<link rel="canonical" href="https://vault.gf.cx/software/tailscale/">

Now advertise the memorable one (vault.gf.cx/tailscale) everywhere a human reads — cross-links, social, print, "go to…" — and the structured one stays the canonical record. The redirect bounces the door to the room; the canonical tag tells the crawler which is the room.

Why 301, not 302

A vanity URL is a permanent promise, so use 301 (permanent): browsers and crawlers cache it and consolidate ranking signals onto the canonical target. Reserve 302 for genuinely temporary forwards where the destination is still settling (e.g. an IA mid-migration). Mixing them up either fails to pass equity (302 where you meant permanent) or hard-caches a move you'll want to undo (301 where you meant temporary).

When it breaks

Where it applies

Reference

First shipped 2026-06-24: vault.gf.cx/tailscale301vault.gf.cx/software/tailscale/ (canonical). The earlier /field-notes/<vendor>/<vendor>/ rules on the same surface are the temporary-302 sibling of this permanent-301 pattern — same mechanism, opposite intent.