# Specs for everettroeth.com Living documentation for proposed and in-progress site changes. Each spec is one feature, written before implementation, updated as things ship. - Site: https://everettroeth.com - Site index: https://everettroeth.com/llms.txt - Full essay corpus: https://everettroeth.com/llms-full.txt - Source: https://github.com/everettroeth/ev-site/tree/main/docs/specs ## Index - **SPEC-001** text-wrap balance and pretty [shipped, effort S] - **SPEC-002** code block copy button and language label [shipped, effort M] - **SPEC-003** RSS feed at /feed.xml [shipped, effort S] - **SPEC-004** prefetch Continue-Reading targets [shipped (covered by Next.js Link default prefetch + SPEC-005 Speculation Rules), effort S] - **SPEC-005** Speculation Rules API for instant navigation [shipped, effort M] - **SPEC-006** per-essay Open Graph image [shipped, effort M] - **SPEC-007** mobile responsive pass [shipped (verified 375 and 768), effort M] - **SPEC-008** orrery animation IntersectionObserver gate [shipped, effort S] - **SPEC-009** external link preview fetcher [shipped, effort L] - **SPEC-010** /threads topology navigation [shipped (early launch with 4 essays; will grow as content lands), effort L] - **SPEC-011** DESIGN.md as single source-of-truth [shipped, effort M] - **SPEC-012** agent accessibility surface [shipped (specs route + llms.txt update); MCP card and public Skill deferred, effort M] --- --- id: SPEC-001 title: text-wrap balance and pretty status: shipped effort: S frontier: CSS text-wrap balance, CSS text-wrap pretty related: ADR-0005 last_revised: 2026-05-13 --- # SPEC-001: text-wrap balance and pretty ## Goal Better line breaks across headlines and body prose without manually inserting non-breaking spaces. Headlines balance (no orphans, even line lengths). Long-form prose uses `pretty` for single-orphan avoidance and improved hyphenation hints. ## Current state Default `text-wrap: wrap`. Headlines occasionally produce widow words on narrow viewports. Body prose has ragged last lines. ## Implementation outline In `app/globals.css`: - `text-wrap: balance` on `article > header h1`, `article > header p` (subtitle), `.prose-editorial h2`, `.prose-editorial h3` - `text-wrap: pretty` on `.prose-editorial p`, `.prose-editorial li` - Add under a labeled comment block `/* === typography polish === */` ## Acceptance criteria - Headlines on 375-1100px viewports no longer leave a single word on the last line - Body paragraphs avoid orphaned final words where the rendering engine supports it - No layout shift, no perf regression - `pnpm typecheck && pnpm test` clean - Browser-verified: visit a-letter-to-the-room at 375 and 1280, observe headline line breaks ## Frontier features - `text-wrap: balance` (CSS Text Module Level 4, baseline 2024 in Chrome/Edge/Safari/Firefox) - `text-wrap: pretty` (Chrome 117+, gradual rollout; falls back to `wrap`) ## Edge cases - Long headlines may take more layout passes; UA implementations cap balance at about six lines - Firefox/Safari fall back to `wrap` for `pretty` until support lands; acceptable --- --- id: SPEC-002 title: code block copy button and language label status: shipped effort: M frontier: Clipboard API, ARIA live regions, rehype-pretty-code data attrs related: ADR-0005 last_revised: 2026-05-13 --- # SPEC-002: code block copy button and language label ## Goal Every code block in essays carries a quiet copy button in the corner and a small monospace language label. Clicking copy puts the code on the clipboard and pulses a "copied" affordance for ~1.5s. ## Current state Code blocks render via `rehype-pretty-code` with syntax highlighting. No copy affordance. Language is implicit. ## Implementation outline - New `components/mdx/code-block.tsx` client component wraps `
` in a relative container - Override `pre` in `mdx-components.tsx`: `pre: (props) =>` - Read `data-language` from the wrapped ` ` for the label - Button uses `navigator.clipboard.writeText(preRef.current?.textContent ?? "")` - `aria-live="polite"` region announces "copied" - CSS in `globals.css` under `/* === code block copy === */` - Position: language label top-left, copy button top-right; both tiny, low opacity, full opacity on hover/focus ## Acceptance criteria - Copy button visible in top-right of every `` rendered from MDX - Click copies code text exactly as displayed - "Copied" label appears for 1.5s then reverts to "copy" - Language label visible in top-left when `data-language` present - Mobile: button tappable (min 40x40 hit area, button itself can stay smaller via padding tricks) - No console errors; existing syntax highlighting unchanged - Browser-verified: copy from a code block, paste into shell, content matches source ## Frontier features - Clipboard API (`navigator.clipboard`) - ARIA live region - `:has()` for parent-aware focus styles (optional) ## Edge cases - Clipboard API requires secure context (HTTPS or localhost); works in Vercel and dev - Old browsers without Clipboard API: button still renders, click is a no-op silently - Very long code blocks: button stays in top-right via sticky positioning if needed --- --- id: SPEC-003 title: RSS feed at /feed.xml status: shipped effort: S frontier: Next.js App Router route handlers, Atom 1.0 related: ADR-0003 last_revised: 2026-05-13 --- # SPEC-003: RSS feed at /feed.xml ## Goal Readers can subscribe to new essays via any RSS or Atom reader. Feed lives at `/feed.xml`. ## Current state No feed. Readers rely on direct visits or social channels. ## Implementation outline - New `app/feed.xml/route.ts` exporting a `GET()` handler - Iterate `getAllArticles()`, excluding `unlisted` - Render Atom 1.0 XML with `` per article: id (canonical URL), title, link, summary (description or preview), updated (revised or date), published, author - Cache-Control: `public, max-age=300, s-maxage=3600` - Add `` to root layout `` ## Acceptance criteria - `GET /feed.xml` returns 200 with `application/atom+xml; charset=utf-8` - Validates against the W3C Feed Validator - Auto-discovery: reader apps find it via `` - HTML in summaries is properly escaped (no broken CDATA) - Dates in RFC 3339 ## Frontier features - Atom 1.0 (preferred over RSS 2.0 for richer metadata) - Next.js Route Handlers with static rendering ## Edge cases - Unlisted essays excluded - Essays without `description` use the derived `preview` (already in articles meta) - XML entities escaped in title and summary --- --- id: SPEC-004 title: prefetch Continue-Reading targets status: shipped (covered by Next.js Link default prefetch + SPEC-005 Speculation Rules) effort: S frontier: link rel=prefetch, Next.js Link prefetch related: SPEC-005 last_revised: 2026-05-13 --- # SPEC-004: prefetch Continue-Reading targets ## Goal The next-essay HTML and JS are warmed in the browser cache before the user clicks. Acts as the Firefox/Safari fallback when SPEC-005 (Speculation Rules) isn't supported. ## Current state Next.js Link prefetches automatically when links enter the viewport. This SPEC formalizes and supplements that. ## Implementation outline Two options: - **Option A** (lighter): rely on default `` behavior (already in `components/continue-reading.tsx`). Verify in DevTools that Continue-Reading targets prefetch on viewport entry. - **Option B** (explicit): emit `` for the three related slugs in `app/writing/[slug]/page.tsx` head. Lean A first; add B only if Speculation Rules prerender (SPEC-005) doesn't cover the gap. ## Acceptance criteria - DevTools Network shows prefetch entries for Continue-Reading targets when they scroll into view - Click on a prefetched link reuses the cache (no duplicate fetch) - Bandwidth budget unchanged for users who don't click through ## Frontier features - `` baseline - Next.js automatic Link prefetch ## Edge cases - Respect `prefers-reduced-data` if it ships as a CSS media query and we ever support data saver --- --- id: SPEC-005 title: Speculation Rules API for instant navigation status: shipped effort: M frontier: Speculation Rules API, prerender, eagerness levels related: SPEC-004, ADR-0006 last_revised: 2026-05-13 --- # SPEC-005: Speculation Rules API for instant navigation ## Goal When a reader clicks a Continue-Reading link, the next page is already fully rendered in the browser. No spinner, no load. The click resolves instantly. Falls back to default navigation in unsupported browsers. ## Current state Next.js prefetches link HTML/JS. The click still requires hydration and paint. Speculation Rules `prerender` goes further: the browser runs the next page in a hidden tab, ready to swap in on activation. ## Implementation outline - In `app/writing/[slug]/page.tsx`, after `getRelated`, build: ```ts const rules = { prerender: [{ urls: related.map((a) => `/writing/${a.slug}`), eagerness: "moderate" }], }; ``` - Render `` server-side inside the article page tree - `eagerness: "moderate"` triggers prerender on stable hover or touchstart (recommended default per WICG) - Confirm `next.config.mjs` does not interfere (it currently only enables `viewTransition`) ## Acceptance criteria - Chrome DevTools > Application > Speculation Rules shows the registered rule on article pages - Hovering a Continue-Reading link initiates `prerender`, status visible in Network tab - Clicking results in instant activation (no white flash, no skeleton) - Firefox and Safari ignore the script tag (no errors) - Validated against the WICG Speculation Rules draft ## Frontier features - Speculation Rules API (Chrome 121+, Edge 121+) - `eagerness` levels: immediate, eager, moderate, conservative - Cooperative prerendering (browser caps active prerenders to ~10) ## Edge cases - Servers can refuse via `Supports-Loading-Mode: credentialed-prerender` header. We don't set headers, defaults are fine. - Prerendered pages run side effects (analytics, etc.). This site has none beyond JsonLdScript and View Transitions, both idempotent. - Resource budget: the browser may decline if memory is tight. Acceptable degradation to default navigation. --- --- id: SPEC-006 title: per-essay Open Graph image status: shipped effort: M frontier: Next.js dynamic image routes, @vercel/og, Satori related: ADR-0001 last_revised: 2026-05-13 --- # SPEC-006: per-essay Open Graph image ## Goal Sharing an essay on social or chat surfaces produces a tailored card: kicker, title, accent stripe, site mark. Replaces the current generic OG image. ## Current state `app/opengraph-image` exists with `@vercel/og` but appears to render a generic card. Per-essay accent and metadata not reflected. ## Implementation outline - `app/writing/[slug]/opengraph-image.tsx` (dynamic per slug) using `ImageResponse` - Fetch `getArticle(slug)` for kicker, title, accent - 1200x630 layout: - Background: `--color-paper` (#0D140C) - Accent stripe at top, 8px, from `lib/accent.ts` - Kicker top-left, JetBrains Mono, uppercase - Title centered, Cormorant Garamond italic - Site mark bottom-right, DM Sans uppercase - Font subsetting at build time (Latin only) to keep payload under 200KB ## Acceptance criteria - Each essay's OG image renders kicker, title, accent - Twitter Card Validator, LinkedIn Post Inspector, Facebook Sharing Debugger all parse correctly - Generation time < 500ms on edge runtime - Title truncation: 60 chars with ellipsis - Missing accent falls back to amber ## Frontier features - Next.js dynamic OG image routes (`opengraph-image.tsx` per route) - `ImageResponse` from @vercel/og (Satori under the hood) - Edge runtime image generation ## Edge cases - Long titles wrap to two lines max - Special characters: keep to Latin-1 subset to avoid font fallback - Caching: Vercel CDN handles per-slug cache headers automatically --- --- id: SPEC-007 title: mobile responsive pass status: shipped (verified 375 and 768) effort: M frontier: container queries, prefers-reduced-motion, pointer fine, hover hover related: every 2026-05-13 UI ship last_revised: 2026-05-13 --- # SPEC-007: mobile responsive pass ## Goal Every interactive feature added in the 2026-05-13 session gracefully degrades on touch and narrow viewports. Cursor wordmark, ambient noise, link previews, word-hover notes, section TOC, view transitions, code block copy. ## Current state Cursor wordmark, link previews, and notes already gate hover via `(hover: hover) and (pointer: fine)`. Section TOC is hidden under 1200px. Ambient noise hasn't been touch-verified. Code copy (SPEC-002) needs sized hit targets. ## Implementation outline QA pass with `gstack-browse` at: - 375x812 (iPhone) - 768x1024 (iPad portrait) - 1024x768 (iPad landscape, small laptop) - 1440x900 (default desktop) - 1920x1080 (large) At each viewport, check the article page and homepage for: - Console errors (must be zero) - Cursor wordmark: tilt only with fine pointer - Ambient noise: visible at bottom, IntersectionObserver pauses out of view - Link previews: tap shows popover, tap outside dismisses - Word-hover notes: tap shows annotation - Section TOC: hidden below 1200px, visible above - Footnote: tap toggles popover - Code block copy: tap works, "copied" feedback visible - Continue-Reading: tappable cards - Drop cap: not awkwardly large on 375px - Reading flow: 16px minimum body, comfortable measure Add narrow-viewport overrides where needed in `app/globals.css`. ## Acceptance criteria - Zero console errors at every tested viewport - All interactive features have a touch-equivalent affordance - No horizontal scroll anywhere - Drop cap, code blocks, and the prose ruler all render readably at 375px - Reduced motion respected: time-of-day drift still applies (CSS variable swap, not motion) ## Frontier features - Container queries for component-local breakpoints (consider for ContinueReading and EndStrip) - `pointer: coarse` for touch detection - `prefers-reduced-motion` already wired across keyframes ## Edge cases - iPad Mini (768): TOC stays hidden (threshold is 1200px) - Foldables (884x500 landscape): fall back to mobile is acceptable - iOS Safari rubber-banding: `overscroll-behavior` already set --- --- id: SPEC-008 title: orrery animation IntersectionObserver gate status: shipped effort: S frontier: IntersectionObserver, animation-play-state related: TODOS.md last_revised: 2026-05-13 --- # SPEC-008: orrery animation IntersectionObserver gate ## Goal Pause the orrery's corona pulse, sun pulse, and twinkle animations when the orrery is scrolled out of view. Minor GPU and battery win, demonstrates respect for the device. ## Current state `components/orrery.tsx` renders the homepage hero orrery. The keyframes `.orrery-corona`, `.orrery-corona-b`, `.orrery-sun-pulse`, `.orrery-twinkle*` run continuously regardless of viewport. ## Implementation outline - In `components/orrery.tsx`, attach an `IntersectionObserver` to the orrery root SVG element - When `isIntersecting`, set `data-orrery="active"`; otherwise `data-orrery="paused"` - In `app/globals.css`, add: ```css [data-orrery="paused"] .orrery-corona, [data-orrery="paused"] .orrery-corona-b, [data-orrery="paused"] .orrery-sun-pulse, [data-orrery="paused"] .orrery-twinkle, [data-orrery="paused"] .orrery-twinkle-b, [data-orrery="paused"] .orrery-twinkle-c { animation-play-state: paused; } ``` - Initial state on mount: `active` if intersecting, `paused` otherwise (no flash) ## Acceptance criteria - Animations pause when the orrery is fully out of viewport - Resume when scrolled back; no animation re-start, just continuation (CSS pause preserves frame) - `prefers-reduced-motion` continues to skip the animations entirely - No console errors - No layout shift ## Frontier features - IntersectionObserver - `animation-play-state: paused` baseline-supported ## Edge cases - SSR: render without `data-orrery`, attach observer on mount; brief moment of running animation off-screen is acceptable - Multiple orreries (none today): observer should attach per instance via ref --- --- id: SPEC-009 title: external link preview fetcher status: shipped effort: L frontier: edge runtime, Cache API, HTML metadata parsing related: SPEC for internal link previews (shipped), ADR-0006 last_revised: 2026-05-13 --- # SPEC-009: external link preview fetcher ## Goal Hovering an external link shows a popover with the linked page's title, favicon, and (optionally) description. Internal links already get this; external is the parallel for completeness. ## Current state ` ` only triggers on `/writing/ ` hrefs. External `` tags fall through to plain `target="_blank"` links without previews. ## Implementation outline - New route `app/api/link-preview/route.ts` with `GET` handler accepting `?url=...` - Server-side fetch the URL with 3s timeout, read first 32KB - Parse ` `, ``, ``, ``, `` via a small streaming parser (regex on the first 32KB is acceptable) - Cache the parsed result for 7 days using Vercel KV (or in-memory dev fallback) - Return JSON: `{ title, description?, favicon?, image? }` - Client-side `mdx-components.tsx` `` override: for external hrefs, route through a new ` ` that fetches `/api/link-preview?url= ` on first hover, caches the response in memory, renders popover - Skeleton during fetch, hide on error - Reuse `usePopoverAnchor` and the link-preview CSS class ## Acceptance criteria - Hover an external link, popover shows title + favicon within ~500ms (cached) or ~1500ms (fresh) - Failed fetches gracefully fail silent: link still works, no popover, no error in console - Server-side respects robots.txt for the target site - No raw HTML leaked to client; only parsed metadata - Cache TTL respected; expired entries refetched ## Frontier features - Edge runtime route handler - Vercel KV or Cache API for caching - Lightweight HTML parsing (no heavy dependencies) ## Edge cases - Cloudflare-blocked sites: fall back to favicon-only via Google S2 favicon proxy - Sites without ` `: use hostname as title - HTTPS-only fetch (refuse `http://`) - Same-origin URLs: route through internal LinkPreview instead (no fetcher needed) --- --- id: SPEC-010 title: /threads topology navigation status: shipped (early launch with 4 essays; will grow as content lands) effort: L frontier: SVG, :has(), View Transitions, force-directed layout related: ADR-0006 last_revised: 2026-05-13 --- # SPEC-010: /threads topology navigation ## Goal Separate route `/threads` showing essays as nodes in a constellation. Edges connect essays that share frontmatter `tags`. Click a node to read. Chronological reading list at `/writing` stays unchanged. Visually distinctive and matches the eco-sci aesthetic (think: night-sky telemetry, not a knowledge graph diagram). ## Current state Only the chronological listing exists at `/writing`. No tag system. Essays lack tags. Memory note: revisit when ~10 essays exist. ## Implementation outline - Add optional `tags: string[]` to article frontmatter - `lib/articles.ts` exposes `tagGraph(): { nodes, edges }` - Edges weighted by number of shared tags; only top-N per node retained - `app/threads/page.tsx` (server component) renders the constellation as SVG - Layout: small custom force-directed simulation (under 100 lines) or precomputed positions cached in `data/threads-layout.json` - Nodes: small circles colored by primary tag; kicker label on hover - Edges: hairlines, opacity scaling with weight - Click a node: navigate to `/writing/ ` with View Transition (shared element) - Mobile: simplified list view fallback (skip SVG) ## Acceptance criteria - At least 10 essays with tags before launch - Constellation renders without overlap; nodes spaced at minimum 80px apart - Hover shows essay kicker and title - Click navigates with View Transition shared element on the title - Performance: layout < 100ms, interaction at 60fps - Mobile renders the list fallback ## Frontier features - View Transitions cross-page shared element - `:has()` for hover-aware adjacent node highlighting - SVG path animations on edge appearance ## Edge cases - Essays with no tags: drift to outer ring, marked as "untagged" - Very dense graphs: cap edges per node to top 5 strongest - Layout determinism: seed the force simulation so rerenders are stable --- --- id: SPEC-011 title: DESIGN.md as single source-of-truth status: shipped effort: M frontier: n/a (documentation) related: TODOS.md, ADR-0001 last_revised: 2026-05-13 --- # SPEC-011: DESIGN.md as single source-of-truth ## Goal A single `DESIGN.md` at repo root that consolidates the visual system: palette tokens, type scale, motion easings, spacing, component recipes, micro-interaction inventory, decision history. Future contributors and agents have one canonical doc instead of hunting across globals.css, ADRs, and AGENTS.md. ## Current state Design info lives in `app/globals.css` `@theme` block, ADR-0001 (stack and identity), ADR-0002 (voice), ADR-0006 (micro-interactions), AGENTS.md (implementation snapshot). Anyone joining has to read all five. ## Implementation outline Sections: 1. **Identity** — eco-sci framing, voice cues (cross-link to ADR-0002) 2. **Palette** — every CSS custom property with hex + usage notes (paper, ink-soft, sage, amber, rule, glass, etc.) 3. **Typography** — Cormorant Garamond (display), Source Serif 4 (body), DM Sans (chrome), JetBrains Mono (micro). Sizes, line heights, weights, intended roles. 4. **Motion** — `--ease-out-expo`, `--ease-out-quint`, duration scale (220ms standard, 140ms popover open, 60s ambient strands), reduced-motion policy 5. **Spacing & layout** — article max-width 720px, prose breakpoint 640px, TOC breakpoint 1200px, header height 6. **Components** — recipes for HUD, hairlines, biome grid, grain, kicker, telemetry strip, drop cap, Footnote, Note, LinkPreview, SectionTOC, AmbientNoise, cursor wordmark, scroll-encoded favicon, TitleProgress, view transitions 7. **Frontier features in use** — pointer to AGENTS.md "Stay on the frontier" 8. **Decisions** — pointer to ADRs with one-line summary each ## Acceptance criteria - Every CSS variable in `@theme` documented with intent - Every shipped component has a recipe (HTML structure + class names + state matrix link) - Cross-links to ADRs preserved (no duplication of decision text) - Indexed in AGENTS.md and `/llms.txt` - Renders cleanly in GitHub preview ## Frontier features - n/a; documentation only ## Edge cases - Staleness risk; AGENTS.md should include "update DESIGN.md when adding new palette tokens" rule --- --- id: SPEC-012 title: agent accessibility surface status: shipped (specs route + llms.txt update); MCP card and public Skill deferred effort: M frontier: llms.txt convention, .well-known discovery, content negotiation related: ADR-0003 last_revised: 2026-05-13 --- # SPEC-012: agent accessibility surface ## Goal Any AI agent crawling the site finds a single, clear entry point and from there can discover and read every text surface: site index, full essay corpus, spec catalog, design system reference, structured data. Minimal duplication, maximum discoverability. ## Current state Already shipped (ADR-0003): - `/llms.txt` — site index for agents - `/llms-full.txt` — concatenated full essay text - `/humans.txt` — human note - `/robots.txt` — with Content Signals (`ai-train: yes`) - JSON-LD Person + Article on every page - Markdown content negotiation at `/writing/ ` (sends raw MDX when `Accept: text/markdown`) ## Implementation outline Ship now: - New route `app/llms-specs.txt/route.ts` concatenating every file in `docs/specs/*.md` with a small TOC header - Update `app/llms.txt/route.ts` to surface `/llms-specs.txt` and `/llms-full.txt` and link to specs directory - Add `` in root layout `` Defer (June 2026 MCP spec stabilization): - `/.well-known/mcp/server-card.json` - `/.well-known/agent-skills/index.json` - Public Claude Skill at `/skills/everettroeth/SKILL.md` ## Acceptance criteria - `GET /llms-specs.txt` returns 200 with `text/plain; charset=utf-8`, all spec content concatenated with a TOC header - `GET /llms.txt` references the specs route in its body - HTML `` includes the alternate link - An agent fetching only `/llms.txt` can discover every other text surface - All surfaces UTF-8, no BOM, LF line endings - Browser-verified via curl with `Accept: text/plain` ## Frontier features - llms.txt convention (community-driven, evolving) - `.well-known` discovery (deferred until MCP spec settles) - HTTP content negotiation - Static-rendered route handlers in Next.js App Router ## Edge cases - Some agents prefer JSON-LD over llms.txt; both surfaces are served - Spec files added to `docs/specs/` after this ship: route reads the directory at request time, so they appear automatically - Cache: same `public, max-age=300, s-maxage=3600` as existing llms.txt