# Pitfalls Research — Page Publishing Flow v1.0

**Research date:** 2026-05-21
**Confidence:** HIGH (most pitfalls derive from concrete codebase evidence + well-documented patterns)

## Critical Pitfalls (rewrite-grade if hit)

### 1. CloudFront SPA-Fallback Eats New Routes
**What goes wrong:** Standard CRA-on-CloudFront pattern uses Custom Error Response that rewrites `403/404 → /index.html` so deep links work. When `/10years/foo.css` 404s on S3, CloudFront returns `index.html` (HTTP 200) — browser parses HTML as CSS, page silently breaks. Worse: when Max deploys static assets that *do* exist, behavior ordering can still route the request to the SPA bundle.
**Warning signs:** New page loads but CSS/JS missing; Content-Type for asset is `text/html`; browser console shows "Resource interpreted as Stylesheet but transferred with MIME type text/html."
**Prevention:**
- Asset paths must be carved out as separate CloudFront behavior with higher precedence than SPA fallback (e.g., `/10years/assets/*` → S3 origin, NO error-page rewrite)
- Use S3 prefixes that are clearly partitioned (e.g., `/static-pages/<slug>/...`) + CloudFront behavior per partition
- Custom Error Response should only rewrite 403/404 for `text/html` Accept headers, OR scope to a specific path pattern excluding static-page namespaces
- Add integration test fetching known asset under `/10years/`, asserting Content-Type matches extension
**Phase:** Hosting / Registry phase (CloudFront behavior design). Block roadmap progress until proven.

### 2. Live Registry as Single Point of Failure
**What goes wrong:** SPA fetches `/api/page-registry` on every cold load. If endpoint is down/slow/CORS-broken, *every* page (including `/`, `/software`, login) is bricked because the router can't decide what to render.
**Prevention:**
- Bake fallback registry into SPA bundle at build time (compiled from the same source the live registry serves). Live registry *augments*, doesn't *replace*
- Aggressive client caching: `Cache-Control: max-age=60, stale-while-revalidate=86400`
- Registry endpoint on same origin/CDN as SPA (avoids CORS, isolates from API outages)
- Circuit-breaker: if registry fetch fails twice in a row, fall through to built-in routes only
- Health-check the registry endpoint independently
**Phase:** Registry design phase. Foundational.

### 3. Marketer-Induced Production Outage via Manifest
**What goes wrong:** Max deploys page with `path: "/"` (homepage), `path: "/*"` (catch-all), `path: "/software"` (collision with React-owned screen), or publishes a draft. Registry redirects homepage to broken content. No Jenkins gate = no human review = site down until rollback.
**Prevention:**
- Server-side allowlist of reservable paths. Hardcode reserved-paths list: `/`, `/software`, `/about`, `/account/*`, `/token2/*`, `/scans/*`, `/browser/*`, `/bigorders/*`, `/auth/*`, `/api/*`, `/enrollprivacy`, `/privacypolicy`, `/termsofservice`, `/hardwareterms`, all existing Builder.io paths. Registry API rejects writes that collide.
- Path prefix scoping per role. `site-editor` role can write under explicitly-allocated prefixes only
- Two-state publishing: `draft` (preview URL, not in registry) vs `published`. Default draft.
- Mandatory diff preview in CLI before publish: "Will affect: /10years, /10years/team. Affects 0 existing routes." Explicit confirmation required.
- Audit log of every registry mutation (actor, timestamp, diff). Append-only. Surfaces in Arda admin.
- Rollback button — every publish has `previous_revision` pointer; one CLI command reverts
**Phase:** Registry API + CLI auth phase. This is the milestone's primary risk.

### 4. CSS Bleed / Global Pollution When Injecting Static HTML
**What goes wrong:** `bigscreen10` ships with `body { margin: 0 }`, `* { box-sizing: border-box }`, global resets, jQuery plugins binding to `document`, custom fonts attached to `body`. Injected into SPA tree, nukes the nav header styling, breaks Footer, double-applies resets. Conversely, the SPA's global SASS deforms the microsite.
**Prevention — pick injection model deliberately:**
- **Option A — iframe (`srcdoc` or `src=`):** CSS/JS isolated, no bleed. Cost: cannot share nav/footer naturally (wrap iframe in React layout, or use postMessage to coordinate height)
- **Option B — Shadow DOM:** inject static HTML into shadow root. CSS scoped automatically; JS still global. Cost: forms/links/scroll get weird
- **Option C — Inline HTML + CSS scoping discipline:** require static pages to scope every rule under root container class. Linted by CLI before publish.
- Strip/transform during publish: CLI parses page HTML, rewrites `body`/`html` selectors to scoping root, removes `<html>`/`<head>`/`<body>` tags
- Forbid global JS by default; warn on `window.foo = ...` or `document.body.classList.add(...)`
- Pin nav/footer rendering outside the injection target
**Phase:** Routing/injection design phase. Decision blocks roadmap.

