Skip to content

Plain AI Playbook

The everything-you-need page for working on plainai.aguidetocloud.com — the AI explainer reading site. Static-first, JSON-driven, fully isolated planet in the cosmos. Written 2026-05-05; positioning corrected 8 May 2026.

Positioning note (8 May 2026 — Sush brand correction)

The early build of Plain AI used a museum metaphor as a design north-star ("Direction C — Museum re-skin", "PLATE № NN" labels, "exhibit" language). Sush retired that framing 8 May 2026 with a clear positioning lock: "Plain AI is where to learn AI in plain language — accessible to everyone. It's not a museum." The visual register (dark-default, cyan/purple/pink gradient covers, JetBrains Mono labels) stays — it's atmosphere, not metaphor. The language has been corrected: tagline is "AI explained plainly, for everyone", topic labels are Topic NN · 78 (not PLATE № NN), and the cosmos-rail tagline reads the same in every aria-label. References to "museum" remaining in this playbook describe design-history phases, not current positioning. When picking up new work, treat Plain AI as a plain-English reading site for the curious — not a museum, not an exhibit, not a curated gallery.

When this applies

Any code change in C:\ssClawy\plainai\. The repo's commits and the Plain AI Status page are the chronological record; this page is the operating manual.

TL;DR — what's at stake

  • Site: plainai.aguidetocloud.com
  • Repo: C:\ssClawy\plainai\ · GitHub susanthgit/plainai (private) · Cloudflare Pages project aguidetocloud-plainai
  • Stack: Pure Node 18+ generator (no Astro / Hugo / Next) → static HTML/CSS/JS → Cloudflare Pages
  • Identity: dark-default · Inter + JetBrains Mono · charcoal #0B0D14 / ivory #F2EFE8 / cyan #67E8F9 accent · cyan/purple/pink gradient palette · positioning: AI explained plainly, for everyone
  • Content: 78 topics across 7 categories + 4 reading paths + 1 about page (84 pages total)
  • Build: ~1s for 84 pages
  • Deploy: ~30s end-to-end via direct CF Pages API (works on win32-arm64 where wrangler doesn't)
  • Cosmos law #6 firewall: No paid-content ever. No /guided/ links, no practice-exam questions, no paraphrased exam content.

File map (where things actually live)

Path What it is
build.mjs The generator. Pure Node, no external deps. Reads content/*.json → writes dist/*.html. Touch with care.
deploy.mjs Direct Cloudflare Pages API uploader. BLAKE3-hashes every asset (matches wrangler's algorithm), uploads only the missing ones, creates a deployment.
audit.mjs Playwright + axe sweep over all 84 pages. Must report 0 violations + 0 tap-targets <44px before push.
content/config.json Brand text, taglines, theme colors, footer text. Single source for site-wide strings.
content/categories.json The 7 categories. Each has id, name, kicker, icon.
content/paths.json The 4 reading paths. Each has slug, name, intro, closing, stops_label, stops_brief, stops[].
content/topics/*.json The 78 topics. One file per topic. Adding a topic = adding a file.
content/pages/about.json The About page (and any future static page goes here too — build.mjs walks the folder).
templates/styles.css The ~26 KB stylesheet. Touch this when changing the visual identity.
templates/theme.js Theme toggle. Local-storage only. No prefers-color-scheme auto-flip (museum default is dark).
templates/search.js Live filter on the home grid + tag-filter URL params (?tag=scam).
public/_headers CSP, security headers, cache-control. Don't add immutable to CSS/JS again — see the cache incident below.
public/_redirects (Empty for now — CF Pages handles .html stripping natively.)
public/favicon.svg The favicon (gradient P square).
scripts/set-gh-secret.mjs Encrypts the CF API token via libsodium NaCl sealed-box → uploads as GH Actions repo secret. Run once after rotating the CF token.
.github/workflows/deploy.yml Push-to-deploy. Calls node build.mjs && node deploy.mjs with the secret token.

The content model

Topic JSON shape

Every topic in content/topics/*.json has the same shape:

{
  "slug": "deepfake",
  "title": "Deepfake",
  "categoryNum": 3,
  "kicker": "SYNTHETIC MEDIA",
  "description": "Card description — one or two sentences, what shows on the home grid.",
  "tags": ["scam", "beginner", "urgent"],
  "gist": [
    "First paragraph of the 30-second gist.",
    "Second paragraph if needed."
  ],
  "expandables": [
    { "summary": "Section heading", "meta": "60s read", "body": "<p>HTML body…</p>" }
  ],
  "example": {
    "label": "WHAT HAPPENED",
    "body": "Real, sourced incident, 1–3 sentences."
  },
  "connected": ["voice-clone", "fake-reviews"]
}
Field Required Notes
slug yes URL slug, matches filename <slug>.json. Lowercase, dashes, no spaces.
title yes Display title. Sentence case usually; ALL-CAPS abbreviations preserved (LLM, RAG).
categoryNum yes 1–7, matches categories.json
kicker yes Short uppercase eyebrow above the title (e.g. "SYNTHETIC MEDIA")
description yes The card subtitle on the home grid. Must NOT contain "30-second gist" boilerplate — the home shows that label automatically. Make this specific and concrete.
tags yes Array of lowercase tags (e.g. scam, beginner, urgent). Used by ?tag= URL routing.
gist yes Array of 1–3 paragraphs. Plain-English, no jargon without gloss. This is the entire point.
expandables yes (can be empty) Optional depth. summary is the <details> heading, meta is the right-aligned read-time label, body is the inner HTML.
example optional A real, named, sourced example. No fabricated case studies.
connected yes (can be empty) Slugs of related topics. Render as a "Connected" footer list.

Adding a topic

  1. Pick the next available slug. Confirm it's not already in content/topics/.
  2. Decide categoryNum (1–7). If the topic doesn't fit any existing category, stop and discuss — adding a category is a v2 decision.
  3. Write the topic JSON. Use the shape above. Keep gist to ≤200 words, ≤2 paragraphs.
  4. Update connected arrays in 1–3 nearby topics to mention this new one (manual cross-linking, no algorithm).
  5. If this topic belongs in an existing reading path, add it to stops[] in content/paths.json. Pick a position deliberately — paths are ordered.
  6. Run the SME check (manually): every claim in gist and expandables must be:
  7. factually correct as of build time, OR
  8. clearly bracketed as opinion ("most experts agree…", "in my view…")
  9. sourced if it's a specific number, study, or incident
  10. never a fabricated study (see the NHS chatbot lesson below)
  11. Run node build.mjs && node audit.mjs. Should report 84 pages clean (was 83, now 84).
  12. Eyeball at http://127.0.0.1:8771/<slug> after python -m http.server 8771 in dist/.
  13. git push → GH Actions deploys → live in ~30s.

Adding a category

Don't, lightly. 7 is already pushing the visible-without-scrolling threshold. If you must: 1. Add to content/categories.json with a new id (next integer). Pick an icon from the existing aesthetic (∴ ⚙ ⚭ ☉ ▲ ◆ ★ etc.) — single character, displays in the cat-icon badge. 2. Update at least 5 topics' categoryNum to use it (categories with <3 topics look empty and unloved). 3. Consider whether this category needs a reading path of its own.

Adding a reading path

  1. Add a new entry to content/paths.json with slug, name (must be a real-life moment in first-person, e.g. "I work in a non-tech job"), intro, closing, stops_label (short summary like "5 stops"), stops_brief (string like "voice clones · panic call · safe-word"), and stops[] (array of topic slugs in order).
  2. Hand-curate the order. Don't sort alphabetically. Don't rank by category. Walk the reader through the moment.
  3. Topics in the path must already exist as content/topics/*.json. The build will throw a clear error if a stop references a missing topic.
  4. The path will auto-render at /path-<slug> and appear on the home page paths grid.

The build pipeline

build.mjs — what it does, in order

  1. Wipes dist/
  2. Loads all content JSON
  3. Computes a stable plateBySlug (1..N from global sort order)
  4. Computes content hashes for each template asset (sha256 → 8 chars) → ?v=<hash> query string for cache busting
  5. Renders 78 topic pages, 4 path pages, 1 index, 1 about page
  6. Generates sitemap.xml + robots.txt
  7. Copies templates (styles.css, theme.js, search.js) and public files (_headers, _redirects, favicon.svg) to dist/

Things to know about the generator

  • Each topic has a stable plate number. It's derived from the global topic sort order (categoryNum, then title alphabetical). Adding a new topic will shift plate numbers for topics alphabetically after it within the same category. That's intentional — plates don't carry meaning, they're just museum labels.
  • The hero gradient uses taglineLead/taglineHighlight/taglineTail from config.json, not the unified tagline. Keep both in sync if changing.
  • Internal links drop .html — CF Pages auto-resolves /<slug>/<slug>.html and .html URLs 308-redirect to clean ones. Internal links must use the clean form to skip the redirect hop.
  • The real-example aside has id="example-h" so the museum aside's TOC can anchor-link to it. Don't remove that ID without updating renderTopic.

deploy.mjs — what it does

  1. Reads dist/ and computes BLAKE3 hash of base64(content) + extension for every file (32 hex chars). Matches wrangler's algorithm exactly — verified by uploading the same dist with both tools and getting identical hashes.
  2. Calls CF API to fetch a 1-time JWT for asset uploads
  3. Calls CF API with the list of hashes to find which assets the CDN already has cached
  4. Uploads only the missing assets (typically 0 on rebuilds where only HTML changed)
  5. Creates a new deployment via the Pages API with the manifest

Token source: CLOUDFLARE_API_TOKEN env var first; falls back to ~/.copilot/secrets/cloudflare-api-token for local runs.

audit.mjs — what it does

  • Spins up a Chromium via Playwright
  • Loads every page (index + 78 topics + 4 paths + about) at 1280×900
  • Runs axe-core's full accessibility analysis
  • Counts tap-targets <44×44px (interactive elements smaller than the WCAG 2.5.5 minimum)
  • Reports per-page failures + a summary count

Hard rule: must report 84/84 clean before any push that touches templates or content.


The deploy gates (mandatory before pushing)

  1. node build.mjs succeeds with no errors
  2. dist/ contains 84 HTML files + sitemap + robots + assets
  3. node audit.mjs reports 84/84 cleanaxe=0, tap<44=0
  4. ✅ Hard-refresh the live site after deploy and confirm changes rendered (cache trap defence)
  5. ✅ Sush voice rules respected — see Voice & Tone
  6. ✅ Cosmos law #6 — no paid content references anywhere

The visual identity rules (DON'T break)

These are the rules that keep Plain AI from collapsing back into a generic Stripe-Docs-lite. If you're tempted to break one, stop and ask Sush.

Type

  • Display + body: Inter (single font does double-duty — clean, neutral, readable in long form, dignified in display). Never Lora, Playfair, or any serif (those are Shift's territory).
  • Mono: JetBrains Mono. Used only for: kickers, plate numbers, meta labels, "PATH · NN" path-card numbers. Never for body text.
  • Caps + tracking: uppercase + letter-spacing: 0.14–0.18em for kickers and labels. This is the museum-label aesthetic. Reserve for: kickers, section labels, plate numbers, masthead strap.

Colour

  • Background: dark default #0B0D14. Light mode is opt-in only.
  • One accent: cyan #67E8F9 (dark) / #0E7490 (light, deeper for AA contrast).
  • Gradient palette only: cyan (#67E8F9) → purple (#A78BFA) → pink (#F472B6). Used in: wordmark AI, hero <span class="grad">, gradient mesh covers. Do not introduce a 4th hue or a 2nd accent.
  • No greens, ambers, reds. Those belong to other planets.

Layout

  • 64ch reading column for body content (not wider).
  • Topic page is 2-column on desktop: main article left, museum aside right. Collapses to single column at 980px.
  • The aside is sticky on desktop, static on mobile.
  • Cards have a 16:9 cover at the top. Don't replace the SVG with photos. Don't remove the cover (it's the museum-label proof).

Motion

  • Hover transitions ≤ 0.2s ease-out. Properties: transform, border-color, color, box-shadow, background. Never animate width, height, padding.
  • No parallax, no scroll-jacking, no sticky animations.
  • prefers-reduced-motion honored — all transitions clamp to 0.01ms.

What's banned

  • Glassmorphism, backdrop-filter, neon glows, scanlines, marquee/ticker effects (those are Brain Bar and Shift's flavours)
  • Per-topic / per-category accent colours (one cyan accent only — differentiation comes from the gradient covers)
  • "Cute" emoji headers (🚀 / 🔥 / ) — the museum tone is dignified, not chirpy
  • AI-generated photography or illustrations (the gradient covers are deliberate non-imagery)

Voice rules (Sush's, applied to Plain AI specifically)

For the universal rules, see Voice & Tone. The Plain-AI-specific extensions:

  • Plain English with glosses for jargon — every acronym is expanded once on first use. Every metaphor is concrete (café, post office, library, party — never "ontology", "epistemology", "modality").
  • No scaremongering. Tools that genuinely have a scam vector get clear warnings; tools that are just unfamiliar get no doom framing.
  • No fake reassurance either. "AI will be fine" is as bad as "AI will end the world." If the honest take is "we don't know yet", say that.
  • No fabricated case studies. Every example is real, named, and ideally has a year + a public source. The lesson here cost us — see "The NHS chatbot incident" below.
  • No "10x", no "revolutionary", no "game-changing". That's hype-speak. We're a museum, not a pitch deck.
  • Direct address — "you", not "the user". And occasionally "we" when honest.
  • Honesty over charm. It's fine to say "I don't fully understand this either, but here's what I do know."

Cache + headers — DON'T add immutable again

This is the most important operational gotcha for Plain AI. Read it twice.

What happened (5 May 2026, ~12:00 NZST)

The Pocket-Reference v1 build shipped with _headers containing:

/*.css
  Cache-Control: public, max-age=31536000, immutable
/*.js
  Cache-Control: public, max-age=31536000, immutable

Hours later, the Museum re-skin shipped — same fixed asset paths (/styles.css, /theme.js, /search.js). The Cloudflare edge cache held the old Pocket-Reference CSS for the next year, served alongside the new museum HTML. Result: museum-styled HTML rendered with light pocket-reference CSS. Looked like nothing had changed.

The two-layer fix (in place)

  1. public/_headers now uses:
    /*.css
      Cache-Control: public, max-age=3600, must-revalidate
    /*.js
      Cache-Control: public, max-age=3600, must-revalidate
    
    Stale entries are revalidated against origin via ETag.
  2. build.mjs computes sha256(file)[:8] for each template asset and appends ?v=<hash> to all HTML references. Different content → new URL → new CDN/browser cache key. Always.

The rule

  • Only add immutable to URLs that contain a content hash in the path itself (e.g. /assets/styles-abc123.css).
  • Fixed-name assets (/styles.css, /theme.js) MUST stay on relaxed cache-control. The ?v=<hash> query string handles cache busting; the relaxed header handles the case where the query-string approach is bypassed by a stale HTML page somewhere.

Pivot history (the why behind every choice)

From "AI for grandma" → "One Thing" → "Pocket Reference" → "Plain AI"

  1. AI for grandma was the spark. Sush wanted his actual mum to be able to use AI safely. The name was placeholder-honest but it boxed the audience too narrowly. Adults curious about AI are a much wider group, and most of them don't have grandkids.
  2. One Thing was the next pivot — a single-topic-per-day site. Killed because: too thin, would die in months, no surface to grow on, fragile to my own pace.
  3. Pocket Reference (Direction D from a 4-direction prototype hub) emerged as the actual product shape: many topics, 30-second gist, depth-on-demand, 4 reading paths for specific moments. Direction picked because it was the only one that worked for both the "I have a panic moment about a phone scam call" reader and the "I'm just curious what RAG means" reader.
  4. Plain AI as the name was picked at the build-pipeline phase. Considered: Lucid AI, Just AI, AI in English, The Plain Layer, Quiet AI, Two-minute AI. Plain AI won because: "Plain" is the verb of what the site does (simplifies), "AI" is the noun without ambiguity. Two words, six letters, instant comprehension.

The 4-direction visual prototype (proto-v3)

After v1 shipped functionally, Sush said "compared to Brain Bar and Shift, this looks uninspired." Fair. Built four bold prototypes side-by-side:

Direction Aesthetic Verdict
A — Pocket Field Guide Sand dot-grid paper + spine binding + dark title bars Strong, but too close to Shift's warm-paper editorial atmosphere — would have broken cosmos law (don't collide).
B — Reading Room Stone-grey paper + Lora serif + drop caps + magazine layout Even more Shift-like. Rejected.
C — Museum Exhibit Charcoal canvas + gradient mesh covers + ivory text + cyan accent Picked. Distinctive, museum-tone, dark-default earns its own gravity.
D — Risograph Zine Putty paper + pink/teal halftone + mono display + handwritten Caveat pull-quote Bold, designer-screenshot-friendly. Rejected as too youthful for the audience (older, non-tech adults).

The lesson: prototyping all four was 1 hour of work and made the decision certain. Without prototypes, Sush would have picked C in theory and we would have rebuilt twice. With them, the choice was concrete and final.

Why dark-default + opt-in light (not OS-preference auto-flip)

The museum opens at night. That's the metaphor we sold. If a user's OS is set to light and we auto-flipped on first visit, they'd see the light companion mode — beautiful, but not the museum's first impression. Decision: dark always wins on first visit. Light is the explicit alternative.

The toggle is in the masthead. It persists via localStorage. There is no prefers-color-scheme fallback. This was a deliberate departure from the typical "respect OS preference" rule.


Lessons learned (apply to future planets)

1. Prototype the visual identity before you ship

Plain AI's first version (Pocket Reference) shipped the content correctly but without a distinctive visual identity. We had to re-skin. Cost: ~2 hours rebuild, ~3 hours of design time. What would have prevented it: running the 4-direction prototype hub before settling on Pocket Reference. Lesson: every new planet build now includes a "what's the atmosphere?" prototype phase upfront.

2. Two SME passes are non-negotiable for any factual content

The first pass on Plain AI caught: - A fabricated NHS chatbot study that I had hallucinated and confidently cited - A misstated description of BloombergGPT as "fine-tuned" when it was actually trained from scratch - Several wrong context-window numbers - An incorrect C2PA acronym

Lesson: for any content site, schedule SME and voice passes as part of the build, not as polish. Use a research subagent for SME-pass — it catches what voice-pass can't, and vice versa.

3. The CDN cache trap is real

The immutable, max-age=31536000 header is fine for content-hashed asset URLs (/assets/styles-abc123.css) and fatal for fixed-name URLs (/styles.css). This bit Plain AI 4 hours after the museum re-skin shipped. Lesson: every static-site planet must use content-hashed asset URLs from day one, OR use relaxed cache-control on fixed names. Never both. Never neither.

4. The right-rail empty space is an opportunity, not a problem

Plain AI's first version had ~600px of empty space to the right of every detail page on desktop. The fix wasn't to widen the article column (which would have hurt readability) — it was to fill the rail with something content-appropriate. The museum aside (PLATE № NN + on-this-page TOC + back-to-category) is now one of the most distinctive elements of the site. Lesson: before adding sidebars or widening columns, ask "what would a museum put in this space?"

5. Walk the site as a real user before considering it done

The UX walk on 5 May 2026 — 4 contexts × 10 user steps × screenshot review — caught 8 issues that the audit + manual eyeballing hadn't surfaced. Including: - Path breadcrumb said generic "Reading path" not the actual path name (high impact) - Search-meta line in mono caps read like a console error (medium impact) - Inconsistent vocabulary: "trails" in hero, "Reading path" in body (low impact, eroded trust) - Empty desktop right-rail on detail pages (high impact)

Lesson: every planet build now ends with a real-user UX walk in 4 contexts (dark+light × desktop+mobile). The walk is non-negotiable.

6. Pure Node generator > framework for content-only sites

Plain AI's build.mjs is ~400 lines of pure Node 18+ with zero external dependencies. Build time: ~1s for 84 pages. Compare to Astro / Hugo / Next: equivalent build with dependencies, configs, plugins, framework-isms. For a static, content-driven site, the framework is overhead. Lesson: when the answer is "JSON in, HTML out", write the generator.

7. The wrangler-on-arm64 deploy gotcha is permanent (for now)

Cloudflare's wrangler CLI doesn't run on win32-arm64 — the workerd binary isn't shipped for that platform. Plain AI's deploy.mjs (direct CF API + BLAKE3) is the workaround. It produces identical output to wrangler (verified by hash comparison). Lesson: never make a workflow depend on wrangler being installable on every machine. Direct-API is the durable path.


Patterns to reuse for future planets

Pattern Where it came from Reusable for
Pure Node generator (no framework) Plain AI Any content-only static site (≤200 pages)
Content-hashed asset URLs (?v=<sha256:8>) Plain AI Every static site that ships CSS/JS
Direct CF Pages API deploy via BLAKE3 Plain AI Every planet (especially on win32-arm64 dev machines)
GH Actions + NaCl-encrypted secret for token Plain AI Every repo that needs a CD pipeline with a CF token
4-direction visual prototype hub before re-skin Plain AI proto-v3 Every visual identity decision worth >2 hours
Real-user UX walk (4 contexts × 10 steps × screenshots) Plain AI v1.1 UX pass Every planet pre-launch and after every visual rebuild
Topic-first JSON + cross-link connected[] Plain AI Any reference / glossary product
Stable plate numbers from global sort Plain AI Any catalog with a deliberate exhibit sequence
Reading paths (hand-curated topic sequences) Plain AI Any catalogue where users come with specific moments, not generic browsing

The non-obvious win

The most underrated outcome of the Plain AI build is the museum metaphor itself. Brain Bar found "terminal", Shift found "newspaper wire", and Plain AI found "museum". Three completely different metaphors, three completely different visual languages, all coexisting under one cosmos.

The metaphor isn't decoration — it shapes every product decision. Plates not articles. Aside not sidebar. Path not playlist. Exhibit not section. When the words match the metaphor, the design follows on its own.


This file is a living document. Update it after each Plain AI work session with what worked, what didn't, and any new operational rules.