# 06 · Site

Astro (latest), static output, deployed to Cloudflare Pages from `site/`. Data is fetched client-side at runtime from the published JSON; **a data refresh never rebuilds the site**. Match the mockups in `../product.html` — they are the design spec, not inspiration.

## Pages

| route | source | mockup |
|---|---|---|
| `/` | `index.json` + `events.json` + `reads.json` | Surface 1 (leaderboard, ticker, hot themes, reads strip) |
| `/models/[slug]` | `models/<slug>/summary.json` (+ `launches.json` when state ≠ baseline) | Surface 2, incl. the what-people-say module and Now/Launch tabs |
| `/models/[slug]/themes/[topic]` | the theme object inside `summary.json` | Surface 2b: verdict, trajectory, facets, voiced-by, vs-the-field table, receipts wall. Own OG card ("67/1k · 2.3× frontier median" is the screenshot) |
| `/good-at/[tag]` | `tags.json` | the searchable capability index: typeahead ("Which model is good at…"), evidence-ranked table with validated verdict snippets + receipts. One static file, client-side search, own OG card per tag. Prerender shells for the top 100 tags weekly for SEO |
| `/launches` + `/launches/[slug]` | `launches.json` | day-0-aligned curves, score cards, overlay select |
| `/reads` + `/reads/[slug]` | `reads.json` + MDX files in-repo | Surface 3 article layout |
| `/methodology` | static MDX + live query strings from registry | product.html methodology section |
| `/open` | static MDX | "How we're open": open data/methodology/registry + formula packages, closed machine, why |

## Design system

Lift tokens exactly from the plan docs' CSS (`:root` block in `../product.html`): cream `#f6f0e3`, card `#fdf8ec`, ink `#2a2540`, accent lavender `#655999`, coral `#a04425`, olive `#5f6f3a`, gold `#9a7b2d`, paper-grain `body::before` SVG noise, Space Grotesk (display) + Inter (body) via Google Fonts with `font-display: swap`. Components to extract as Astro components: `Sentbar`, `Sharebar`, `Sparkline`, `AspectChips`, `SaySummary`, `AspDetail`, `ScoreCard`, `Ticker`, `VoicesTable`, `PostEmbed`, `WindowSwitcher`, `KpiRow`, `CapabilityScorecard` (rubric rows w/ n + trend), `TagChips` + `TagSearch` (typeahead over tags.json), `ThreadCard` (post embed + thread footprint: replies · participants · depth · organic/owned badge). The entity page's default content tab is **Deep threads** (organic roots only); "Owned" is its own labeled tab. The site brand mark is a placeholder (`vibebench` in mockups) behind one config constant: `SITE_NAME`.

## Hydration model

- Astro islands, vanilla TS, no React. Pages fetch `public/manifest.json` first (no-store), then the immutable run files it points to (cacheable forever) — see `08-workers.md` §7. Re-poll the manifest every 5 min when `document.visibilityState === "visible"`; entity pages in `launch` state poll every 60s and show the "LAUNCH MODE" pill.
- Charts: inline SVG generated from JSON in the island (the mockups' chart shapes are the rendering targets). No chart library in v0/v1.
- All islands render a skeleton from static HTML first; no layout shift on data arrival (reserve heights).

## X embeds

- Entity/leaderboard pages: `widgets.js` loaded once, lazily, on first embed scrolled near viewport. Render `blockquote.twitter-tweet` from `post_id` (`https://twitter.com/i/status/<id>` — author-less URLs work).
- Reads: oEmbed HTML fetched at **read authoring time** (publish.x.com, no auth) and stored in the MDX, so reads work even if widgets.js flakes.
- Fallback: if the iframe hasn't rendered 4s after widgets.js load, swap in the styled fallback card (post link, "view on X", author class badge from our data — no text, since public JSON has none).
- "Accounts to follow" uses official follow-button embeds only.

## OG cards

Worker route `/og/<kind>/<slug>.png` using `workers-og` (satori on Workers): leaderboard card (top-5 + total), entity card (name, vibe score, sentiment split, spark), launch card (score + curve). Meta tags per page point at these; cache 10 min at the edge.

## Quality bar

- Lighthouse ≥ 90 perf/a11y/best-practices on `/` and `/models/fable-5` (embeds lazy-loaded below the fold keep perf honest)
- Total JS budget excluding widgets.js: < 50KB gz
- Works fully (minus embeds) with widgets.js blocked — verified in a test
- Mobile: leaderboard collapses to cards at < 720px; entity KPIs wrap 2×; mockup grid proportions hold at 1280px
- `noindex` until the name/domain decision; flip via env