### 5. Asset Path Resolution Under Nested Paths (CRA `homepage`)
**What goes wrong:** `bigscreen10` was built standalone with assets like `<link href="/assets/main.css">` (absolute). Deployed to `/10years/`, those become `https://www.bigscreenvr.com/assets/main.css` — 404. Conversely, building with `homepage: "/10years"` bakes path prefix in, but preview deploys to `/preview/abc123/` then break.
**Prevention:**
- Mandate relative asset paths (`./assets/main.css`). CLI lints for absolute root-relative paths and fails publish
- OR: CLI rewrites paths at publish time — parse HTML/CSS/JS, rewrite `/assets/*` → `<page-base>/assets/*`
- Document `<base href="...">` as canonical solution — set `<base href="/10years/">` in HTML head so all relative URLs resolve against it. Allows preview deploys without rebuild.
- For build-step pages (Vite/Next exports), require `base: '<manifest-path>'` in build config
**Phase:** CLI / publish pipeline phase.

### 6. LLM Agent Misinterprets Manifest / Overwrites Wrong Page
**What goes wrong:** Claude Code skill reads Max's intent loosely. Max says "update the team section" — agent edits a different page's manifest because slugs are similar. Agent infers `path:` field, picks `/team` instead of `/10years/team`, silently overwrites another microsite. Agent invokes `publish --force` to bypass draft-confirmation it doesn't understand.
**Prevention:**
- Manifest path is immutable per page directory. Directory name *is* the slug. Agent cannot "change the path" of a page
- CLI requires typed confirmation token for destructive ops: `Type the exact path you are overwriting: /10years/team`. Agents must reason about literal path
- Dry-run by default for any agent-initiated publish. Agent must explicitly request live publish with a separate command
- Server-side write quota per actor per hour
- CLAUDE.md / skill manifest enumerates forbidden operations with reasons: never publish to `/`, never use `--force`, never edit `reserved-paths.json`, always run `gsd:status` before publish
- Agent-readable diff before publish: list of files changed, URLs affected, byte deltas
- Append-only audit trail distinguishes `actor=max-human` from `actor=max-agent`
**Phase:** CLI design + Claude Code skill phase.

## Moderate Pitfalls

### 7. Race Condition: Registry Updated Before Assets Reachable
Publish pipeline writes registry entry *before* S3 upload + CloudFront invalidation complete. SPA fetches new registry, navigates to page whose CSS/JS is still propagating.
**Prevention:** Atomic publish order — upload assets → wait for S3 PUT confirmation → trigger CloudFront invalidation → poll to "Completed" → THEN write registry entry. Never reverse. Use content-hashed asset paths so new/old coexist. Health-check probe before registry update.
**Phase:** Publish pipeline.

### 8. Stale Clients with Cached Registry Don't See New Pages
Aggressive cache headers mean users with SPA already loaded won't see `/10years` until next reload.
**Prevention:** Stale-while-revalidate on registry response. SPA periodic poll (every N min) or SSE. Document propagation window in CLAUDE.md + CLI output.
**Phase:** Registry + SPA integration.

### 9. CloudFront Invalidation Cost Explosion
AWS charges per path after 1000/month free. Naive "invalidate `/*` on every publish" racks up bills, rate-limits pipeline.
**Prevention:** Invalidate only affected prefix (`/10years/*`). Versioned/hashed asset filenames so invalidation only needed for HTML + registry endpoint. Batch invalidations. Low-TTL CloudFront response policy on registry endpoint specifically (60s).
**Phase:** CloudFront design.

### 10. OAuth Device Flow Token Storage on Disk
CLI stores refresh token in `~/.bigscreen/credentials` world-readable; another process exfiltrates. Token never rotates.
**Prevention:** OS keychain integration (`keytar`, `pass`, macOS Keychain, Windows Credential Manager). Fall back to file with `0600` + warn. Short-lived access tokens (15 min) + rotating refresh tokens. Per-device tokens — each CLI install registers as device. Scope tokens to publish-only. Audit/revoke UI in Arda.
**Phase:** Auth phase.

