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-sitehas its ownyarn.lock.zodmust be pinned to4.1.12: a throwaway build proved that zod 4.6.5 (what a fresh install resolves) makes every page 500 withInvalid input: expected nonoptional, received undefined → at childrenfromnextra-theme-docs’sLayout. 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.mdor specs. Quote real file paths and commands. - No secrets in the docs, and no mention of values from
.envfiles. - 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 -Aorgit add .: stage named paths only (a stray local.envandproject.inlang/editor files must never be committed). - Production branch for the docs project is
production;mainpushes 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
| File | Responsibility |
|---|---|
docs-site/package.json, yarn.lock | Dependencies (exact), scripts. |
docs-site/next.config.mjs | Nextra with .md as plain Markdown. |
docs-site/tsconfig.json, mdx-components.tsx | TS config, theme MDX components. |
docs-site/app/layout.tsx, app/[[...mdxPath]]/page.tsx | Theme 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.json | Docs-only build trigger. |
.github/workflows/ci.yml | Builds and tests the docs. |
tsconfig.json, .prettierignore, .gitignore, renovate.json | Root configs that must know about docs-site/. |
src/containers/Footer/Footer.tsx, redirects.ts, PRODUCT.md, README.md | Link 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/nodeConfirm 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.jsonand a starterdocs-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: appenddocs-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: appenddocs-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:ciExpected: 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 .prettierignorethengit 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 } | nullfor names like2026-10-06-resume-page-design.md. -
Produces
titleOf(markdown: string, fallback: string): string(first#heading). -
Produces
buildProcessDocs({ sourceRoot, outRoot }): { specs: Entry[]; plans: Entry[] }whereEntry = { date: string; slug: string; title: string }; writes<outRoot>/specs/<slug>.md,<outRoot>/plans/<slug>.md, a_meta.jsin 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 buildrenders/how-it-was-built/specs/...with the repo’s real specs and plans;curlone page (200) afteryarn 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) fromPRODUCT.mdandpackage.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/resumerequest 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/*.tsandpublic/resume.pdf. Must contain: the exact shape ofExperienceTimelineItem, how to add or edit a role and what updates automatically (landing timeline,/experience/[slug],/resume, sitemap), how skills (skills.ts,stackLabels) andresume.ts(contact, education) are edited, and how to replacepublic/resume.pdf. Include the rule fromPRODUCT.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>), thePageShellbuilding 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 Vitestunitandstorybookprojects, 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 (productionbranch), 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 buildpasses; every internal link resolves (grep the built HTML forhref="/, compare against generated routes);yarn format:ciat the repo root passes (Prettier coversdocs-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.svgtodocs-site/public/logo.svg(viewBox0 0 703.5 229.04). Inlayout.tsxuselogo={<img src="/logo.svg" alt="Anthony Mejia" height={20} />}; addlogoLink="https://antmejia.com"if the Navbar accepts it in 4.6.1 (checknode_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/googleinlayout.tsx(variables--docs-font-body,--docs-font-heading,--docs-font-display) and add them to<body className>. Inapp/theme.css(imported afternextra-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), setbodyfont-family to the body variable, andh1, h2to the heading variable at weights the font supports (Italiana ships weight 400 only). - Step 3: Verify.
yarn build && yarn start -p 3400; Playwright (seescratchpad/v-resume.mjsas the pattern) screenshots of/,/architecture,/how-it-was-builtat 1440 and 390 px; axe vianode_modules/axe-coreagainst 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 auditon 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/andsrc/, 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 +xnot required (invoked withbash). Step 6: Commitci: build the docs only when docs changed.
Task 6: CI, Renovate, footer, cleanup
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.tsxto see how links render. If external URLs are supported (check fortarget/rel), change{ label: "Documentation", url: "/docs" }tourl: "https://docs.antmejia.com"; if not, keep/docsand add toredirects.ts:{ source: "/docs", destination: "https://docs.antmejia.com", permanent: false }. Either way add that redirect too, soantmejia.com/docsworks. - 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 indocs-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 fromproductiononly 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 theCheck PRrun 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(orcreate_project+ git link) for the repoant-mejia/operation-looking-glass, namelooking-glass-docs, frameworknextjs, rootDirectorydocs-site. Verify withget_project:rootDirectoryisdocs-siteandsourceFilesOutsideRootDirectoryistrue(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_domaindocs.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_projectthat 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/returns200; open/how-it-was-built, run a search, and check the footer link onantmejia.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,buildProcessDocsand thedocs-sitescript names match across tasks; the ignore script’s contract (exit 0 skip, 1 build) matches its tests andvercel.json.