dependabot/github_actions/pnpm/action-setup-6 deployment.pmndrs/docs
pmndrs/* projects.npm@pmndrs/docs storybook chromaticstorybook
Summary
A static MDX documentation generator, with a GitHub reusable workflow. It is primarily used for some pmndrs/* projects, but will work for anyone.

Those projects are known to be using this generator.
INSTALL
Clone the repository
$ git clone https://github.com/pmndrs/docs.git
$ cd docs
Use the right Node version
With nvm, which reads the repository's .nvmrc:
$ nvm install
$ nvm use
$ node -v # make sure your version satisfies package.json#engines.node
nb: if you want this node version to be your default nvm's one:
nvm alias default node
Install the dependencies
$ pnpm install
Configuration
Environment variables configure the website. For MDX extensions, add docs.config.ts in the current working directory: components registers React components and scope exposes named data. Both fields are optional. See Custom components and data for the typed configuration and a live example.
Variables default to "" (empty) unless a fallback is documented below.
* Required
Readers can re-seed THEME_PRIMARY, THEME_SCHEME, THEME_CONTRAST and THEME_COLOR_MATCH from the theme controls in the sidebar. An overridden control is outlined, and a double-click (or Delete) brings the site's value back.
MDX_BASEURL
Given a advanced/introduction.mdx file in the MDX folder:

becomes (for a MDX_BASEURL=http://localhost:60141 value):

http://localhost:60141 being the MDX folder served.
When deployed on GitHub Pages, MDX_BASEURL will typically value something like https://github.com/pmndrs/uikit/raw/main/docs, thanks to build.yml rule.
MDX_BASEURL can also be a path only, root-relative, such as /react-three-fiber (or / for a site served at the root of its domain): the MDX folder then has to be served at that path, on the same host as the website. URLs then come out root-relative too:

so they follow the website onto any host it answers on (e.g. both the deployment and the branch URL of a Vercel preview), where an absolute URL would point to one host only.
With a path, the image frontmatter (og:image) is made absolute against the origin of NEXT_PUBLIC_URL, when set, since social images need an absolute URL.
THEME_*
We implement m3 design system, using material-theme-builder.
This is an embed iframe of material-theme-builder's <Mtb> story.
- Material Color for more information
- The defaults are pmndrs/design-system's palette: the lime
#CAF543under color match, a warm grey neutral, and seven colors every site gets as custom colors,lime,teal,cyan,purple,red,orangeandyellow(<Color role="teal" />). THEME_PRIMARYis the seed: the secondary and tertiary colors are derived from it, unless the site gives its own (THEME_SECONDARY,THEME_TERTIARY). The neutral, neutral variant and error colors are the pmndrs ones, unless the site gives its own (THEME_NEUTRAL,THEME_NEUTRAL_VARIANT,THEME_ERROR).THEME_COLOR_MATCH(on unless"false") is Material Theme Builder's "Color match - Stay true to my color inputs": each core and custom color keeps its input's chroma instead of being toned down to the scheme's, soTHEME_SCHEMEis ignored and the scheme toggle is hidden while color match is on. Readers can toggle color match from the sidebar. A custom color with:blendis still harmonized first.
VERSION_*, LIB_VERSION, TAG_MATCH
- Label:
git describe --tags(eg.v10.7.9,v10.7.9-3-ga1b2c3d), or<branch>@<shortsha>without tags.LIB_VERSIONoverrides it. TAG_MATCHnarrows the tags:
VERSION_URL_TEMPLATEenables the switcher: the label lists the branches, each leading to the same page on its deployment. Full public URL (base path included), with a placeholder:
-
A hostname label holds 63 characters at most. When a long branch would overflow it,
{branch:vercel}follows Vercel's own truncation, and{branch}keeps its slug's first63 - (rest of the label) - 7characters, drops a trailing-, and appends-and the first 6 hex characters of the branch name's SHA-256 (printf %s "$branch" | sha256sum | cut -c1-6): eg.claude/pages-shadcn-tailwind-prep-98d23cinhttps://design-system-git-{branch}-pmndrs.vercel.appbecomesdesign-system-git-claude-pages-shadcn-tailwind-pr-2675d4-pmndrs. Outside the hostname,{branch}is never cut. To alias a deployment there,pmndrs-docs version-urlprints the URL (--hostname: the hostname alone), or see the reusable workflow'sversion_hostnameoutput. -
VERSION_PRODUCTION_BRANCHleads toVERSION_PRODUCTION_URL; every other branch shows a banner linking to production. -
Branches: the remote's, minus
dependabot/,renovate/,changeset-release/.VERSION_BRANCHESreplaces that filter,VERSION_BRANCHES_LISTthe whole list. Read at build time.
- The per-branch URLs are yours to create: Vercel's Git integration does,
vercel deploydoes not (vercel alias set, seeci.yml). - Git needs full history and tags (
fetch-depth: 0).
Usage
dev
Run dev.sh. The site listens on PORT (default 3000), the MDX folder on any free port, so several checkouts can run side by side:
$ ./dev.sh
which is:
#!/bin/sh
trap 'kill -9 0' SIGINT
export PORT=${PORT:-3000}
export _PORT=$(node -e 'const s = require("net").createServer().listen(0, () => { console.log(String(s.address().port)); s.close() })')
export MDX=docs
export NEXT_PUBLIC_LIBNAME="docs"
export NEXT_PUBLIC_LIBNAME_SHORT="docs"
export BASE_PATH=
export HOME_REDIRECT=
export MDX_BASEURL=http://localhost:$_PORT
export SOURCECODE_BASEURL="vscode://file$(pwd)"
export EDIT_BASEURL="vscode://file$(pwd)/docs"
export NEXT_PUBLIC_URL=
export ICON=
export LOGO=gutenberg.jpg
export GITHUB=https://github.com/pmndrs/docs
export DISCORD=https://discord.com/channels/740090768164651008/1264328004172255393
export CONTRIBUTORS_PAT=
npx serve $MDX -p $_PORT --no-port-switching --no-clipboard &
pnpm run dev &
wait
Then go to: http://localhost:3000
When running the engine against another repository, set both MDX and DOCS_CONFIG; the configuration is discovered from the working directory, not the MDX folder:
MDX=/path/to/library/docs DOCS_CONFIG=/path/to/library/docs.config.ts pnpm dev
If HOME_REDIRECT= empty, / will not redirect, and instead displays an index of libraries.
build
Run start.sh, same ports:
$ ./start.sh
which is:
#!/bin/sh
trap 'kill -9 0' SIGINT
rm -rf out
export PORT=${PORT:-3000}
export _PORT=$(node -e 'const s = require("net").createServer().listen(0, () => { console.log(String(s.address().port)); s.close() })')
export MDX=docs
export NEXT_PUBLIC_LIBNAME="docs"
export NEXT_PUBLIC_LIBNAME_SHORT="docs"
export BASE_PATH=
export HOME_REDIRECT=
export MDX_BASEURL=http://localhost:$_PORT
export SOURCECODE_BASEURL=
export EDIT_BASEURL=
export NEXT_PUBLIC_URL=
export ICON=
export LOGO=gutenberg.jpg
export GITHUB=https://github.com/pmndrs/docs
export DISCORD=https://discord.com/channels/740090768164651008/1264328004172255393
export CONTRIBUTORS_PAT=
# The static export is the CLI's, built from this checkout: the same one the package publishes
pnpm run build:cli || exit 1
node bin/pmndrs-docs.mjs build $MDX out --format website || exit 1
npx serve $MDX -p $_PORT --no-port-switching --no-clipboard &
npx serve out -p $PORT &
wait
CLI
The published CLI builds without cloning the engine. If your custom components import packages, install those dependencies in your repository first:
$ cd ~/code/pmndrs/react-three-fiber
$ curl -sL https://raw.githubusercontent.com/pmndrs/docs/refs/heads/main/preview.sh | \
MDX="docs" \
NEXT_PUBLIC_LIBNAME="React Three Fiber" \
HOME_REDIRECT="/getting-started/introduction" \
ICON="🇨🇭" \
sh
which is:
#!/bin/sh
main() {
trap 'kill -9 0' SIGINT
export PORT="${PORT:-3000}"
export _PORT="${_PORT:-$(node -e 'const s = require("net").createServer().listen(0, () => { console.log(String(s.address().port)); s.close() })')}"
export VERSION="${VERSION:-latest}"
export MDX="${MDX:-docs}"
export NEXT_PUBLIC_LIBNAME="${NEXT_PUBLIC_LIBNAME:-docs}"
# Relative assets resolve against the MDX folder, served below on the same port
export MDX_BASEURL="${MDX_BASEURL:-http://localhost:$_PORT}"
rm -rf "$MDX/out"
# Every other option is read straight from the environment — same process, nothing to forward
npx -y "@pmndrs/docs@$VERSION" build "$MDX" "$MDX/out" --format website || exit 1
npx serve $MDX -p $_PORT --no-port-switching --no-clipboard &
npx -y serve "$MDX/out" -p $PORT &
wait
}
main
Any configuration variable goes in the same way; VERSION picks a published version of the CLI.
Then go to: http://localhost:3000
npx @pmndrs/docs@latest build --help lists the CLI flags, including --config for a configuration outside the repository root.
Both --format website and --format fragment use the same Next.js build and static export.
Fragments contain only the compiled document: no layout, stylesheet or script. The CLI
copies assets alongside folder fragments and preserves static Mermaid and Sandpack content.
$ npx @pmndrs/docs@latest build page.mdx
$ cat page.mdx | npx @pmndrs/docs@latest build
A single file or stdin writes HTML to stdout and build logs to stderr. These inputs also
run a full Next.js build. Consumer components and data use the same docs.config.ts as
the website.
read
The same CLI reads every pmndrs library in the terminal:
$ npx @pmndrs/docs # every library, every page
$ npx @pmndrs/docs drei # one library
$ npx @pmndrs/docs drei/performances/instances # one page
$ npx @pmndrs/docs search instanced mesh # one result per line, on stdout
↑↓ moves in the focused pane, ⇥ switches pane, ←→ changes library, / searches, o opens
the page in a browser, q quits. npx @pmndrs/docs browse --help lists every key.
search prints {lib} {path} - {title} and exits 1 when nothing matches; --in drei narrows
it to one library, --in drei/performances/instances to the matching lines of one page.
Pages come from each library's llms-full.txt, cached for an hour (--refresh refetches).
Piped, a page goes to stdout: npx @pmndrs/docs drei/performances/instances | glow.
Agents
llms.txt dumps and the pmndrs MCP server moved to their own page: Agents.