Arbitrum documentation portal — Next.js 16 / Fumadocs migration of OffchainLabs/arbitrum-docs.
Status: Phase 0 MVP. Single-committer, local-only; Vercel + CI follow in Phase 0.5.
pnpm install
pnpm dev # http://localhost:3000
pnpm types:checkNode 22 LTS · pnpm 10 · TypeScript strict · Tailwind 4.
Search and the "Ask AI" chat button are powered by Inkeep. Set the
publishable key in a local .env (gitignored):
NEXT_PUBLIC_INKEEP_API_KEY=<inkeep-search-key>Config lives in lib/inkeep.ts; the widgets mount in components/inkeep/ and are wired into
RootProvider in app/[lang]/layout.tsx (Inkeep replaces the built-in Fumadocs search dialog).
| Path | Purpose |
|---|---|
app/[lang]/docs/ |
Localized docs routes (en, zh-CN, ja). |
content/docs/<lang>/ |
MDX content + meta.json sidebars. |
content/partials/ |
Reusable _-prefixed fragments + generated CATALOG.md (see Partials). |
components/mdx/ |
Custom MDX components (registered in components/mdx.tsx). |
lib/source.ts |
Fumadocs source adapter. |
proxy.ts |
i18n routing + static-asset bypass list. |
source.config.ts |
Fumadocs MDX config (Zod-typed frontmatter). |
Reusable content fragments live in content/partials/ — a single source of truth you can inline
anywhere. They are _-prefixed and sit outside the doc collection, so they are never routed.
Before writing a banner, note, config table, or troubleshooting block, search
content/partials/CATALOG.md (⌘F by intent — title, summary, tags)
and reuse it instead of duplicating prose. The catalog gives you a copy-paste snippet per partial.
CATALOG.md and manifest.json (the machine-readable index for agents) are generated — never edit
them by hand.
Two ways to pull a partial into a page:
<!-- From a doc page: root-anchored, so moving the page never breaks it -->
<include cwd>content/partials/launch-arbitrum-chain/_raas-providers-notice.mdx</include><!-- From another partial: MUST be file-relative, not cwd -->
<include>../_hardware-requirements.mdx</include>// As a React component (e.g. an interactive selector):
import RollupProsCons from '@/content/partials/launch-arbitrum-chain/features/_rollup-pc.mdx';Why the split: a cwd include resolves from the repo root and is invariant under page moves, but it
only works in the docs pipeline — a partial compiled outside it (when ESM-imported) has no cwd
context and crashes the build. So partial→partial includes are always relative. partials:check
enforces this.
- Create
content/partials/<area>/_your-partial.mdx. No frontmatter —<include>strips it. - Reference it (see above), then run
pnpm partials:catalogto refresh the catalog + manifest. - Optionally curate its title/summary/tags/scope in
content/partials/registry.json:{ "content/partials/<area>/_your-partial.mdx": { "summary": "…", "tags": ["…"], "scope": "neutral" } }
pnpm partials:catalog # regenerate content/partials/CATALOG.md + manifest.json
pnpm partials:check # validate include/import resolution, no routing leak, catalog freshnesspartials:check fails on: an unresolved include or partial import, a _-prefixed file left under
content/docs/, a cwd include inside a partial, a bad registry.json entry, or a stale catalog.
Design notes: .claude/docs/superpowers/specs/2026-07-09-partials-registry-design.md.
Values that move on a release cadence — version tags, chain parameters, node image names — live in
one JSON file instead of being retyped across pages. Edit the value once and every page that
references it follows. This replaces Docusaurus's @@varName@@ preprocessing.
The current Nitro release is <Var name="nitroVersionTag" />.Var is a server component registered globally in components/mdx.tsx, so pages need no import. It
works inside partials too.
- Edit the value in
content/vars.json. - Run
pnpm vars:check.
Adding a new variable takes both files: the key in content/vars.json and its type in the
varsSchema in content/vars.ts. Miss either side and the gate fails.
vars.json is plain JSON, so writing a value needs no TypeScript. vars.ts validates it with a Zod
strictObject at module load, so a missing or mistyped key throws immediately with a field-level
error — in the pnpm dev console and in CI.
The strictness is load-bearing. A plain z.object silently strips keys that are present in the JSON
but absent from the schema, so <Var> renders the literal string undefined into the page; that is
how 27 variables once came to render undefined across 85 sites.
.mdx never passes through tsc, so the VarKey type does not protect MDX callers and
pnpm types:check exits 0 on a page full of broken variables. pnpm vars:check is the only gate
that catches a <Var name> with no matching key — it blocks in CI.
pnpm vars:check # fail if any <Var name> cannot resolve; also lists unreferenced keys
pnpm vars:check --json # machine-readable audit; exits 0
pnpm nitro:check-release # bump the pinned Nitro release values to the newest tagValues mirror upstream arbitrum-docs/src/resources/globalVars.js.
Keep them in sync while that site is still live.
Every redirect lives in redirects.config.mjs and is served by Next's
redirects(). It runs before proxy.ts, so a redirected URL gets markdown negotiation on the
destination, not on the first hop.
Two blocks, neither hand-edited:
- Moved pages —
pnpm move-doc <from> <to>writes the old→new URL between theAUTO-GENERATEDmarkers. - Legacy
docs.arbitrum.ioURLs —pnpm redirects:legacyregeneratesredirects.legacy.mjsfrom the upstreamvercel.json. It emits a redirect only when the destination exists here; the rest land inredirects.legacy.todo.jsonrather than being guessed at, because a redirect to a plausible-but-wrong page is worse than a 404.
After either, run pnpm redirects:check with the site running (pnpm dev). It validates every
destination against /llms.txt — the router's own page list — and fails on a dead destination or a
source that shadows a live page.
- Redirects: see Redirects — never hand-edit
redirects.config.mjs; move pages withpnpm move-docso the redirect is written for you. - Global variables: see Variables — edit
content/vars.json, never hardcode a version or chain parameter into a page. - Theme tokens are
--color-fd-*(Fumadocs) — never--ifm-*(legacy Docusaurus).
Fumadocs docs · Next.js App Router · MDX 3.