Arbitrum documentation portal, on Next.js 16 and Fumadocs. Serves English MDX docs from
OffchainLabs/arbitrum-docs; deployed on Vercel.
This file covers how to work on the docs. For how the codebase works and why, see INTERNALS.md. Contributing a page or a PR? Start with CONTRIBUTE.md instead. It covers the frontmatter contract, partials, variables, moving pages, and the gates to run before you push. For the prose itself, the house editorial standard is STYLE-GUIDE.md: plain-language rules, words and phrases to replace or cut, the terminology table, and the glossary-linking convention.
New to Fumadocs, or arriving from the old Docusaurus site? Start with What Fumadocs is and Coming from Docusaurus. They take about five minutes and cover the differences that cause the most mistakes.
Create your contribution branch from master and open pull requests against master.
pnpm install # runs a postinstall that generates .source/
pnpm dev # install, clean, then http://localhost:3000 with hot reloadNode 22 (>=22.18 <23) · pnpm 10. Other Node majors are rejected by engines.
Python 3.9 or newer must be available as python3 for pnpm test, which runs the brand-helper
security tests alongside the TypeScript suites. Those Python tests use only the standard library.
pnpm-workspace.yaml sets strictDepBuilds, so an install fails with ERR_PNPM_IGNORED_BUILDS
when a dependency's build script was skipped. Two cases: a node_modules installed before that
setting records the skipped build, so delete node_modules and install again; a new dependency
with a build script needs an entry in ignoredBuiltDependencies or onlyBuiltDependencies in
that file, with a comment saying what its script does. minimumReleaseAge there also refuses a
version published in the last seven days; add it to minimumReleaseAgeExclude to take one on
purpose.
Browse on localhost:3000, not 127.0.0.1. On 127.0.0.1 React does not hydrate and every
component looks broken.
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/layout.tsx.
None of these are needed to run the site locally; everything that reads them degrades to a no-op or a documented fallback.
| Variable | Used by | Without it |
|---|---|---|
NEXT_PUBLIC_INKEEP_API_KEY |
search and the "Ask AI" button | both are unavailable |
NEXT_PUBLIC_SITE_URL |
metadataBase, app/sitemap.ts, app/robots.ts, request tracking |
http://localhost:3000 locally; a production build fails |
NEXT_PUBLIC_POSTHOG_KEY |
page feedback (lib/posthog.ts), web analytics, and request tracking in proxy.ts |
feedback submissions and tracking events are dropped with a server-side log |
NEXT_PUBLIC_VERCEL_ENV |
the production gate on web analytics and the Inkeep event bridge | neither fires; Vercel sets this one, you never do |
VERCEL_DEEP_CLONE |
hasFullGitHistory() in source.config.ts, which gates the last-modified dates |
a shallow Vercel clone resolves no dates: no "Last updated" line, no <lastmod>, no article:modified_time, and nothing warns. Set it to true on the Vercel project |
Set the PostHog token the same way as the Inkeep key, in a local .env (gitignored):
NEXT_PUBLIC_POSTHOG_KEY=phc_<posthog-project-token>NEXT_PUBLIC_POSTHOG_KEY is PostHog's documented name for the publishable phc_ project token
(Project settings, Project API key). It is write-only, so the NEXT_PUBLIC_ prefix is safe even
though two of its three consumers read it on the server. Set it on Vercel for Preview and
Production.
Page feedback needs the key locally. Web analytics does not fire locally or on a preview deployment
no matter what you set, because components/analytics/posthog-provider.tsx also requires
NEXT_PUBLIC_VERCEL_ENV to be production and only Vercel sets that. Request tracking is gated the
same way, on the server-side VERCEL_ENV, so nothing is sent locally or from a preview and no key
is needed for either. See Analytics.
Each tracking event carries a random distinct_id and creates no person profile. The proxy never
reads the reader's IP address. See Routing and proxy.ts.
pnpm types:check # the main verification gate
pnpm frontmatter:check # every documentation page satisfies the frontmatter schema
pnpm test # tooling tests, including the sidebar and redirect checks
pnpm check-links # broken internal links and MDX fragments
pnpm content:lint # MDX that compiles but renders wrong
pnpm faq:check # the FAQ partials match their Notion snapshots
pnpm format # prettier, in placeCI runs ten blocking checks, then a pnpm build that serves the built site and checks it over
HTTP. pnpm build runs the same link check first, so a broken link fails the Vercel deploy too. See
The gates for the full list. There is no pre-commit hook, so run these
yourself.
types:check checks TypeScript; frontmatter:check validates page metadata. Neither proves
the render. Type checking passes on a page that serves literal ::: or
undefined. Always confirm content changes in a browser.
| Path | Purpose |
|---|---|
content/docs/ |
MDX pages and the meta.json files that order the sidebar |
content/partials/ |
Reusable _-prefixed fragments, included into pages |
content/glossary/ |
Glossary terms for <Term> (hand-written) |
content/vars.json |
Global variables |
app/(docs)/[...slug]/ |
Docs route |
components/mdx.tsx |
The MDX component registry |
components/widgets/ |
The four interactive widgets, each used by one page |
lib/source.ts |
Fumadocs source adapter |
proxy.ts |
PostHog tracking for markdown and llms*.txt fetches |
source.config.ts |
Fumadocs MDX config: the two collections |
lib/page-schema.ts |
The frontmatter schema every page must satisfy |
Every page needs a title and a description. A missing one fails pnpm frontmatter:check and
the build. pnpm types:check does not see frontmatter, so run the check before you push.
---
title: 'How to run a full node'
description: One-line summary shown in search results and social cards.
content_type: how-to
author: your-github-handle
sme: reviewing-sme-handle
---content_type, author, sme, sidebar_label and user_story are optional. When set, content_type must be
one of how-to, concept, quickstart, tutorial, reference, troubleshooting, faq.
The sidebar comes from the meta.json in each directory: a page's directory is its place, and
that file's pages array orders it. sidebar_label replaces the title as the page's sidebar name.
See Place your page in the sidebar and
The sidebar and its roots.
Callouts use Fumadocs' component. Docusaurus ::: directives render as plain text.
<Callout type="warn" title="Before you start">
Fund the batch poster account first.
</Callout>type is one of info, warn, error, idea or success.
Before writing a banner, note, config table, or troubleshooting block, look in
content/partials/ and reuse a partial instead of duplicating prose. File names say what each
one holds.
<!-- From a doc page: root-anchored, so moving the page never breaks it -->
<include cwd>content/partials/_hardware-requirements.mdx</include><!-- From another partial: file-relative -->
<include>../_hardware-requirements.mdx</include>To add one, create content/partials/<area>/_your-partial.mdx and include it. It needs no
frontmatter. (Details.)
Values that move on a release cadence (version tags, chain parameters, node image names) live in one file, so you edit them once and every page follows.
The current Nitro release is <Var name="nitroVersionTag" />.Var is registered globally, so pages need no import. It works inside partials too.
Variables do not work inside code. MDX does not evaluate components inside a fenced code block
or an inline code span, so <Var name="…" /> there renders as a literal tag, not its value.
Usually the value was never code to begin with, and dropping the backticks is the whole fix. When a
reader is meant to copy the line, as in a docker run command, hardcode the current value in the
code and reference the variable in the prose next to it. pnpm content:lint (rule var-in-code)
fails on any <Var> found inside code. To have a hardcoded copy of latestNitroNodeImage kept current for you,
put {/* sync-with-var: latestNitroNodeImage */} anywhere in the page and
pnpm nitro:check-release --to <tag> will rewrite it when it bumps that variable. Do not put that marker on a page that states a
Nitro version as a historical fact, such as an ArbOS release note, or a bump will rewrite history.
Variables do not work in a link destination either, and that one leaves no link at all. A
markdown link destination may not contain a space and <Var name="…" /> contains two, so the link
never parses and the reader is served the literal [text](…) brackets. Write the variable as a
{var:name} placeholder in the destination instead:
[Interface](https://github.com/OffchainLabs/{var:nitroRepositorySlug}/blob/{var:nitroVersionTag}/precompiles/ArbSys.go)Use as many placeholders as the URL needs. The same form works in an href, to or src
attribute, in a link title, and in an internal /<slug> destination, which pnpm check-links
expands before it resolves. Everywhere else on the page, the link text included, keep using
<Var name="…" />: a placeholder written in prose is read as a JavaScript expression and fails the
build with an acorn parse error. The one destination it cannot do is a local image path
(), which is imported before the placeholder is expanded, so write that path out in
full. pnpm content:lint (rule var-in-link) fails on a <Var> left in a destination, and pnpm vars:check
reads placeholders too, so a mistyped name is caught the way a mistyped <Var> name is.
To update a value: edit content/vars.json, then run pnpm vars:check.
To add a new variable: add the key to content/vars.json. No other file changes.
(Details.)
Never hardcode a version or chain parameter into a page.
Links to a file in this repository are variables too. docsRepositoryUrl and
docsRepositoryBranch hold this repository's own GitHub identity, so a link to CONTRIBUTE.md,
or STYLE-GUIDE.md is written
[Contribute]({var:docsRepositoryUrl}/blob/{var:docsRepositoryBranch}/CONTRIBUTE.md). The same two
values build the edit link and the "Request an update" button on every page, so editing
docsRepositoryUrl once moves every link home at the same time. That is the one value that changes
when this repository takes over the arbitrum-docs name. pnpm check-links skips an external URL
without resolving it, so a hardcoded one would not be caught if it went dead.
The bar above the navbar is configured from the same file, so turning it on, rewording it, or retiring it is a content edit. Five keys control it:
| Key | Meaning |
|---|---|
announcementEnabled |
false renders nothing at all |
announcementText |
The message, shown before the link |
announcementLinkText |
The link label |
announcementLinkHref |
Where the link goes |
announcementId |
Dismissal key. Change it whenever you change the message |
announcementId also lands in the page as an HTML id and inside a CSS selector, so it has to
start with a letter and use only letters, digits, hyphens and underscores. pnpm vars:check fails
on anything else.
Keep the message short: announcementText plus announcementLinkText under roughly 140
characters combined. The bar has a fixed height (3rem, and 4rem below 640px) because the layout
feeds that number into the sticky offsets of every page, so it cannot grow to fit a longer message.
It will not clip a message at the length above, but there is no gate on this and nothing will warn
you. After changing the text, look at the top of a docs page in a browser window narrowed to about
400px wide and confirm nothing is cut off.
Dismissal is permanent per viewer, not per session. A reader who closes the banner has
announcementId written to their browser's localStorage, which survives closing the tab and
every later visit, so they never see that id again on that browser. Reuse an id for a new message
and everyone who dismissed the old one misses the new one. Give each message its own id.
announcementLinkHref is checked by pnpm vars:check: it has to be an https URL, or a
root-absolute internal path that resolves to a real page or a file under public/. Nothing else
would catch a typo there, because pnpm check-links only reads MDX.
pnpm move-doc <from> <to>This rewrites inbound links, re-bases the moved page's own relative links and includes, updates
meta.json, and appends the redirect. Add --dry-run to see all of it without touching a file. It
touches no other redirect. If an older redirect pointed at the old URL, pnpm test fails and names
the entry to retarget by hand.
Never hand-edit between the AUTO-GENERATED markers in redirects.config.ts, since move-doc
owns that block. (Details.)
pnpm dev # pnpm install, pnpm clean, next dev on http://localhost:3000
pnpm clean # delete .next/ and .source/ (next dev regenerates .source/)
pnpm types:check # regenerate .source/, generate Next types, tsc --noEmit
pnpm frontmatter:check # every page's frontmatter satisfies lib/page-schema.ts
pnpm build # production build (runs check-links first)
pnpm start # serve the production build
pnpm test # tooling test suites, including the sidebar and redirects
pnpm test:browser # Chromium interactions against a running production server
pnpm check-links # broken internal doc links and MDX fragments
pnpm vars:check # every <Var name> and {var:name} resolves; banner keys are valid
pnpm references:check # every <Term id> resolves
pnpm contracts:check # the contract-address partial is current
pnpm faq:check # the six FAQ partials match content/faq/*.json
pnpm content:lint # MDX structural defects
pnpm format:check # prettier (pnpm format writes)
pnpm move-doc <from> <to>Nitro, precompile, contract, CLI, Stylus and edge-challenge tooling runs by hand only. See Hand-run tools.
For browser tests, run pnpm exec playwright install chromium, pnpm build, and pnpm start.
In another terminal, run pnpm test:browser. It uses http://localhost:3000 by default; set
STATIC_DOCS_TEST_URL to use another server. The suite checks native text search and text-fragment
reveal on hidden tab and accordion panels, simulates repeated beforematch events, and checks
manual selection.
- Theme tokens are
--color-fd-*(Fumadocs). Never--ifm-*(legacy Docusaurus). - Route constants live in
lib/shared.ts. Reference these rather than hardcoding paths. - Never hand-edit generated files:
.source/, theAUTO-GENERATEDblock inredirects.config.ts, the precompile tables and contract-address partial, the generated region of the Nitro CLI flags page, and every page undercontent/docs/stylus/stylus-by-example/(republished fromoffchainlabs/stylus-by-examplebypnpm stylus:generate, so fix those upstream). - Fumadocs reference: https://www.fumadocs.dev/llms.txt