# personal-site ("The Construct") — Technical Architecture Report

> Skill: codebase-report · Repo: personal-site · Date: 2026-07-01

A durable, zero-prior-context handoff document for the CODE surface of Boden Fuller's personal site (bodenfuller.com). Every claim below carries a `file:line` citation. The site lives inside a large non-code career/knowledge markdown workspace (`career/`, `.agents/`, `docs/`, root canon docs) — that content layer is architecture context only and is **not** covered here.

---

## Executive Summary

**personal-site** is a Next.js 16 App Router marketing / proof / writing site with an MDX blog, a strict per-request CSP, and a bespoke "phosphor-terminal / drawing-set" design system called **The Construct**. It renders dynamically (not a static export), single-sources its thesis copy, and deploys to Vercel on push to `main`.

**Key Statistics:**
- ~8,200 lines of code across `src/` + `scripts/` + `e2e/` + `middleware.ts` (`.ts`/`.tsx`/`.mjs`; `wc -l` total 8,241)
- Language: TypeScript 5.9 (strict, `noUnusedLocals`/`noUnusedParameters` — `tsconfig.json:11,43-44`), React 19.2, Node 24.x (`package.json:74`)
- Framework: Next.js 16.1 App Router with `typedRoutes: true` (`next.config.ts:10`) and Turbopack (`next.config.ts:5`)
- Content pipeline: MDX via `next-mdx-remote-client` 2.1 + gray-matter frontmatter; 25 essays in `src/content/writing/*.mdx`
- Design system: 3 Google fonts (JetBrains Mono / Fraunces / Spectral) wired to CSS vars, styled via React inline `style={{}}` attributes (`src/components/construct/tokens.ts`)
- Key libs: `next`, `react`, `next-mdx-remote-client`, `gray-matter`, `@vercel/analytics` + `@vercel/speed-insights`, `remark-gfm`/`rehype-highlight`/`rehype-slug` (`package.json:29-48`)

**Scout-map corrections (verified against the tree):**
- The scout map claimed "NO GitHub Actions (.github/workflows is empty)." **False** — `.github/workflows/ci.yml` exists and runs a 3-job pipeline (`validate` → `e2e` + `vibe-check`) on push/PR to `main` (`.github/workflows/ci.yml:13-105`). CI is a real enforcement layer, not just Vercel.
- `SITE_URL` default is `https://www.bodenfuller.com` (`src/lib/constants.ts:1`), not the bare apex.

---

## Entry Points

| Entry | Location | Purpose |
|-------|----------|---------|
| Root layout | `src/app/layout.tsx:98-128` | Wires 3 fonts into `--font-*` CSS vars (`layout.tsx:20-41`), sets site metadata/OG/Twitter (`layout.tsx:43-96`), renders `SiteBackground` + skip link; `await connection()` forces dynamic rendering site-wide (`layout.tsx:103`) |
| Middleware (per-request) | `middleware.ts:4-24` | Generates a base64 nonce (`middleware.ts:5`), builds the CSP (`middleware.ts:6-9`), sets it + `x-nonce` on request and response headers; matcher excludes `api`/`_next/static`/`_next/image`/`favicon` (`middleware.ts:26-36`) |
| Home route | `src/app/page.tsx:30-40` + `src/app/HomeClient.tsx:1` | Server page injects home JSON-LD inline (`page.tsx:33-36`); `HomeClient` (`'use client'`) renders hero + two-door fork |
| MDX post route | `src/app/writing/[slug]/page.tsx:49-125` | `generateStaticParams` from `getAllPosts` (`page.tsx:21-23`); renders `MDXRemote` with Construct styling + BlogPosting/Breadcrumb JSON-LD (`page.tsx:57-67`) |
| CSP source of truth | `src/lib/constants.ts:10-46` | `buildContentSecurityPolicy()` — the single place the load-bearing `style-src` decision lives |
| Blog content pipeline | `src/lib/blog.ts:31-114` | Reads `src/content/writing/*.mdx` via gray-matter, drops drafts in prod, sorts by date; feeds pages, `/api/posts`, sitemap, `/llms.txt` |
| Sitemap generator | `src/app/sitemap.ts:43-141` | Shells `git log` for `lastModified` with mtime fallback (`sitemap.ts:23-41`); `SITEMAP_SOURCE_PATHS` is a hardcoded route list (`sitemap.ts:8-17`) |
| `/llms.txt` route handler | `src/app/llms.txt/route.ts:11-55` | Emits an llms.txt from posts + internal builds + key pages (a top-level route, not under `/api`) |

