| title | How to Build and Publish Dynamo Docs |
|---|---|
| subtitle | Branch model, sync workflow, and publishing pipeline for the Fern-powered Dynamo documentation site. |
| sidebar-title | Building and Publishing |
This document describes the architecture, workflows, and maintenance procedures for the NVIDIA Dynamo documentation website powered by Fern. It covers how the docs system works: the branch model, sync workflow, and publishing pipeline. For how to write docs content (page types, structure, prose, terminology, and links), see the Documentation Style Guide.
The documentation website is published at [https://docs.nvidia.com/dynamo](https://docs.nvidia.com/dynamo). CI handles publishing, including hosting, CDN, and versioned URL routing. The `docs-website` branch is **CI-managed and must never be edited by hand**. All documentation authoring happens on `main` (or a feature branch based on `main`). The sync workflow copies changes to `docs-website` automatically.- Branch Architecture
- Directory Layout
- Configuration Files
- GitHub Workflows
- Content Authoring
- Callout Conversion
- Running Locally
- Version Management
- How Publishing Works
- Common Tasks
- Claude Code Skills
A single Claude Code skill automates common docs tasks. Invoke it as a slash
command in Claude Code (e.g., /dynamo-docs) — the skill walks through
the full workflow: creating, editing, or removing the markdown file, updating
the navigation in docs/index.yml, and running fern check to validate.
| Skill | Description |
|---|---|
| dynamo-docs | Add, update, move, or remove a docs page |
The documentation system uses a dual-branch model:
| Branch | Purpose | Content | Fern config |
|---|---|---|---|
main |
Source of truth for dev (unreleased) documentation | docs/ |
fern/ |
docs-website |
Published documentation including all versioned snapshots | fern/pages/ |
fern/ |
Authors edit pages on main. A GitHub Actions workflow automatically syncs
changes to the docs-website branch and publishes them to Fern. The
docs-website branch is never edited by hand — it is entirely managed by CI.
The docs-website branch accumulates versioned snapshots over time (e.g.
pages-v0.8.0/, pages-v0.8.1/). Keeping these on a separate branch avoids
bloating the main branch with frozen copies of old documentation.
fern/ # Fern CLI configuration (fern/ is a Fern convention)
├── fern.config.json # Fern org + CLI version pin
├── docs.yml # Site configuration (instances, branding, layout)
├── components/
│ └── CustomFooter.tsx # React component for the site footer
├── main.css # Custom CSS (NVIDIA branding, dark mode, etc.)
├── convert_callouts.py # GitHub → Fern admonition converter script
└── .gitignore # Fern-specific ignores
docs/ # Documentation content
├── index.yml # Navigation tree for the dev version
├── getting-started/ # Markdown content (the actual docs)
├── kubernetes/
├── reference/
├── ...
├── assets/ # Images, fonts, SVGs, logos
├── digest/ # Digest posts
└── diagrams/ # D2 diagram source files
The docs-website branch has a different layout optimized for Fern's directory
conventions, plus versioned snapshots:
fern/
├── fern.config.json # Fern org + CLI version pin
├── docs.yml # Includes the full versions array
├── versions/
│ ├── dev.yml # "Next" / dev navigation (synced from main)
│ ├── v0.8.1.yml # Navigation for v0.8.1 snapshot
│ └── v0.8.0.yml # Navigation for v0.8.0 snapshot
├── pages/ # Current dev content (synced from main)
├── pages-v0.8.1/ # Frozen snapshot of pages/ at v0.8.1
├── pages-v0.8.0/ # Frozen snapshot of pages/ at v0.8.0
├── components/ # React components
├── main.css # Custom CSS
├── convert_callouts.py # Callout converter
├── digest/ # Digest posts (synced from main)
└── assets/ # Images, fonts, SVGs
Each pages-vX.Y.Z/ directory is an immutable snapshot built from docs/ at
the corresponding Git tag. The matching versions/vX.Y.Z.yml is built from
that tag's docs/index.yml, with content paths rewritten to
../pages-vX.Y.Z/.
The sync workflow copies content from main's docs/ into fern/pages/ and
transforms navigation paths in index.yml → versions/dev.yml accordingly.
{
"organization": "nvidia",
"version": "3.73.0"
}- organization: The Fern organization that owns the docs site.
- version: Pins the Fern CLI version used for generation.
This is the main Fern site configuration. Key sections:
| Section | Purpose |
|---|---|
instances |
Deployment targets — staging URL and custom production domain |
products |
Defines the product ("Dynamo") and its version list |
navbar-links |
GitHub repo link in the navigation bar |
footer |
Points to CustomFooter.tsx React component |
layout |
Page width, sidebar width, searchbar placement, etc. |
colors |
NVIDIA green (#76B900) accent, black/white backgrounds |
typography |
NVIDIA Sans body font, Roboto Mono code font |
logo |
NVIDIA logos (dark + light variants), 20px height |
js |
Adobe Analytics script injection |
css |
Custom main.css stylesheet |
Important: On main, docs.yml only lists the dev version. On
docs-website, it contains the full versions array (dev + all releases).
The sync workflow preserves the versions array from docs-website when copying
docs.yml from main.
Defines the navigation tree — the sidebar structure of the docs site. Each entry maps a page title to a markdown file path:
navigation:
- section: Getting Started
contents:
- page: Quickstart
path: getting-started/quickstart.mdxx
- page: Support Matrix
path: reference/support-matrix.mdPaths are relative to the docs/ directory. Sections can be nested. Pages can
be marked as hidden: true to make them accessible by URL but invisible in the
sidebar.
During sync to docs-website, the workflow copies index.yml to
fern/versions/dev.yml and transforms paths (e.g., getting-started/X →
../pages/getting-started/X) to match the docs-website directory layout.
Location: .github/workflows/fern-docs.yml
This single consolidated workflow handles linting, syncing, versioning, and publishing. It runs three jobs depending on the trigger:
Triggers: Pull requests that modify docs/** files.
Steps:
fern check— validates Fern configuration syntaxfern docs broken-links— checks for broken internal links
Purpose: Catches broken docs before they merge.
Triggers: Push to main that modifies docs/** files, or manual
workflow_dispatch (with no tag specified).
Steps:
- Checks out both
mainanddocs-websitebranches side-by-side - Copies content from
main'sdocs/→docs-website'sfern/pages/ - Copies
docs/index.yml→fern/versions/dev.ymland transforms paths for the docs-website layout usingyq - Syncs assets from
docs/assets/and Digest posts fromdocs/digest/ - Copies Fern config files from
fern/→ docs-website'sfern/(fern.config.json,components/,main.css,convert_callouts.py) - Runs
convert_callouts.pyto transform GitHub-style callouts to Fern format - Updates
docs.ymlfrommainwhile preserving the versions array fromdocs-website(usesyqto save/restore the versions list) - Commits and pushes to
docs-website - Publishes to Fern via
fern generate --docs
Triggers: New Git tags matching vX.Y.Z (e.g., v0.9.0, v1.0.0), or
manual workflow_dispatch with a tag specified.
Steps:
- Validates tag format (must be exactly
vX.Y.Z, no suffixes like-rc1) - Checks out the tagged source and the
docs-websitebranch side by side - Checks that the version doesn't already exist, unless a manual dispatch sets
force_rebuild=true - Creates
fern/pages-vX.Y.Z/from the tag's rawdocs/, excluding the shareddigest/tree andindex.yml - Rewrites GitHub links in the snapshot:
github.com/ai-dynamo/dynamo/tree/main→tree/vX.Y.Zgithub.com/ai-dynamo/dynamo/blob/main→blob/vX.Y.Z
- Runs the tag's
convert_callouts.pyonce on the raw snapshot - Creates
fern/versions/vX.Y.Z.ymlfrom the tag'sdocs/index.yml - Updates
fern/docs.yml:- Inserts new version right after the "dev" entry
- Sets the product's default
pathto the new version - Updates the "Latest" display-name to
"Latest (vX.Y.Z)"
- Verifies the tagged file inventory, versioned navigation targets, and Fern configuration; missing shared Digest targets produce warnings
- Commits and pushes to
docs-website - Publishes to Fern via
fern generate --docs
Anti-recursion note: Pushes made with GITHUB_TOKEN do not trigger other
workflows (GitHub's built-in guard). This is why the publish step is inline in
each job rather than in a separate workflow.
Location: .github/workflows/docs-link-check.yml
Triggers: Push to main and pull requests.
Runs two independent link-checking jobs:
| Job | Tool | What it checks |
|---|---|---|
lychee |
Lychee | External HTTP links (with caching, retries, rate-limit handling). Runs offline for PRs. |
broken-links-check |
Custom Python script (detect_broken_links.py) |
Internal relative markdown links and symlinks. Creates GitHub annotations on PRs pointing to exact lines with broken links. |
This section covers the mechanics of authoring on main. For the writing standard (page types,
headings, prose, terminology, links, and the pre-merge checklist), follow the
Documentation Style Guide.
- Edit or add markdown files in
docs/. - If adding a new page, add an entry in
docs/index.ymlto make it appear in the sidebar navigation. - Use standard GitHub-flavored markdown. Callouts (admonitions) should use
GitHub's native syntax — they are automatically converted during sync:
> [!NOTE] > This is a note that will become a Fern `<Note>` component. > [!WARNING] > This warning will become a Fern `<Warning>` component.
- Open a PR. The lint jobs (
fern check,fern docs broken-links, lychee, broken-links-check) run automatically. - Once merged to
main, the sync-dev workflow publishes changes within minutes.
Place images in docs/assets/ and reference them with relative paths from your
markdown files:
React components in fern/components/ can be used in markdown via MDX. The
CustomFooter.tsx renders the NVIDIA footer with legal links and branding.
The fern/convert_callouts.py script bridges the gap between GitHub-flavored
markdown and Fern's admonition format. This lets authors use GitHub's native
callout syntax on main while Fern gets its required component format.
| GitHub Syntax | Fern Component |
|---|---|
> [!NOTE] |
<Note> |
> [!TIP] |
<Tip> |
> [!IMPORTANT] |
<Info> |
> [!WARNING] |
<Warning> |
> [!CAUTION] |
<Error> |
# Convert all files in a directory (recursive, in-place)
python fern/convert_callouts.py --dir docs/
# Convert a single file
python fern/convert_callouts.py input.md output.md
# Run built-in tests
python fern/convert_callouts.py --testThe conversion happens automatically during the sync-dev and release-version workflows. Authors never need to run it manually.
You can preview the documentation site on your machine using the Fern CLI. This is useful for verifying layout, navigation, and content before opening a PR.
Install the Fern CLI globally via npm:
npm install -g fern-apiRun fern check from the repo root to validate that fern/docs.yml,
fern/fern.config.json, and the navigation files are syntactically correct:
fern checkUse fern docs broken-links to scan all pages for internal links that don't
resolve:
fern docs broken-linksThis is the same check that runs in CI on every pull request.
Build a tagged snapshot in temporary worktrees and run the release validation without committing, pushing, or publishing:
fern/release_dryrun.sh v1.2.1The script requires git, fern, yq, jq, rsync, and Python 3.10 or
newer. Set PYTHON=.venv/bin/python to select a non-default interpreter, and
set KEEP=1 to retain the temporary worktrees for inspection.
Run fern docs dev to build the site and serve it locally with hot-reload:
fern docs devThe local server lets you see exactly how pages will look on the live site, including navigation, version dropdowns, and custom styling.
The Fern site supports a version dropdown in the UI. Each version is defined by:
- A navigation file (
fern/versions/vX.Y.Z.yml) — sidebar structure pointing to version-specific pages (on thedocs-websitebranch). - A pages directory (
fern/pages-vX.Y.Z/) — frozen snapshot of the markdown content at release time (on thedocs-websitebranch). - An entry in
fern/docs.yml— tells Fern about the version's display name, slug, and config path.
| Version | Display Name | Slug | Description |
|---|---|---|---|
| Latest | Latest (vX.Y.Z) |
/ |
Default version; points to the newest release |
| Stable releases | vX.Y.Z |
vX.Y.Z |
Immutable snapshots |
| Dev | dev |
dev |
Tracks main; updated on every push |
- Latest (default):
docs.nvidia.com/dynamo/ - Specific version:
docs.nvidia.com/dynamo/v0.8.1/ - Dev:
docs.nvidia.com/dynamo/dev/
Simply push a semver tag:
git tag v0.9.0
git push origin v0.9.0The release-version job in fern-docs.yml handles everything else
automatically.
When a page's URL changes — moved to a different section, or its nav label
renamed — add a redirect to the redirects: list in fern/docs.yml so existing
links keep working. The key rule is scope the redirect to the version whose
nav actually changed, and for everyday authoring that is always dev:
-
A
mainedit todocs/index.ymlonly regenerates thedevnav. So a page you move or rename onmainchanges only its/dynamo/dev/<old>URL. Add a single dev-scoped rule:- source: "/dynamo/dev/<old-path>" destination: "/dynamo/dev/<new-path>"
-
Do not add unversioned (
/dynamo/<old>) or/dynamo/latest/<old>redirects for that same move. The unversioned root and/latest/both resolve to Latest (slug/) — a frozen snapshot of the newest release thatmainedits never touch — so the old path keeps serving there. Alatest/unversioned redirect would hijack a still-working URL and send it to a<new-path>that does not exist in Latest until the next release re-snapshots it. -
Pinned versions (
/dynamo/vX.Y.Z/...) are immutable and never need new redirects after the fact.
A page reachable in Latest therefore keeps its old URL at the unversioned and
/latest/ prefixes after a main-only move — only the dev prefix moves, and
only the dev redirect is needed.
┌─────────────────────────────────────────────────────────────────────┐
│ CONTINUOUS (dev) │
│ │
│ Developer pushes to main │
│ │ │
│ ▼ │
│ docs/** changed? ── No ──▶ (nothing happens) │
│ │ │
│ Yes │
│ │ │
│ ▼ │
│ sync-dev job: │
│ 1. Copy docs/ content → fern/pages/ on docs-website branch │
│ 2. Copy fern/ configs → fern/ on docs-website branch │
│ 3. Convert GitHub callouts → Fern admonitions │
│ 4. Preserve version list from docs-website's docs.yml │
│ 5. Commit + push to docs-website │
│ 6. fern generate --docs (publishes to Fern) │
│ │ │
│ ▼ │
│ Live on docs.nvidia.com/dynamo/dev/ within minutes │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ VERSION RELEASE │
│ │
│ Maintainer pushes vX.Y.Z tag │
│ │ │
│ ▼ │
│ release-version job: │
│ 1. Validate tag format (vX.Y.Z only) │
│ 2. Check out the tag and docs-website │
│ 3. Build pages-vX.Y.Z/ from the tagged docs/ │
│ 4. Rewrite GitHub links (tree/main → tree/vX.Y.Z) │
│ 5. Convert callouts once with the tagged converter │
│ 6. Build the version nav from the tagged docs/index.yml │
│ 7. Update fern/docs.yml (insert version, set as default) │
│ 8. Validate tagged inventory, nav targets, and Fern config │
│ 9. Commit + push to docs-website │
│ 10. fern generate --docs (publishes to Fern) │
│ │ │
│ ▼ │
│ New version visible in dropdown at docs.nvidia.com/dynamo/ │
└─────────────────────────────────────────────────────────────────────┘
| Secret | Purpose |
|---|---|
FERN_TOKEN |
Authentication token for fern generate --docs. Required for publishing. Stored in GitHub repo secrets. |
- Edit files in
docs/on a feature branch. - If adding a new page, add its entry in
docs/index.yml. - Open a PR — linting runs automatically.
- Merge — sync + publish happens automatically.
- Create a directory under
docs/(e.g.,docs/new-section/). - Add markdown files for each page.
- Add a new
- section:block indocs/index.ymlwith the desired hierarchy.
git tag v1.0.0
git push origin v1.0.0That's it. The workflow snapshots the current dev docs, creates the version config, and publishes.
Go to Actions → Fern Docs → Run workflow:
- Leave tag empty to trigger a dev sync.
- Enter a tag (e.g.,
v0.9.0) to trigger a version release.
- Check the Actions tab for the failed
Fern Docsworkflow run. - Common issues:
- Broken links: Fix the links flagged by
fern docs broken-links. - Invalid YAML: Check
fern/docs.ymlordocs/index.ymlsyntax. - Expired
FERN_TOKEN: Rotate the token in repo secrets. - Duplicate version: The tag was already released; check
docs-websitefor existingfern/pages-vX.Y.Z/directory.
- Broken links: Fix the links flagged by