Skip to navigation

Preview changes

View as Markdown

Fern offers two ways to preview documentation changes:

Prerequisites

Install the following:

  • Node.js version 22 or higher
  • The Fern CLI
  • pnpm, available on your PATH. fern docs dev uses pnpm internally to install the dependencies it needs to render your preview (for example, esbuild), so it must be installed globally even if your project uses npm or yarn.

Local development

Run a local preview server to view documentation changes instantly with hot reload. Offline access is available after the first online run.

# Start preview server (from directory containing fern folder)
fern docs dev
# Or use a custom port
fern docs dev --port 3002

Your documentation will be available at http://localhost:3000 (default) or the port you specified. If you attempt to run Fern on a port that’s already in use, it will use the next available port.

Some features are disabled in local development, including search, SEO (favicon, auto-generated meta tags, etc.), and authentication.

On Windows, fern docs dev requires long path support to be enabled.

To enable long path support, run the following command in an elevated PowerShell prompt, then restart your terminal:

New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 -PropertyType DWORD -Force

If you can’t enable long path support, use Windows Subsystem for Linux (WSL) to run fern docs dev in a Linux environment instead.

Generate shareable preview URLs to review and collaborate on documentation changes before publishing. Preview links aren’t indexed by search engines and don’t expire.

By default, each run generates a new URL with a unique UUID. The --id flag creates a stable, named preview link — rerunning with the same --id updates the existing preview in place.

# Generate a preview link with a unique URL
fern generate --docs --preview
# Or use --id for a stable, named preview link
fern generate --docs --preview --id my-feature
Example output
# Without --id (unique UUID each time)
[docs]: Published docs to https://fern-preview-c973a36e-337b-44f5-ab83-aab.docs.buildwithfern.com/learn
# With --id my-feature (stable URL)
[docs]: Published docs to https://fern-preview-my-feature.docs.buildwithfern.com/learn

When a preview with the same --id already exists, Fern prompts you to confirm the overwrite. This is skipped automatically in GitHub Actions, but for other CI environments (e.g., Azure Pipelines), use --force to skip the confirmation.

fern generate --docs --preview --id my-feature --force

If your docs use role-based access control, a role selector on the preview link lets you view the site as a specific viewer and verify role-gated visibility before publishing.

Preview links persist indefinitely — Fern doesn’t auto-expire them. Any organization member can use fern docs preview list to see active previews and fern docs preview delete to remove them when they’re no longer needed. To clean up previews automatically when PRs merge, set up a GitHub Actions workflow.

Automate with GitHub Actions

You can use a GitHub Actions workflow to automatically generate a preview URL when a pull request is opened. By passing --id with the branch name, every push to the same PR updates the same preview URL instead of creating a new one. The workflow posts a comment on the PR with the preview link and direct links to every page changed in the PR, so reviewers can jump straight to affected pages.

GitHub Actions bot comment on a pull request showing a named preview URL and direct links to changed documentation pages

If you set up your site using the guided UI or CLI quickstart, this workflow is automatically included in your repository. Otherwise, add it manually using the examples below.

These workflows require a FERN_TOKEN repository secret. If you used the guided workflow, this secret is added automatically. Otherwise, generate an API key from the API keys page in the Dashboard or by running fern token in your terminal, then add it in your repository’s Settings > Secrets and variables > Actions with the name FERN_TOKEN.

You may need to re-run preview builds for any PRs that were opened before you configured the FERN_TOKEN.

.github/workflows/preview-docs.yml
name: Preview Docs
on:
pull_request:
types: [opened, synchronize, ready_for_review]
branches:
- main
jobs:
run:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Fern CLI
uses: fern-api/setup-fern-cli@v1
- name: Generate preview URL
id: generate-docs
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
HEAD_REF: ${{ github.head_ref }}
run: |
OUTPUT=$(fern generate --docs --preview --id "$HEAD_REF" 2>&1) || true
echo "$OUTPUT"
URL=$(echo "$OUTPUT" | grep -oP 'Published docs to \K.*(?= \()')
echo "preview_url=$URL" >> $GITHUB_OUTPUT
echo "Preview URL: $URL"
- name: Get page links for changed MDX files
id: page-links
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
run: |
PREVIEW_URL="${{ steps.generate-docs.outputs.preview_url }}"
CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- '*.mdx' 2>/dev/null || echo "")
if [ -z "$CHANGED_FILES" ] || [ -z "$PREVIEW_URL" ]; then
echo "page_links=" >> $GITHUB_OUTPUT; exit 0
fi
BASE_URL=$(echo "$PREVIEW_URL" | grep -oP 'https?://[^/]+')
FILES_PARAM=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//')
RESPONSE=$(curl -sf -H "FERN_TOKEN: $FERN_TOKEN" "${PREVIEW_URL}/api/fern-docs/get-slug-for-file?files=${FILES_PARAM}" 2>/dev/null) || {
echo "page_links=" >> $GITHUB_OUTPUT; exit 0
}
PAGE_LINKS=$(echo "$RESPONSE" | jq -r --arg url "$BASE_URL" \
'.mappings[] | select(.slug != null) | "- [\(.slug)](\($url)/\(.slug))"')
if [ -n "$PAGE_LINKS" ]; then
{ echo "page_links<<EOF"; echo "$PAGE_LINKS"; echo "EOF"; } >> $GITHUB_OUTPUT
else
echo "page_links=" >> $GITHUB_OUTPUT
fi
- name: Create comment content
run: |
echo ":herb: **Preview your docs:** <${{ steps.generate-docs.outputs.preview_url }}>" > comment.md
if [ -n "${{ steps.page-links.outputs.page_links }}" ]; then
echo "" >> comment.md
echo "Here are the markdown pages you've updated:" >> comment.md
echo "${{ steps.page-links.outputs.page_links }}" >> comment.md
fi
- name: Post PR comment
uses: thollander/actions-comment-pull-request@v2.4.3
with:
filePath: comment.md
comment_tag: preview-docs
mode: upsert