**API routes:**

| Route | Location | Behavior |
|-------|----------|----------|
| `GET /api/health` | `src/app/api/health/route.ts:9-15` | `force-dynamic` (`route.ts:3`), returns `{status:'healthy'}` with `no-store`; used by docker healthcheck |
| `GET /api/posts` | `src/app/api/posts/route.ts:4-10` | Returns `{count, posts[]}` metadata (strips MDX `content`, `route.ts:5`); `public max-age=3600 s-maxage=86400` |
| `GET /api/posts/:slug` | `src/app/api/posts/[slug]/route.ts:4-21` | Single post via `getPostBySlug` (async `params`, `route.ts:8`); 404 `no-store` on miss |

---

## Key Types

| Type | Location | Purpose |
|------|----------|---------|
| `BlogPost` | `src/lib/types.ts:6-14` | Core rendered domain object — `slug/title/description/date/tags/content/readingTime`; the currency of the whole content pipeline |
| `BlogPostFrontmatter` | `src/lib/types.ts:16-22` | Shape gray-matter parses out of each `.mdx` front block; `draft?` drives prod filtering |
| `Build` | `src/data/builds.ts:1-8` | Typed registry entry for a project/system; `external` flag drives `/llms.txt` internal-only filtering (`llms.txt/route.ts:13`) |
| CSS-var style tokens (`MONO`/`DISPLAY`/`READ`/`TICK`) | `src/components/construct/tokens.ts:9-24` | `CSSProperties` inline-style tokens reading from the `--font-*` vars set in `layout.tsx`; the atoms of The Construct design system |
| `ContentSecurityPolicyOptions` | `src/lib/constants.ts:5-8` | `{nonce, isDev?}` input to the CSP builder — the seam between middleware and policy |
| `CaseStudy` / `AgentOpsMetrics` | `src/lib/types.ts:25-63` | Case-study + metrics shapes for `/work` subpages (several fields currently aspirational/unused by live data) |

---

## Data Flow

```
                     ┌─────────────────────────────────────────────┐
  HTTP request  ───► │ middleware.ts  (runs on every non-asset req) │
                     │  • nonce = base64(randomUUID)   :5           │
                     │  • buildContentSecurityPolicy() :6           │
                     │  • set x-nonce + CSP on req+res headers      │
                     └───────────────┬─────────────────────────────┘
                                     ▼
                     ┌─────────────────────────────────────────────┐
   layout.tsx  ────► │ RootLayout  await connection() → dynamic     │
                     │  fonts → --font-mono/display/sans   :20-41   │
                     │  <SiteBackground/> + skip link + metadata    │
                     └───────────────┬─────────────────────────────┘
                                     ▼
        ┌────────────────────────────┴───────────────────────────┐
        ▼                                                         ▼
 ┌──────────────┐                                    ┌─────────────────────────┐
 │ page.tsx     │  static/registry routes            │ writing/[slug]/page.tsx │
 │ HomeClient   │  ← src/data/builds.ts              │ getPostBySlug(slug)     │
 │ (two-door)   │                                    └───────────┬─────────────┘
 └──────┬───────┘                                                ▼
        │                                        ┌───────────────────────────────┐
        │                                        │ src/lib/blog.ts                │
        │                                        │  fs.readFile → gray-matter     │
        │                                        │  SAFE_SLUG/FILENAME guards :8-9│
        │                                        │  drop draft if NODE_ENV=prod   │
        │                                        └───────────────┬───────────────┘
        │                                                        ▼
        │                                        ┌───────────────────────────────┐
        │                                        │ MDXRemote (remark-gfm,         │
        │                                        │  rehype-highlight, rehype-slug)│
        │                                        │  + Construct inline styles     │
        │                                        └───────────────┬───────────────┘
        ▼                                                        ▼
 ┌────────────────────────────────────────────────────────────────────────────┐
 │ Rendered HTML.  JSON-LD scripts MUST carry the nonce:                        │
 │   JsonLdScript reads x-nonce via headers()  (JsonLdScript.tsx:12)           │
 │   → passes to next/script so it passes the strict script-src                 │
 └────────────────────────────────────────────────────────────────────────────┘

 Same blog.ts feeds, non-page consumers:
   getAllPosts() ──► /api/posts (strip content)   api/posts/route.ts:5
                └──► /llms.txt (posts+builds)      llms.txt/route.ts:11-14
                └──► sitemap.ts writingPages        sitemap.ts:132-138
```