### 11. Breaking `/enrollprivacy` and Builder.io Coexistence
New dynamic registry adds fallback "if not found, render Builder.io" — but `/enrollprivacy` is *also* in existing hardcoded `builderIoFilter`. Order of evaluation matters; overlapping registry patterns shadow Builder.io paths.
**Prevention:**
- Document + test resolution order: (1) exact-match registry entries → (2) prefix-match (longest-prefix-wins) → (3) hardcoded React-owned routes → (4) `builderIoFilter` → (5) 404
- Registry rejects entries whose path matches any existing Builder.io path (snapshot at registry-API-deploy time)
- E2E smoke test on every registry write: SPA renders each of existing routes, confirms right layout branch + Builder.io fetch
- `getComposedRegex` is order-sensitive (`App.js:114`); new dynamic-routing must NOT introduce new ordering surprises
**Phase:** Routing migration phase. High risk for existing site.

### 12. Scroll Restoration / History Breaks
React Router's `<BrowserRouter>` preserves history state. Static page's own `history.pushState` confuses it. Back button from `/10years/team` triggers two pops, scrolls wrong, or remounts both layouts. Anchor links (`#section`) don't scroll because SPA shell absorbed nav event.
**Prevention:** Forbid `history.pushState` in static pages (lint at publish). Intercept anchor links — convert `<a href="#x">` clicks into `element.scrollIntoView()`. Scroll restoration at layout level. Back-button QA matrix.
**Phase:** Routing integration phase.

### 13. Double-Mount on React Re-render
Static page injected via `dangerouslySetInnerHTML` + embedded `<script>` — React StrictMode (`src/index.js:14`) mounts twice in dev, runs effects twice. Inline scripts execute twice → duplicate event listeners, double-bound analytics. In prod, parent re-render reinjects HTML.
**Prevention:** Inject HTML in `useEffect` with stable deps, not JSX body. Guard with ref. `dangerouslySetInnerHTML` does NOT execute scripts — CLI extracts scripts and emits as external `<script src=...>` OR bans inline scripts. Cleanup function in effect. Document idempotency requirement.
**Phase:** Routing integration phase.

### 14. Manifest Drift / Missing Assets at Publish Time
Manifest declares `entrypoint: "index.html"` but file moved to `dist/index.html` after build. CLI uploads but page 404s.
**Prevention:** Validate manifest in CLI before upload — all declared files exist; all in-page `<link>`/`<script>`/`<img>` references resolve. Schema versioning (`manifestVersion: 1`). Dry-run output lists every file, size, content-type. Post-publish smoke test: HTTP 200, `<title>` matches, OG image 200. If any fails, auto-rollback.
**Phase:** CLI phase.

### 15. Accidentally Publishing Secrets or Drafts
Page directory contains `.env.local` with API keys, `notes.md` with internal pricing, `staging/` subdirectory. CLI tars and ships everything.
**Prevention:** Default-deny upload manifest — only files matching explicit `include:` list (or detected from manifest references). Ignore patterns: `.staticpagesignore` honored + hardcoded denylist (`.env*`, `.git/`, `node_modules/`, `*.key`, `*.pem`, `id_rsa*`, `*.tfstate`). Run `gitleaks` on upload set. Mandatory file-list confirmation for first publish.
**Phase:** CLI phase.

### 16. CSP / SOP Issues With Embedded Third-Party HTML
Bigscreen has no CSP today, but injected `<script src="https://cdn.tailwindcss.com">` or inline scripts will be blocked if/when CSP is added.
**Prevention:** CSP audit gate at publish — CLI scans for external script/style/font/img/connect origins, surfaces in manifest as `externalOrigins: [...]`. Registry allowlist controls permitted origins. Plan CSP rollout: start `Content-Security-Policy-Report-Only`. SRI required for external scripts/styles ≥ size threshold.
**Phase:** Hardening / security phase (post-v1 flag).

### 17. SEO and Analytics Regressions
- Sitemap.xml may not include new static-page URLs
- GA4/pixels load once at SPA boot; navigating to static page never fires pageview (existing SPA route-change pageview bug per CONCERNS.md is amplified)
- Canonical URLs / `<title>` / `<meta>` persist from prior route
- OG/Twitter scrapers don't execute JS — see SPA shell, not rendered static-page meta
**Prevention:** Dynamic sitemap endpoint generates from registry + Builder.io + hardcoded routes (cache 1h). SPA route-change pageview emission. react-helmet-async for title/meta per registry entry. Canonical URL set per page. For OG bots: edge-prerender (Lambda@Edge / CloudFront Function) for known bot UAs, OR accept Builder.io's existing SSR for Builder pages + document static pages publish OG via CDN edge. Pre-publish SEO checklist in CLI.
**Phase:** SEO/analytics phase (mid-roadmap; not v1.0 blocker but tracked).

## Minor Pitfalls

### 18. StrictMode Effect Doubling Confuses Static-Page JS
StrictMode runs effects twice in dev. Inline scripts that aren't idempotent misbehave. Fix: explicit unmount cleanup; document idempotency.