If your repository accepts contributions from forks, use pull_request_target instead of pull_request to allow the workflow to access your FERN_TOKEN secret:

.github/workflows/preview-docs.yml
name: Preview Docs
on:
pull_request_target:
types: [opened, synchronize, ready_for_review]
branches:
- main
jobs:
run:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout PR
run: |
git fetch origin pull/${{ github.event.pull_request.number }}/head:pr-${{ github.event.pull_request.number }}
git checkout pr-${{ github.event.pull_request.number }}
- name: Setup Fern CLI
uses: fern-api/setup-fern-cli@v1
- name: Generate preview URL
id: generate-docs
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
HEAD_REF: ${{ github.head_ref }}
run: |
OUTPUT=$(fern generate --docs --preview --id "$HEAD_REF" 2>&1) || true
echo "$OUTPUT"
URL=$(echo "$OUTPUT" | grep -oP 'Published docs to \K.*(?= \()')
echo "preview_url=$URL" >> $GITHUB_OUTPUT
echo "Preview URL: $URL"
- name: Get page links for changed MDX files
id: page-links
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
run: |
PREVIEW_URL="${{ steps.generate-docs.outputs.preview_url }}"
CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- '*.mdx' 2>/dev/null || echo "")
if [ -z "$CHANGED_FILES" ] || [ -z "$PREVIEW_URL" ]; then
echo "page_links=" >> $GITHUB_OUTPUT; exit 0
fi
BASE_URL=$(echo "$PREVIEW_URL" | grep -oP 'https?://[^/]+')
FILES_PARAM=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//')
RESPONSE=$(curl -sf -H "FERN_TOKEN: $FERN_TOKEN" "${PREVIEW_URL}/api/fern-docs/get-slug-for-file?files=${FILES_PARAM}" 2>/dev/null) || {
echo "page_links=" >> $GITHUB_OUTPUT; exit 0
}
PAGE_LINKS=$(echo "$RESPONSE" | jq -r --arg url "$BASE_URL" \
'.mappings[] | select(.slug != null) | "- [\(.slug)](\($url)/\(.slug))"')
if [ -n "$PAGE_LINKS" ]; then
{ echo "page_links<<EOF"; echo "$PAGE_LINKS"; echo "EOF"; } >> $GITHUB_OUTPUT
else
echo "page_links=" >> $GITHUB_OUTPUT
fi
- name: Create comment content
run: |
echo ":herb: **Preview your docs:** <${{ steps.generate-docs.outputs.preview_url }}>" > comment.md
if [ -n "${{ steps.page-links.outputs.page_links }}" ]; then
echo "" >> comment.md
echo "Here are the markdown pages you've updated:" >> comment.md
echo "${{ steps.page-links.outputs.page_links }}" >> comment.md
fi
- name: Post PR comment
uses: thollander/actions-comment-pull-request@v2.4.3
with:
filePath: comment.md
comment_tag: preview-docs
mode: upsert

To clean up preview links automatically after a PR is merged, add this workflow alongside the one above. It calls fern docs preview delete with the PR’s branch name as the --id, matching the identifier used when the preview was generated.

.github/workflows/cleanup-preview.yml
name: Clean up preview links
on:
pull_request:
types: [closed]
jobs:
cleanup:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Fern CLI
uses: fern-api/setup-fern-cli@v1
- name: Delete preview deployment
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
run: |
echo "Deleting preview for branch: ${{ github.head_ref }}"
fern docs preview delete --id "${{ github.head_ref }}" || echo "Preview deletion returned non-zero — it may already be gone"