**Happy path (blog post):** Request hits `middleware.ts` → nonce minted and CSP set on headers (`middleware.ts:5-13`) → `RootLayout` opts into dynamic rendering and mounts fonts/background (`layout.tsx:103-119`) → `writing/[slug]/page.tsx` calls `getPostBySlug(slug)` (`page.tsx:51`), which validates the slug against `SAFE_SLUG_PATTERN` and drops drafts in prod (`blog.ts:76-89`) → `MDXRemote` renders the markdown with gfm/highlight/slug plugins and Construct inline styles (`page.tsx:96-106`) → `JsonLdScript` reads `x-nonce` and emits nonce-authorized JSON-LD (`JsonLdScript.tsx:12-18`) → HTML returned.

---

## External Dependencies

| Dependency | Purpose | Critical? |
|------------|---------|-----------|
| `next` 16.1 (`package.json:37`) | App Router, RSC, middleware, routing, image opt, redirects/headers | Yes |
| `react` / `react-dom` 19.2 (`package.json:39-40`) | Rendering; `'use client'` islands (HomeClient, Construct primitives) | Yes |
| `next-mdx-remote-client` 2.1 (`package.json:38`) | RSC MDX compile+render for blog posts (`writing/[slug]/page.tsx:96`) | Yes |
| `gray-matter` (`package.json:35`) | Frontmatter parsing in `blog.ts:48,82` | Yes |
| `remark-gfm` / `rehype-highlight` / `rehype-slug` (`package.json:41-43`) | MDX transform pipeline (`writing/[slug]/page.tsx:100-102`) | Yes (rendering fidelity) |
| `tailwindcss` v4 + `@tailwindcss/postcss` (`package.json:30,45`) | Layout/utility CSS (inline styles carry the Construct look) | Yes |
| `next/font/google` (JetBrains Mono / Fraunces / Spectral) (`layout.tsx:2`) | The 3-font typographic system → `--font-*` vars | Yes |
| `@vercel/analytics` + `@vercel/speed-insights` (`layout.tsx:3-4`, `package.json:32-33`) | Telemetry widgets in root layout | No |
| `git` CLI (via `spawnSync`) (`sitemap.ts:25`) | Sitemap `lastModified` timestamps; degrades to file mtime | No (graceful fallback) |
| `lucide-react`, `clsx`, `tailwind-merge` (`package.json:34,44,46`) | Icons + `cn()` class merge (`utils.ts:8-10`) | No |
| **No database / no external API / no auth backend** | Site is a static-content + registry app; `AdminCredentials`/`AdminSession` types (`types.ts:104-112`) are unused scaffolding | — |

---

## Configuration

| Source | Location / Example | Precedence |
|--------|--------------------|------------|
| Env var `NEXT_PUBLIC_SITE_URL` | `src/lib/constants.ts:1` → falls back to `https://www.bodenfuller.com` | 1 (highest; overrides default) |
| Env var `NODE_ENV` | Gates CSP dev branch (`constants.ts:12,24-25,41`) + draft filtering (`blog.ts:52,87`) | 1 (behavioral switch) |
| Env var `CI` | Playwright retries/workers/forbidOnly (`playwright.config.ts:38-40`) | 1 (test-time) |
| `next.config.ts` | `typedRoutes`, redirects, security headers, image `remotePatterns` (`next.config.ts:3-100`) | 2 (build/runtime framework config) |
| `tsconfig.json` | strict mode, `@/*` path aliases, excludes `ag-*` (`tsconfig.json:25-41,54-57`) | 2 (compile) |
| `vitest.config.ts` | 3 test projects + duplicated `@/*` aliases + coverage thresholds (`vitest.config.ts:4-61`) | 2 (test) |
| Hardcoded thesis copy | `src/lib/mission.ts:5-8` — `HERO_LEAD`/`MISSION_LINE`/`SITE_DESCRIPTION` single-sourced | 3 (content defaults) |
| Hardcoded route list | `src/app/sitemap.ts:8-17` `SITEMAP_SOURCE_PATHS` — must be hand-synced when routes are added | 3 (must-maintain) |