### 19. CLI UX Drift Across Node Versions
Node 22 build errors recent. CLI on Node 22 may behave differently on Max's Node 18. Fix: declare `engines: { node: ">=20" }`; ship CLI as single binary via `pkg`/`bun build --compile`.

### 20. Path Case-Sensitivity Drift
Max publishes `/10Years` once, `/10years` next — two registry entries, half-broken. Fix: lowercase-normalize at write; reject mixed case.

### 21. Trailing-Slash Inconsistency
`/10years` vs `/10years/` resolve differently in some CloudFront configs. Fix: choose canonical (no trailing slash), redirect the other; document.

### 22. Inline Pixels in `public/index.html` Run on Static Pages Too
Marketing pixels (Reddit/Twitter/Meta/AdSense) fire on every SPA route, including `/10years`. Confirm with marketing — if certain pages should suppress, SPA needs conditional based on registry metadata.

### 23. `BrowserRouter` Already Wraps `<App/>` — Static Pages Cannot Wrap Their Own
Nested routers cause undefined behavior. Fix: forbid by lint; document.

### 24. Dead-Code Regex Filter Survival
Existing `builderIoFilter` (`App.js:114-160`) will outlive migration. New dynamic routing must NOT duplicate logic — pick one place that decides "is this a Builder.io page?"

## Phase-Specific Warnings

| Phase Topic | Likely Pitfall | Mitigation |
|---|---|---|
| **Hosting / CloudFront design** | SPA fallback eats new routes (#1); invalidation cost (#9); trailing-slash drift (#21) | Behavior-ordering matrix documented + tested; carve out static-page behaviors with higher precedence than SPA fallback |
| **Registry API design** | SPOF (#2); race conditions on deploy (#7); stale clients (#8); migration coexistence (#11) | Baked-in fallback registry; atomic publish ordering; SWR caching; reserved-path allowlist |
| **CLI + Auth** | Token storage (#10); secrets in publish (#15); manifest drift (#14); LLM misuse (#6) | Keychain integration; default-deny upload; dry-run by default; typed-confirm for destructive ops |
| **Routing integration (SPA)** | CSS bleed (#4); double-mount (#13); history/scroll breaks (#12); asset paths (#5) | Pick injection model (iframe / Shadow DOM / scoped inline); `<base href>` mandate; cleanup hooks |
| **LLM agent / Claude skill** | Wrong path overwrites (#6); `--force` misuse | Skill manifest enumerates forbidden ops; dry-run default; per-actor audit + quota |
| **SEO / analytics** | Sitemap drift, no pageviews, OG scrapers (#17) | Dynamic sitemap endpoint; route-change analytics emission; helmet for meta; edge-prerender for bots |
| **Migration cutover** | Breaking `/enrollprivacy` and other Builder.io paths (#11) | Resolution-order matrix; existing-path snapshot reservation; full E2E smoke test on every registry write |
| **Security hardening (post-v1)** | CSP violations (#16); pixels firing on wrong pages (#22) | CSP report-only rollout; per-page pixel control in registry metadata |

## Roadmap Implications

Five **critical** pitfalls (#1–#6) each warrant their own design-doc-grade decision before implementation. Front-load them as gates:

1. **CloudFront behavior ordering** — design + test before any deploy pipeline work
2. **Registry SPOF strategy** — bake-in fallback + caching policy decided before API design
3. **Reserved-path + role-scoping model** — decided before CLI auth wires up
4. **Injection model (iframe vs Shadow DOM vs scoped inline)** — decided before SPA dynamic-router work
5. **Asset-path resolution strategy** — decided before first publish of `/10years`
6. **Agent guardrails** — Claude Code skill written *after* CLI has dry-run + typed-confirm

**Migration risk (#11)** — breaking existing `builderIoFilter` routes — is biggest "can't undo" risk. Dedicated E2E test harness as roadmap deliverable, not afterthought.

**Pre-existing SPA bugs** (no route-change analytics, no error reporting, `BrowserRouter` already mounted, StrictMode effect doubling, hardcoded `BUILDER_IO_API_KEY`) become amplifiers of new pitfalls. Add error monitor *before* live registry rollout so issues are visible.

## Open Questions for Downstream Phases

- Confirm CloudFront + S3 hosting (pitfalls #1, #7, #9 assume this)
- Which API owns registry endpoint (`cloud-api` vs `admin-api` vs new) determines auth boundary + SPOF blast radius
- Existing CSP posture (likely none, verify edge config)
- Whether Builder.io content has its own CDN edge (separate failure domain) or hits Builder.io's origin live

---

*Pitfalls research — 2026-05-21*
