| sidebar-title |
|---|
Dynamo Docs Guide |
This document describes the architecture, workflows, and maintenance procedures for the NVIDIA Dynamo documentation website powered by Fern.
The documentation website is hosted entirely on [Fern](https://buildwithfern.com). CI publishes to `dynamo.docs.buildwithfern.com`; the production domain `docs.dynamo.nvidia.com` is a custom domain alias that points to the Fern-hosted site. There is no separate server — Fern handles 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
├── blogs/ # Blog 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
├── blogs/ # Blog posts (synced from main)
└── assets/ # Images, fonts, SVGs
Each pages-vX.Y.Z/ directory is an immutable copy of pages/ taken at
release time. The corresponding versions/vX.Y.Z.yml file is a copy of
dev.yml with all ../pages/ 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.md
- 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 blogs fromdocs/blogs/ - 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 that the version doesn't already exist (no duplicate snapshots)
- Creates
fern/pages-vX.Y.Z/by copyingfern/pages/ - 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
convert_callouts.pyon the snapshot - Creates
fern/versions/vX.Y.Z.ymlfromdev.ymlwith paths updated to../pages-vX.Y.Z/ - 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)"
- 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. |
- 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.
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.dynamo.nvidia.com/dynamo/ - Specific version:
docs.dynamo.nvidia.com/dynamo/v0.8.1/ - Dev:
docs.dynamo.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.
┌─────────────────────────────────────────────────────────────────────┐
│ 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.dynamo.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 version doesn't already exist │
│ 3. Snapshot fern/pages/ → fern/pages-vX.Y.Z/ │
│ 4. Rewrite GitHub links (tree/main → tree/vX.Y.Z) │
│ 5. Convert callouts in snapshot │
│ 6. Create fern/versions/vX.Y.Z.yml (paths → pages-vX.Y.Z/) │
│ 7. Update fern/docs.yml (insert version, set as default) │
│ 8. Commit + push to docs-website │
│ 9. fern generate --docs (publishes to Fern) │
│ │ │
│ ▼ │
│ New version visible in dropdown at docs.dynamo.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