**Key config notes:**
- CSP `script-src` is strict (`'self'` + `'nonce-…'` + `'strict-dynamic'`, plus `'unsafe-eval'` only in dev) — `constants.ts:14,24-25`.
- CSP `style-src` is `'self' 'unsafe-inline'` **on purpose** — `constants.ts:22` (see Gotchas). This is not a hardening gap.
- Static security headers (HSTS, `X-Frame-Options: DENY`, COOP/CORP, Permissions-Policy) are set framework-side in `next.config.ts:57-97`, separate from the per-request CSP.
- `@/*` path aliases are declared **twice** — `tsconfig.json:25-41` and `vitest.config.ts:4-12` — and must be kept in sync.

---

## Module Structure

```
personal-site/
├── middleware.ts                 # per-request nonce + CSP
├── next.config.ts                # typedRoutes, redirects, headers, images
├── src/
│   ├── app/                      # App Router
│   │   ├── layout.tsx            # root: fonts, metadata, connection() → dynamic
│   │   ├── page.tsx / HomeClient # home (server shell + client island)
│   │   ├── {about,method,training,ai-partner,first-win}/
│   │   ├── work/                 # + 5 case-study subpages + 12-factor-agentops
│   │   ├── writing/[slug]/       # dynamic MDX post route
│   │   ├── api/{health,posts,posts/[slug]}/route.ts
│   │   ├── llms.txt/route.ts     # top-level route handler (not /api)
│   │   ├── sitemap.ts, opengraph-image.tsx, icon/apple-icon, not-found
│   ├── components/
│   │   ├── construct/            # tokens.ts, primitives.tsx, case.tsx (design system)
│   │   ├── blog/MDXComponents.tsx
│   │   ├── Header/Footer/PageLayout/SiteBackground/JsonLdScript/ConstructIcon
│   ├── lib/                      # blog.ts, jsonld.ts, mission.ts, constants.ts,
│   │                             #   utils.ts, types.ts, theme.ts, proof-og.tsx
│   ├── data/builds.ts            # typed project/system registry
│   └── content/writing/*.mdx     # 25 essays (code-adjacent content)
├── scripts/                      # ~12 build/content tools (.mjs/.py/.sh)
├── e2e/                          # 5 Playwright specs
└── career/ .agents/ docs/        # CONTENT layer — ~728 md files, NOT audited as code
```

Most `src/lib/*` and shared components carry a colocated `*.test.ts(x)` (e.g. `Header.test.tsx`, `PageLayout.test.tsx`, `Footer.test.tsx`).

---

## Test Infrastructure

| Type | Location | Count / Runner |
|------|----------|----------------|
| Unit (node) | `src/**/*.test.ts` (colocated) | 15 files; Vitest `node` project (`vitest.config.ts:31-38`) |
| Component (jsdom) | `src/**/*.test.tsx` | 3 files; Vitest `jsdom` project (`vitest.config.ts:39-47`) |
| Script tests | `scripts/**/*.test.mjs` | 2 files (`x-atomize.test.mjs`, `x-draft-lint.test.mjs`); Vitest `scripts` project (`vitest.config.ts:48-57`) |
| E2E | `e2e/*.spec.ts` | 5 specs (smoke, navigation, accessibility+axe, ai-partner, work-subpages); Playwright chromium-only (`playwright.config.ts:80-84`) |

**Coverage thresholds are deliberately low:** statements 14 / branches 12 / functions 13 / lines 14 (`vitest.config.ts:28`) — the suite guards the content pipeline + config seams, not blanket coverage.

**Running tests:**
```bash
npm test                 # Vitest, all 3 projects (package.json:14)
npm run test:coverage    # with v8 coverage + thresholds
npm run test:e2e         # Playwright; webServer boots `npm run dev` on :3000 (playwright.config.ts:107-114)
npm run validate         # type-check → lint → build (package.json:22)
npm run type-check       # typegen-clean.mjs → next typegen → tsc --noEmit (package.json:21)
npm run knip             # dead-code detection
npm run slop-lint        # prose slop lint (scripts/slop-lint.mjs)
```

