---
sidebar-title: Dynamo Docs Guide
---
# How to Build and Publish Dynamo Docs
This document describes the architecture, workflows, and maintenance procedures for the
NVIDIA Dynamo documentation website powered by [Fern](https://buildwithfern.com).
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.
---
## Table of Contents
- [Branch Architecture](#branch-architecture)
- [Directory Layout](#directory-layout)
- [Configuration Files](#configuration-files)
- [GitHub Workflows](#github-workflows)
- [Fern Docs Workflow](#fern-docs-workflow-fern-docsyml)
- [Docs Link Check Workflow](#docs-link-check-workflow-docs-link-checkyml)
- [Content Authoring](#content-authoring)
- [Callout Conversion](#callout-conversion)
- [Running Locally](#running-locally)
- [Version Management](#version-management)
- [How Publishing Works](#how-publishing-works)
- [Common Tasks](#common-tasks)
- [Claude Code Skills](#claude-code-skills)
---
## Claude Code Skills
Three Claude Code skills automate common docs tasks. Invoke them as slash
commands in Claude Code (e.g., `/add-dynamo-docs`) — each skill walks through
the full workflow: creating or editing the markdown file, updating the
navigation in `docs/versions/dev.yml`, and running `fern check` to validate.
| Skill | Description |
|-------|-------------|
| [add-dynamo-docs](/.claude/skills/add-dynamo-docs/SKILL.md) | Add a new page — creates the file with frontmatter, adds the nav entry |
| [rm-dynamo-docs](/.claude/skills/rm-dynamo-docs/SKILL.md) | Remove a page — deletes the file, removes the nav entry, checks for broken links |
| [update-dynamo-docs](/.claude/skills/update-dynamo-docs/SKILL.md) | Update a page — edit content, rename, or move between sections |
---
## Branch Architecture
The documentation system uses a **dual-branch model**:
| Branch | Purpose | Docs directory |
|---|---|---|
| `main` | Source of truth for **dev** (unreleased) documentation | `docs/` |
| `docs-website` | Published documentation including **all versioned snapshots** | `docs/` |
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.
### Why two branches?
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.
---
## Directory Layout
### On `main`
```text
docs/
├── fern.config.json # Fern org + CLI version pin
├── docs.yml # Site configuration (instances, branding, layout)
├── versions/
│ └── dev.yml # Navigation tree for the dev version
├── pages/ # Markdown content (the actual docs)
│ ├── getting-started/
│ ├── guides/
│ ├── kubernetes/
│ ├── reference/
│ └── ...
├── assets/ # Images, fonts, SVGs, logos
├── 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
└── diagrams/ # D2 diagram source files
```
### On `docs-website`
The `docs-website` branch mirrors the above structure, plus versioned snapshots:
```text
docs/
├── 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
└── ... # (other files same as main)
```
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/`.
---
## Configuration Files
### `fern.config.json`
```json
{
"organization": "nvidia",
"version": "3.73.0"
}
```
- **organization**: The Fern organization that owns the docs site.
- **version**: Pins the Fern CLI version used for generation.
### `docs.yml`
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`.
### `versions/dev.yml`
Defines the navigation tree — the sidebar structure of the docs site. Each
entry maps a page title to a markdown file path:
```yaml
navigation:
- section: Getting Started
contents:
- page: Quickstart
path: ../pages/getting-started/quickstart.md
- page: Support Matrix
path: ../pages/reference/support-matrix.md
```
Sections can be nested. Pages can be marked as `hidden: true` to make them
accessible by URL but invisible in the sidebar.
---
## GitHub Workflows
### Fern Docs Workflow (`fern-docs.yml`)
**Location:** `.github/workflows/fern-docs.yml`
This single consolidated workflow handles linting, syncing, versioning, and
publishing. It runs three jobs depending on the trigger:
#### Job 1: Lint (PRs)
**Triggers:** Pull requests that modify `docs/**` files.
**Steps:**
1. `fern check` — validates Fern configuration syntax
2. `fern docs broken-links` — checks for broken internal links
**Purpose:** Catches broken docs before they merge.
#### Job 2: Sync dev (push to `main`)
**Triggers:** Push to `main` that modifies `docs/**` files, or manual
`workflow_dispatch` (with no tag specified).
**Steps:**
1. Checks out both `main` and `docs-website` branches side-by-side
2. Copies from `main` → `docs-website`:
- `docs/pages/` — all markdown content
- `docs/versions/dev.yml` — navigation structure
- `docs/assets/` — images, fonts, SVGs
- `docs/fern.config.json` — Fern config
- `docs/components/` — React components
- `docs/main.css` — custom styles
- `docs/convert_callouts.py` — conversion script
3. Runs `convert_callouts.py` to transform GitHub-style callouts to Fern format
4. Updates `docs.yml` from `main` **while preserving the versions array** from
`docs-website` (uses `yq` to save/restore the versions list)
5. Commits and pushes to `docs-website`
6. Publishes to Fern via `fern generate --docs`
**Symlink trick:** The Fern CLI expects a `fern/` directory. Since docs live in
`docs/`, the workflow creates a symlink `docs/fern → docs/.` (i.e., pointing to
itself) so Fern can find its config files.
#### Job 3: Version Release (tags)
**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:**
1. Validates tag format (must be exactly `vX.Y.Z`, no suffixes like `-rc1`)
2. Checks that the version doesn't already exist (no duplicate snapshots)
3. Creates `docs/pages-vX.Y.Z/` by copying `docs/pages/`
4. Rewrites GitHub links in the snapshot:
- `github.com/ai-dynamo/dynamo/tree/main` → `tree/vX.Y.Z`
- `github.com/ai-dynamo/dynamo/blob/main` → `blob/vX.Y.Z`
5. Runs `convert_callouts.py` on the snapshot
6. Creates `docs/versions/vX.Y.Z.yml` from `dev.yml` with paths updated to
`../pages-vX.Y.Z/`
7. Updates `docs.yml`:
- Inserts new version right after the "dev" entry
- Sets the product's default `path` to the new version
- Updates the "Latest" display-name to `"Latest (vX.Y.Z)"`
8. Commits and pushes to `docs-website`
9. 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.
### Docs Link Check Workflow (`docs-link-check.yml`)
**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](https://lychee.cli.rs/) | 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. |
---
## Content Authoring
### Writing docs on `main`
1. Edit or add markdown files in `docs/pages/`.
2. If adding a new page, add an entry in `docs/versions/dev.yml` to make it
appear in the sidebar navigation.
3. Use standard GitHub-flavored markdown. Callouts (admonitions) should use
GitHub's native syntax — they are automatically converted during sync:
```markdown
> [!NOTE]
> This is a note that will become a Fern `` component.
> [!WARNING]
> This warning will become a Fern `` component.
```
4. Open a PR. The lint jobs (`fern check`, `fern docs broken-links`, lychee,
broken-links-check) run automatically.
5. Once merged to `main`, the sync-dev workflow publishes changes within minutes.
### Assets and images
Place images in `docs/assets/` and reference them with relative paths from your
markdown files:
```markdown

```
### Custom components
React components in `docs/components/` can be used in markdown via MDX. The
`CustomFooter.tsx` renders the NVIDIA footer with legal links and branding.
---
## Callout Conversion
The `docs/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.
### Mapping
| GitHub Syntax | Fern Component |
|---|---|
| `> [!NOTE]` | `` |
| `> [!TIP]` | `` |
| `> [!IMPORTANT]` | `` |
| `> [!WARNING]` | `` |
| `> [!CAUTION]` | `` |
### Usage
```bash
# Convert all files in a directory (recursive, in-place)
python convert_callouts.py --dir docs/pages
# Convert a single file
python convert_callouts.py input.md output.md
# Run built-in tests
python convert_callouts.py --test
```
The conversion happens automatically during the sync-dev and release-version
workflows. Authors never need to run it manually.
---
## Running Locally
You can preview the documentation site on your machine using the
[Fern CLI](https://buildwithfern.com/learn/cli-api/overview). This is useful
for verifying layout, navigation, and content before opening a PR.
### Prerequisites
Install the Fern CLI globally via npm:
```bash
npm install -g fern-api
```
### Create the `fern` symlink
The Fern CLI requires its configuration files to live inside a directory called
`fern/`. In this repo the docs live in `docs/`, so you need to create a symlink
that points `fern` back to the same directory:
```bash
cd docs
ln -s . fern
```
This makes the CLI find `fern/fern.config.json`, `fern/docs.yml`, etc. without
moving any files. The symlink is listed in `.gitignore` and should not be
committed.
### Validate configuration
Run `fern check` to validate that `docs.yml`, `fern.config.json`, and the
navigation files are syntactically correct:
```bash
cd docs
fern check
```
### Check for broken links
Use `fern docs broken-links` to scan all pages for internal links that don't
resolve:
```bash
cd docs
fern docs broken-links
```
This is the same check that runs in CI on every pull request.
### Start a local preview server
Run `fern docs dev` to build the site and serve it locally with hot-reload:
```bash
cd docs
fern docs dev
```
The local server lets you see exactly how pages will look on the live site,
including navigation, version dropdowns, and custom styling.
---
## Version Management
### How versions work
The Fern site supports a version dropdown in the UI. Each version is defined by:
1. **A navigation file** (`docs/versions/vX.Y.Z.yml`) — sidebar structure
pointing to version-specific pages.
2. **A pages directory** (`docs/pages-vX.Y.Z/`) — frozen snapshot of the
markdown content at release time.
3. **An entry in `docs.yml`** — tells Fern about the version's display name,
slug, and config path.
### Version types
| 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 |
### URL structure
- **Latest (default):** `docs.dynamo.nvidia.com/dynamo/`
- **Specific version:** `docs.dynamo.nvidia.com/dynamo/v0.8.1/`
- **Dev:** `docs.dynamo.nvidia.com/dynamo/dev/`
### Creating a new version
Simply push a semver tag:
```bash
git tag v0.9.0
git push origin v0.9.0
```
The `release-version` job in `fern-docs.yml` handles everything else
automatically.
---
## How Publishing Works
```text
┌─────────────────────────────────────────────────────────────────────┐
│ CONTINUOUS (dev) │
│ │
│ Developer pushes to main │
│ │ │
│ ▼ │
│ docs/** changed? ── No ──▶ (nothing happens) │
│ │ │
│ Yes │
│ │ │
│ ▼ │
│ sync-dev job: │
│ 1. Copy docs/pages/, assets/, configs → docs-website branch │
│ 2. Convert GitHub callouts → Fern admonitions │
│ 3. Preserve version list from docs-website's docs.yml │
│ 4. Commit + push to docs-website │
│ 5. 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 pages/ → pages-vX.Y.Z/ │
│ 4. Rewrite GitHub links (tree/main → tree/vX.Y.Z) │
│ 5. Convert callouts in snapshot │
│ 6. Create versions/vX.Y.Z.yml (paths → pages-vX.Y.Z/) │
│ 7. Update 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/ │
└─────────────────────────────────────────────────────────────────────┘
```
### Secrets
| Secret | Purpose |
|---|---|
| `FERN_TOKEN` | Authentication token for `fern generate --docs`. Required for publishing. Stored in GitHub repo secrets. |
---
## Common Tasks
### Update existing documentation
1. Edit files in `docs/pages/` on a feature branch.
2. If adding a new page, add its entry in `docs/versions/dev.yml`.
3. Open a PR — linting runs automatically.
4. Merge — sync + publish happens automatically.
### Add a new top-level section
1. Create a directory under `docs/pages/` (e.g., `docs/pages/new-section/`).
2. Add markdown files for each page.
3. Add a new `- section:` block in `docs/versions/dev.yml` with the desired
hierarchy.
### Release versioned documentation
```bash
git tag v1.0.0
git push origin v1.0.0
```
That's it. The workflow snapshots the current dev docs, creates the version
config, and publishes.
### Manually trigger a sync or release
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.
### Debug a failed publish
1. Check the **Actions** tab for the failed `Fern Docs` workflow run.
2. Common issues:
- **Broken links:** Fix the links flagged by `fern docs broken-links`.
- **Invalid YAML:** Check `docs.yml` or `versions/dev.yml` syntax.
- **Expired `FERN_TOKEN`:** Rotate the token in repo secrets.
- **Duplicate version:** The tag was already released; check `docs-website`
for existing `pages-vX.Y.Z/` directory.