Skip to Content
How it was builtImplementation plansDocumentation Site Implementation Plan

Documentation Site Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Ship https://docs.antmejia.com: a Nextra 4 docs site (own Vercel project) with six hand-written engineering pages and a generated “How it was built” section built from docs/superpowers/.

Architecture: docs-site/ is a standalone Next 15 app (not a workspace) using nextra + nextra-theme-docs. A prebuild script copies the repo’s specs and plans into docs-site/content/how-it-was-built/ as plain Markdown. A separate Vercel project (Root Directory docs-site, production branch production) builds it only when docs paths changed.

Tech Stack: Next 15.5.9, React 19.1.1, Nextra 4.6.1 + nextra-theme-docs 4.6.1, Pagefind 1.5.2 (search), TypeScript 5.9.2, node:test for script tests, Yarn Classic with exact pins.

Spec: docs/superpowers/specs/2026-10-06-docs-site-design.md

Global Constraints

  • Exact-pinned dependencies (save-exact); docs-site has its own yarn.lock. zod must be pinned to 4.1.12: a throwaway build proved that zod 4.6.5 (what a fresh install resolves) makes every page 500 with Invalid input: expected nonoptional, received undefined → at children from nextra-theme-docs’s Layout. With 4.1.12 every page, including the real spec and plan, renders. Do not let Renovate bump it (Task 6).
  • Nothing invented: every claim in a docs page must be checkable against this repo’s code, README, PRODUCT.md or specs. Quote real file paths and commands.
  • No secrets in the docs, and no mention of values from .env files.
  • Conventional commits ending with Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>; PR bodies end with 🤖 Generated with [Claude Code](https://claude.com/claude-code).
  • Do not use git add -A or git add .: stage named paths only (a stray local .env and project.inlang/ editor files must never be committed).
  • Production branch for the docs project is production; main pushes must not deploy docs.
  • CI must pass: yarn type-check, yarn lint, yarn format:ci, yarn build, story tests, commitlint, plus the new docs build.

File Structure

FileResponsibility
docs-site/package.json, yarn.lockDependencies (exact), scripts.
docs-site/next.config.mjsNextra with .md as plain Markdown.
docs-site/tsconfig.json, mdx-components.tsxTS config, theme MDX components.
docs-site/app/layout.tsx, app/[[...mdxPath]]/page.tsxTheme layout and the catch-all page.
docs-site/content/**Hand-written pages and _meta.js ordering.
docs-site/scripts/sync-process-docs.mjs (+ .test.mjs)Copies specs/plans into generated content.
docs-site/scripts/vercel-ignore.sh (+ vercel-ignore.test.mjs), vercel.jsonDocs-only build trigger.
.github/workflows/ci.ymlBuilds and tests the docs.
tsconfig.json, .prettierignore, .gitignore, renovate.jsonRoot configs that must know about docs-site/.
src/containers/Footer/Footer.tsx, redirects.ts, PRODUCT.md, README.mdLink the docs and fix stale statements.

Task 1: Scaffold docs-site/ and prove it builds

Files: Create everything under docs-site/ listed below; Modify tsconfig.json, .prettierignore, .gitignore.

Interfaces: Produces the docs-site scripts dev, sync, prebuild, build, postbuild (Pagefind), test.

  • Step 1: Create docs-site/package.json
{ "name": "operation-looking-glass-docs", "private": true, "scripts": { "sync": "node scripts/sync-process-docs.mjs", "predev": "yarn sync", "dev": "next dev", "prebuild": "yarn sync", "build": "next build", "postbuild": "pagefind --site .next/server/app --output-path public/_pagefind", "start": "next start", "test": "node --test scripts/" } }
  • Step 2: Install exact versions (run in docs-site/, Node from .nvmrc)
cd docs-site yarn add --exact next@15.5.9 react@19.1.1 react-dom@19.1.1 nextra@4.6.1 nextra-theme-docs@4.6.1 zod@4.1.12 yarn add --dev --exact pagefind@1.5.2 typescript@5.9.2 @types/react @types/node

Confirm node -e 'console.log(require("zod/package.json").version)' prints 4.1.12. If @types/react/@types/node resolve to ranges, pin them to the resolved versions in package.json.

  • Step 3: Create docs-site/next.config.mjs
import nextra from "nextra"; // "detect": .md files compile as plain Markdown (no JSX or {expressions}), .mdx files as MDX. The generated // "How it was built" pages are .md, so raw "<" or "{" in a spec can never break the docs build. const withNextra = nextra({ mdxOptions: { format: "detect" } }); export default withNextra({ reactStrictMode: true });
  • Step 4: Create docs-site/mdx-components.tsx
import { useMDXComponents as getDocsMDXComponents } from "nextra-theme-docs"; export const useMDXComponents = (components?: Record<string, unknown>) => ({ ...getDocsMDXComponents(), ...components, });
  • Step 5: Create docs-site/app/layout.tsx
import { Footer, Layout, Navbar } from "nextra-theme-docs"; import { Head } from "nextra/components"; import { getPageMap } from "nextra/page-map"; import "nextra-theme-docs/style.css"; export const metadata = { title: { default: "Docs | Anthony Mejia", template: "%s | Docs" } }; export default async function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" dir="ltr" suppressHydrationWarning> <Head> <meta name="theme-color" content="#111827" /> </Head> <body> <Layout navbar={<Navbar logo={<b>Docs</b>} projectLink="https://github.com/ant-mejia/operation-looking-glass" />} pageMap={await getPageMap()} docsRepositoryBase="https://github.com/ant-mejia/operation-looking-glass/tree/main/docs-site" footer={<Footer>Anthony Mejia · antmejia.com</Footer>} > {children} </Layout> </body> </html> ); }
  • Step 6: Create docs-site/app/[[...mdxPath]]/page.tsx
import { generateStaticParamsFor, importPage } from "nextra/pages"; import { useMDXComponents as getMDXComponents } from "../../mdx-components"; export const generateStaticParams = generateStaticParamsFor("mdxPath"); type Props = { params: Promise<{ mdxPath?: string[] }> }; export async function generateMetadata(props: Props) { const params = await props.params; const { metadata } = await importPage(params.mdxPath); return metadata; } // The theme's wrapper component (table of contents, breadcrumbs, edit link). // eslint-disable-next-line @typescript-eslint/no-explicit-any const Wrapper = getMDXComponents().wrapper as any; export default async function Page(props: Props) { const params = await props.params; const { default: MDXContent, toc, metadata, sourceCode } = await importPage(params.mdxPath); return ( <Wrapper toc={toc} metadata={metadata} sourceCode={sourceCode}> <MDXContent {...props} params={params} /> </Wrapper> ); }
  • Step 7: Create docs-site/tsconfig.json and a starter docs-site/content/index.mdx
{ "compilerOptions": { "target": "ES2022", "lib": ["dom", "esnext"], "jsx": "preserve", "module": "esnext", "moduleResolution": "bundler", "strict": true, "skipLibCheck": true, "noEmit": true, "esModuleInterop": true, "resolveJsonModule": true, "isolatedModules": true, "incremental": true, "allowJs": true, "plugins": [{ "name": "next" }] }, "exclude": ["node_modules"], "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"] }
# Documentation How the portfolio at antmejia.com is built, and how it was specified and planned.

Also create an empty docs-site/scripts/sync-process-docs.mjs stub that exits 0 (process.exit(0)) so prebuild runs; Task 2 replaces it.

  • Step 8: Update root configs

  • tsconfig.json: change "exclude": ["node_modules", "storybook-static"] to ["node_modules", "storybook-static", "docs-site"].

  • .gitignore: append docs-site/.next/, docs-site/node_modules/, docs-site/public/_pagefind/, docs-site/next-env.d.ts, docs-site/content/how-it-was-built/, docs-site/*.tsbuildinfo.

  • .prettierignore: append docs-site/.next, docs-site/public/_pagefind, docs-site/content/how-it-was-built.

  • Step 9: Verify

cd docs-site && yarn build ls public/_pagefind | head -3 (yarn start -p 3400 &) ; sleep 5; curl -s -o /dev/null -w "%{http_code}\n" localhost:3400/ cd .. && yarn type-check && yarn lint && yarn format:ci

Expected: build succeeds, Pagefind indexes pages, 200, root checks pass (the root type-check must not pick up docs-site). Stop the server by PID found with ss -ltnp | grep :3400 (never pkill -f).

  • Step 10: Commit (stage named paths): git add docs-site tsconfig.json .gitignore .prettierignore then git commit -m "feat: scaffold the Nextra docs site".

Task 2: Process-docs sync script (TDD)

Files: Create docs-site/scripts/sync-process-docs.mjs (replacing the stub) and docs-site/scripts/sync-process-docs.test.mjs.

Interfaces:

  • Produces parseDocName(filename: string): { date: string; slug: string } | null for names like 2026-10-06-resume-page-design.md.

  • Produces titleOf(markdown: string, fallback: string): string (first # heading).

  • Produces buildProcessDocs({ sourceRoot, outRoot }): { specs: Entry[]; plans: Entry[] } where Entry = { date: string; slug: string; title: string }; writes <outRoot>/specs/<slug>.md, <outRoot>/plans/<slug>.md, a _meta.js in each folder (newest first, titles as labels), <outRoot>/_meta.js, and <outRoot>/index.md.

  • Step 1: Write the failing tests (node:test)

import assert from "node:assert/strict"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { buildProcessDocs, parseDocName, titleOf } from "./sync-process-docs.mjs"; test("parseDocName splits date and slug", () => { assert.deepEqual(parseDocName("2026-10-06-resume-page-design.md"), { date: "2026-10-06", slug: "resume-page-design", }); assert.equal(parseDocName("README.md"), null); }); test("titleOf uses the first H1, else the fallback", () => { assert.equal(titleOf("intro\n# Real Title\n## Sub", "fb"), "Real Title"); assert.equal(titleOf("no heading", "fb"), "fb"); }); const setup = () => { const src = mkdtempSync(path.join(tmpdir(), "src-")); const out = mkdtempSync(path.join(tmpdir(), "out-")); for (const kind of ["specs", "plans"]) mkdirSync(path.join(src, kind), { recursive: true }); writeFileSync(path.join(src, "specs", "2026-10-06-b-design.md"), "# B design\n\nuses <script> and {braces}\n"); writeFileSync(path.join(src, "specs", "2026-09-01-a-design.md"), "# A design\n"); writeFileSync(path.join(src, "plans", "2026-10-06-b.md"), "# B plan\n- [ ] step\n"); writeFileSync(path.join(src, "specs", "notes.txt"), "ignored"); return { src, out }; }; test("copies specs and plans as .md, newest first, and ignores other files", () => { const { src, out } = setup(); const result = buildProcessDocs({ sourceRoot: src, outRoot: out }); assert.deepEqual( result.specs.map((e) => e.slug), ["b-design", "a-design"] ); assert.ok(existsSync(path.join(out, "specs", "b-design.md"))); assert.ok(existsSync(path.join(out, "plans", "b.md"))); assert.ok(!existsSync(path.join(out, "specs", "notes.md"))); assert.match(readFileSync(path.join(out, "specs", "b-design.md"), "utf8"), /<script> and \{braces\}/); }); test("writes _meta.js with titles and an index that lists every page", () => { const { src, out } = setup(); buildProcessDocs({ sourceRoot: src, outRoot: out }); const meta = readFileSync(path.join(out, "specs", "_meta.js"), "utf8"); assert.ok(meta.indexOf("b-design") < meta.indexOf("a-design")); assert.match(meta, /"B design"/); const index = readFileSync(path.join(out, "index.md"), "utf8"); assert.match(index, /\[B design\]\(\/how-it-was-built\/specs\/b-design\)/); assert.match(index, /\[B plan\]\(\/how-it-was-built\/plans\/b\)/); }); test("is idempotent and clears stale generated files", () => { const { src, out } = setup(); buildProcessDocs({ sourceRoot: src, outRoot: out }); writeFileSync(path.join(out, "specs", "stale.md"), "# old"); buildProcessDocs({ sourceRoot: src, outRoot: out }); assert.ok(!existsSync(path.join(out, "specs", "stale.md"))); });
  • Step 2: Run to confirm failure. cd docs-site && yarn test → fails (exports missing).

  • Step 3: Implement sync-process-docs.mjs

import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const NAME = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/; export const parseDocName = (filename) => { const match = NAME.exec(filename); return match ? { date: match[1], slug: match[2] } : null; }; export const titleOf = (markdown, fallback) => /^# (.+)$/m.exec(markdown)?.[1].trim() ?? fallback; const KINDS = [ { dir: "specs", label: "Design specs" }, { dir: "plans", label: "Implementation plans" }, ]; const metaFile = (entries) => `export default ${JSON.stringify(Object.fromEntries(entries.map((e) => [e.slug, e.title])), null, 2)};\n`; export const buildProcessDocs = ({ sourceRoot, outRoot }) => { rmSync(outRoot, { recursive: true, force: true }); const result = {}; for (const { dir } of KINDS) { const from = path.join(sourceRoot, dir); const to = path.join(outRoot, dir); mkdirSync(to, { recursive: true }); const entries = []; for (const name of readdirSync(from)) { const parsed = parseDocName(name); if (!parsed) continue; const body = readFileSync(path.join(from, name), "utf8"); const title = titleOf(body, parsed.slug); writeFileSync(path.join(to, `${parsed.slug}.md`), body); entries.push({ ...parsed, title }); } entries.sort((a, b) => b.date.localeCompare(a.date) || a.slug.localeCompare(b.slug)); writeFileSync(path.join(to, "_meta.js"), metaFile(entries)); result[dir] = entries; } writeFileSync( path.join(outRoot, "_meta.js"), `export default ${JSON.stringify({ index: "Overview", specs: "Design specs", plans: "Implementation plans" }, null, 2)};\n` ); const list = (dir, entries) => entries.map((e) => `- [${e.title}](/how-it-was-built/${dir}/${e.slug}) (${e.date})`).join("\n"); writeFileSync( path.join(outRoot, "index.md"), [ "# How it was built", "", "Every feature on this site starts as a written design spec, then an implementation plan, then the code. These are the real documents, copied from the repository's `docs/superpowers/` folder at build time.", "", "## Design specs", "", list("specs", result.specs), "", "## Implementation plans", "", list("plans", result.plans), "", ].join("\n") ); return result; }; if (process.argv[1] === fileURLToPath(import.meta.url)) { const here = path.dirname(fileURLToPath(import.meta.url)); const { specs, plans } = buildProcessDocs({ sourceRoot: path.resolve(here, "../../docs/superpowers"), outRoot: path.resolve(here, "../content/how-it-was-built"), }); console.log(`sync-process-docs: ${specs.length} specs, ${plans.length} plans`); }
  • Step 4: Run tests. yarn test → all pass.
  • Step 5: End-to-end check. yarn build renders /how-it-was-built/specs/... with the repo’s real specs and plans; curl one page (200) after yarn start -p 3400.
  • Step 6: Commit feat: generate the How it was built docs from the repo specs and plans.

Task 3: Hand-written pages

Files: Create under docs-site/content/: _meta.js, index.mdx (replace), architecture.mdx, content-model.mdx, styling.mdx, quality-gates.mdx, release-process.mdx.

Rules for every page: read the named sources first; every file path, command and number you write must exist in the repo (verify with grep); write in the site’s own plain voice; no marketing adjectives; use MDX only for headings, lists, tables and fenced code (no JSX needed). Link between pages with relative /architecture style paths.

  • Step 1: _meta.js
export default { index: "Overview", architecture: "Architecture", "content-model": "Content model", styling: "Styling system", "quality-gates": "Quality gates", "release-process": "Release process", "how-it-was-built": "How it was built", };
  • Step 2: Overview (index.mdx) from PRODUCT.md and package.json. Must contain: what the site is (personal portfolio for a New York City senior software engineer), the stack (Next.js 15 App Router, React 19, TypeScript, vanilla-extract with sprinkles, framer-motion, Lenis), the four product principles verbatim in meaning, a “where to go next” list linking the other pages and the Storybook link (https://storybook.antmejia.com) for components.

  • Step 3: Architecture from src/ tree, next.config.ts, src/app/**. Must contain: a folder map (src/app, src/sections, src/partials, src/components, src/containers, src/content, src/config, src/lib) with one-line responsibilities checked against the code; how a route composes (page.tsx → PageShell → content module); the /resume request flow (src/app/resume, ResumeDownload, src/lib/resume-request, src/app/api/resume-request/route.ts), including that nothing is stored; the env-validation modules (src/config/env).

  • Step 4: Content model from src/content/*.ts and public/resume.pdf. Must contain: the exact shape of ExperienceTimelineItem, how to add or edit a role and what updates automatically (landing timeline, /experience/[slug], /resume, sitemap), how skills (skills.ts, stackLabels) and resume.ts (contact, education) are edited, and how to replace public/resume.pdf. Include the rule from PRODUCT.md: only owner-written content.

  • Step 5: Styling system from src/config/styles/themes/theme.css.ts, src/partials/PageShell/*. Must contain: tokens (themeColors, breakpoints), sprinkles, the vanilla-extract identifier scheme (am-<hash>), the PageShell building blocks with a short usage example copied from a real page, and the print approach used by /resume.

  • Step 6: Quality gates from .github/workflows/ci.yml, package.json, vitest.config.ts, .storybook/. Must contain: the CI steps in order, local equivalents (yarn type-check, yarn lint, yarn format:ci, yarn build, yarn vitest run), the Vitest unit and storybook projects, how story tests work, commitlint/husky, and the honest limitation that branch protection is unavailable on the GitHub Free plan for a private repo.

  • Step 7: Release process from the README Deployment section and .github/workflows/{release-please,promote-production}.yml. Must contain: the flow diagram as a fenced text block, staging (main) versus production (production branch), release-please conventions (feat → minor, fix → patch), the rollback procedure with the exact workflow name and inputs, and the Vercel settings it depends on. State that the README remains the short entry point.

  • Step 8: Verify. yarn build passes; every internal link resolves (grep the built HTML for href="/, compare against generated routes); yarn format:ci at the repo root passes (Prettier covers docs-site/content/*.mdx).

  • Step 9: Commit docs: add the engineering docs pages.


Task 4: Branding

Files: Modify docs-site/app/layout.tsx; Create docs-site/app/theme.css, docs-site/public/logo.svg.

  • Step 1: Logo. Copy src/assets/icons/logos/logo.svg to docs-site/public/logo.svg (viewBox 0 0 703.5 229.04). In layout.tsx use logo={<img src="/logo.svg" alt="Anthony Mejia" height={20} />}; add logoLink="https://antmejia.com" if the Navbar accepts it in 4.6.1 (check node_modules/nextra-theme-docs/dist/components/navbar/index.d.mts).
  • Step 2: Fonts and accent. Load Prompt (body), Italiana (headings) and Saira via next/font/google in layout.tsx (variables --docs-font-body, --docs-font-heading, --docs-font-display) and add them to <body className>. In app/theme.css (imported after nextra-theme-docs/style.css) set the Nextra primary hue to a navy hue (--nextra-primary-hue: 220deg; --nextra-primary-saturation: 40%; verify the variable names in the built CSS), set body font-family to the body variable, and h1, h2 to the heading variable at weights the font supports (Italiana ships weight 400 only).
  • Step 3: Verify. yarn build && yarn start -p 3400; Playwright (see scratchpad/v-resume.mjs as the pattern) screenshots of /, /architecture, /how-it-was-built at 1440 and 390 px; axe via node_modules/axe-core against the portfolio repo’s copy (/home/antmejia/Projects/operation-looking-glass/node_modules/axe-core/axe.min.js) reports 0 violations; keyboard Tab order reaches search, nav and content; Pagefind search for “release” returns the Release process page.
  • Step 4: Impeccable audit once (/impeccable:impeccable audit on the built docs, theme limits acknowledged); fix findings in one batch.
  • Step 5: Commit feat: brand the docs site.

Task 5: Docs-only build trigger for Vercel (TDD)

Files: Create docs-site/scripts/vercel-ignore.sh, docs-site/scripts/vercel-ignore.test.mjs, docs-site/vercel.json.

Contract: Vercel runs ignoreCommand from the Root Directory (docs-site/); exit 0 = skip the build, exit 1 = build.

  • Step 1: Failing tests (create a temp git repo with docs-site/, docs/ and src/, commit, then run the script with env vars)
import assert from "node:assert/strict"; import { execFileSync, spawnSync } from "node:child_process"; import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; const script = path.resolve(import.meta.dirname, "vercel-ignore.sh"); const git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8" }).trim(); const repo = () => { const dir = mkdtempSync(path.join(tmpdir(), "vi-")); git(dir, "init", "-q", "-b", "production"); git(dir, "config", "user.email", "t@t"); git(dir, "config", "user.name", "t"); for (const d of ["docs-site", "docs", "src"]) mkdirSync(path.join(dir, d)); for (const f of ["docs-site/a.txt", "docs/a.txt", "src/a.txt"]) writeFileSync(path.join(dir, f), "1"); git(dir, "add", "."); git(dir, "commit", "-qm", "base"); return dir; }; const commit = (dir, file) => { writeFileSync(path.join(dir, file), String(Math.random())); git(dir, "add", "."); git(dir, "commit", "-qm", file); }; const run = (dir, env) => spawnSync("bash", [script], { cwd: path.join(dir, "docs-site"), env: { ...process.env, ...env }, encoding: "utf8" }) .status; test("skips branches other than production", () => { const dir = repo(); assert.equal(run(dir, { VERCEL_GIT_COMMIT_REF: "main", VERCEL_GIT_PREVIOUS_SHA: git(dir, "rev-parse", "HEAD") }), 0); }); test("skips production when only unrelated files changed since the last deploy", () => { const dir = repo(); const prev = git(dir, "rev-parse", "HEAD"); commit(dir, "src/a.txt"); commit(dir, "src/a.txt"); assert.equal(run(dir, { VERCEL_GIT_COMMIT_REF: "production", VERCEL_GIT_PREVIOUS_SHA: prev }), 0); }); test("builds production when docs-site or docs changed anywhere since the last deploy", () => { const dir = repo(); const prev = git(dir, "rev-parse", "HEAD"); commit(dir, "docs/a.txt"); commit(dir, "src/a.txt"); assert.equal(run(dir, { VERCEL_GIT_COMMIT_REF: "production", VERCEL_GIT_PREVIOUS_SHA: prev }), 1); const prev2 = git(dir, "rev-parse", "HEAD"); commit(dir, "docs-site/a.txt"); assert.equal(run(dir, { VERCEL_GIT_COMMIT_REF: "production", VERCEL_GIT_PREVIOUS_SHA: prev2 }), 1); }); test("builds production when there is no previous deploy or the diff cannot be computed", () => { const dir = repo(); assert.equal(run(dir, { VERCEL_GIT_COMMIT_REF: "production", VERCEL_GIT_PREVIOUS_SHA: "" }), 1); assert.equal(run(dir, { VERCEL_GIT_COMMIT_REF: "production", VERCEL_GIT_PREVIOUS_SHA: "deadbeef" }), 1); });
  • Step 2: Run to confirm failure. yarn test → fails (script missing).
  • Step 3: Implement vercel-ignore.sh
#!/usr/bin/env bash # Vercel "Ignored Build Step" for the docs project (Root Directory: docs-site). Exit 1 = build, 0 = skip. # Only the production branch deploys, and only when docs-site/ or docs/ changed since the last deployed commit. # VERCEL_GIT_PREVIOUS_SHA (not HEAD^) because the production branch fast-forwards over many commits per release. if [ "$VERCEL_GIT_COMMIT_REF" != "production" ]; then echo "Branch ${VERCEL_GIT_COMMIT_REF:-unknown} - docs deploy from production only, skipping." exit 0 fi if [ -z "$VERCEL_GIT_PREVIOUS_SHA" ]; then echo "No previous deployment - building." exit 1 fi git diff --quiet "$VERCEL_GIT_PREVIOUS_SHA" HEAD -- . ../docs 2>/dev/null case $? in 0) echo "No docs changes since $VERCEL_GIT_PREVIOUS_SHA - skipping."; exit 0 ;; 1) echo "Docs changed since $VERCEL_GIT_PREVIOUS_SHA - building."; exit 1 ;; *) echo "Could not diff against $VERCEL_GIT_PREVIOUS_SHA - building to be safe."; exit 1 ;; esac
  • Step 4: docs-site/vercel.json
{ "$schema": "https://openapi.vercel.sh/vercel.json", "ignoreCommand": "bash scripts/vercel-ignore.sh" }
  • Step 5: Run tests (all pass), chmod +x not required (invoked with bash). Step 6: Commit ci: build the docs only when docs changed.

Files: Modify .github/workflows/ci.yml, renovate.json, src/containers/Footer/Footer.tsx, redirects.ts, PRODUCT.md, README.md.

  • Step 1: CI. After “Build Storybook” (before commitlint) add:
- name: Install docs dependencies run: yarn --cwd docs-site install --frozen-lockfile - name: Test docs scripts run: yarn --cwd docs-site test - name: Build docs run: yarn --cwd docs-site build env: NEXT_TELEMETRY_DISABLED: "1"
  • Step 2: Renovate. Add to packageRules: { "matchFileNames": ["docs-site/package.json"], "matchPackageNames": ["zod"], "enabled": false, "description": "Nextra 4.6 breaks with newer zod; revisit when Nextra is upgraded." }.
  • Step 3: Footer. Read src/containers/Footer/Footer.tsx to see how links render. If external URLs are supported (check for target/rel), change { label: "Documentation", url: "/docs" } to url: "https://docs.antmejia.com"; if not, keep /docs and add to redirects.ts: { source: "/docs", destination: "https://docs.antmejia.com", permanent: false }. Either way add that redirect too, so antmejia.com/docs works.
  • Step 4: PRODUCT.md. Replace “Storybook and docs are deployed as separate services.” with “Storybook (storybook.antmejia.com) and the documentation site (docs.antmejia.com, source in docs-site/) are deployed as separate Vercel projects.” and update the “real destinations for /docs, /storybook and /resume” line to reflect that /docs and /resume now exist.
  • Step 5: README. Add a short “Documentation” note in the Deployment section: the docs site, where its source lives, yarn --cwd docs-site dev, and that it deploys from production only when docs changed.
  • Step 6: Verify yarn type-check && yarn lint && yarn format:ci && yarn build; push a throwaway branch? No: open the real PR and read the Check PR run for the docs steps.
  • Step 7: Commit ci: build docs in CI and link them from the footer.

Task 7: Vercel project, domain, release

  • Step 1: Create the project with the Vercel tools (no team scope params): create_git_project (or create_project + git link) for the repo ant-mejia/operation-looking-glass, name looking-glass-docs, framework nextjs, rootDirectory docs-site. Verify with get_project: rootDirectory is docs-site and sourceFilesOutsideRootDirectory is true (the sync script reads ../docs); if not, tell the owner to enable “Include source files outside of the Root Directory” in Settings → General.
  • Step 2: Production branch. The tools cannot set it. Ask the owner: Settings → Environments → Production Branch = production. Verify through the tools afterward.
  • Step 3: Domain. add_project_domain docs.antmejia.com; read back the DNS target and give the owner the exact Cloudflare record (CNAME, DNS only).
  • Step 4: Deployment protection. Confirm with get_project that production on the custom domain is public (SSO protection must not cover custom domains, as on the portfolio project); fix with the owner if not.
  • Step 5: After the release that carries the docs (merge the next release-please PR): production branch advances, a docs deployment starts, and curl -s -o /dev/null -w "%{http_code}" https://docs.antmejia.com/ returns 200; open /how-it-was-built, run a search, and check the footer link on antmejia.com.
  • Step 6: Close #44 with a comment summarizing the result and the owner steps completed.

Self-review

  • Spec coverage: structure and root-config isolation (T1, T6), content pages (T3), generated process docs with plain-Markdown safeguard (T2), look (T4), Vercel project/branch/domain/ignore step (T5, T7), CI (T6), footer, PRODUCT.md, README (T6), testing/probes/audit (T1, T4), risks (zod pin T1/T6, MDX safeguard T1/T2).
  • Placeholders: none; content tasks list required facts and sources instead of prose so the writer must verify against the repo.
  • Consistency: parseDocName, titleOf, buildProcessDocs and the docs-site script names match across tasks; the ignore script’s contract (exit 0 skip, 1 build) matches its tests and vercel.json.