**Enforcement layers:**
- **CI (`.github/workflows/ci.yml`):** `validate` job = type-check → lint → `npm test --run` → build (`ci.yml:13-39`); then `e2e` (chromium, `ci.yml:41-72`) and `vibe-check` (`ci.yml:74-105`), both gated on `validate`. Runs on push + PR to `main` with concurrency cancellation (`ci.yml:9-11`). *(Note: CI uses node 20 — `ci.yml:23` — while `package.json:74` pins engines to 24.x; a version drift worth reconciling.)*
- **Pre-commit / pre-push (`.pre-commit-config.yaml`):** trailing-whitespace/EOF/yaml/json, gitleaks (pre-push), eslint (staged), tsc (pre-push); "pre-commit hooks are helpers, CI is the enforcer" (`.pre-commit-config.yaml:6`). Husky wires it (`package.json:26`).
- **Deploy:** push to `main` → Vercel auto-deploy. `Dockerfile`/`docker-compose.yml` exist for a self-hosted prod-like container (health-checked via `/api/health`) but are **not** the canonical deploy.

---

## Notes & Gotchas

1. **CSP `style-src` uses `'unsafe-inline'` ON PURPOSE — do not "harden" it.** The entire Construct design system styles via React inline `style={{}}` attributes (`tokens.ts`, `primitives.tsx`, `writing/[slug]/page.tsx:72-93`). CSP nonces authorize `<style>` **elements**, never style **attributes** — so a nonce-only `style-src` silently strips ALL styling in production (a documented prior prod bug). The rationale is inlined at `constants.ts:15-22`. Adding a style nonce alongside `'unsafe-inline'` makes some browsers ignore the `'unsafe-inline'`.

2. **JSON-LD scripts MUST carry the nonce.** Use `JsonLdScript` (reads `x-nonce` via `headers()`, `JsonLdScript.tsx:12`) or the inline `dangerouslySetInnerHTML` pattern used in `page.tsx:33-36`. A raw `<script>` without the nonce is blocked by the strict `script-src` in prod.

3. **CSP differs by env; verify against a real prod build.** The dev branch adds `'unsafe-eval'` and drops `upgrade-insecure-requests` (`constants.ts:24-25,41-43`). `next dev`'s relaxed CSP hides breakage — validate visual/outward-facing changes with `next build && next start`, not `next dev`.

4. **Site renders dynamically, not as a static export.** `layout.tsx:103` calls `await connection()`, forcing dynamic rendering site-wide; `/api/health` is additionally `force-dynamic` (`api/health/route.ts:3`).

5. **`typedRoutes` is ON (`next.config.ts:10`).** Internal `Link` hrefs are compile-time validated against the `Route` type (see `primitives.tsx:6` importing `type { Route }`). Renaming a route requires updating all typed links AND the redirects block (`next.config.ts:33-50`).

6. **`sitemap.ts` route list is hand-maintained.** `SITEMAP_SOURCE_PATHS` (`sitemap.ts:8-17`) and the `staticPages` array (`sitemap.ts:50-129`) must be updated whenever a route is added — `first-win` was recently added there (`sitemap.ts:16,123-128`).

7. **Slug inputs are regex-guarded against path traversal.** `blog.ts:8-9` (`SAFE_SLUG_PATTERN`/`SAFE_FILENAME_PATTERN`) gate every filesystem read in `resolveBlogFilePath`/`resolveBlogSlugPath` (`blog.ts:11-25`).

8. **Dockerfile is STALE.** Header says "Next.js 15" and it uses `node:20-alpine` (`Dockerfile:1,5`), but `package.json` pins Next 16 and engines node 24.x. Canonical deploy is Vercel.

9. **Node version drift.** `package.json:74` requires node 24.x; CI (`ci.yml:23,50,85`) and the Dockerfile use node 20. Reconcile before relying on 24-only behavior.

10. **`@/*` aliases are duplicated** in `tsconfig.json:25-41` and `vitest.config.ts:4-12` — keep them in sync. `tsconfig.json:56` excludes `ag-*` (beads/Dolt artifact dirs) from the TS project.

11. **Content vs code boundary.** `src/content/writing/*.mdx` IS code-adjacent (rendered). `career/`, `.agents/`, `docs/`, and root canon docs (`SOUL.md`/`CODEX.md`/`AGENTS.md`/`USER.md`/etc.) are a ~728-file knowledge workspace — architecture context, not an audit target.

12. **In-flight/uncommitted work (git status at analysis time):** new `src/app/first-win/` route, new `outputs/` dir, and modifications to `ai-partner/page.tsx`, `sitemap.ts`, `construct/primitives.tsx`. The prior "known CSP bug" is already resolved in `constants.ts`.

---

*Generated: 2026-07-01 · Skill: codebase-report (STANDARD mode) · Scope: CODE surface only.*
