> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://buildwithfern.com/learn/llms.txt. # Preview changes > Learn how to preview documentation changes with Fern using local development servers and shareable preview links before publishing. Fern offers two ways to preview documentation changes: * **[Local development](#local-development)**: Fast iteration with hot reload, best for active development * **[Preview links](#preview-links)**: Shareable URLs for reviews and collaboration #### Prerequisites Install the following: * Node.js version 22 or higher * [The Fern CLI](/learn/cli-api-reference/cli-reference/overview#install-fern-cli) * [pnpm](https://pnpm.io/installation), 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](/learn/cli-api-reference/cli-reference/docs-commands#fern-docs-dev) to view documentation changes instantly with hot reload. Offline access is available after the first online run. ```bash # 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](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation#enable-long-paths-in-windows-10-version-1607-and-later) to be enabled. To enable long path support, run the following command in an elevated PowerShell prompt, then restart your terminal: ```powershell 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)](https://learn.microsoft.com/en-us/windows/wsl/install) to run `fern docs dev` in a Linux environment instead. ### Common errors #### `No docs.yml file found. Please make sure your project has one.` `fern docs dev` and `fern generate --docs` must be run from a directory that contains a [`fern/` folder](/learn/docs/getting-started/project-structure) with a `docs.yml` inside. Change into your project directory, or add a `docs.yml`. #### `Failed to install required package due to error: Command failed with exit code 1: pnpm i esbuild` `fern docs dev` shells out to pnpm to install the dependencies it needs to render your preview. Install [pnpm](https://pnpm.io/installation) globally and make sure it's on your `PATH`, then rerun `fern docs dev`. Your project's package manager (npm, yarn, etc.) doesn't need to change. If the same command also prints `[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild`, run `pnpm approve-builds` and approve `esbuild` so pnpm allows its post-install scripts to run. #### `Broken link to /some/path (resolved path: ...)` `fern check` reports this when a link in a Markdown page doesn't resolve to a real page, anchor, or file in your docs. * For [internal pages](/learn/docs/writing-content/markdown-basics#link-format), use the published URL path from your `docs.yml` config (for example, `/learn/docs/configuration/navigation`) — not a relative path or on-disk file path. * For [images and other assets](/learn/docs/writing-content/markdown-media), use a path relative to the Markdown file. Broken internal links fail by default. Use [`fern generate --docs --no-strict-broken-links`](/learn/cli-api-reference/cli-reference/docs-commands#fern-generate---docs) to downgrade the failure to a warning while you fix them. #### `Invalid URL: /some/path` A Markdown link or `` uses a value that isn't a valid URL. Check for typos, unescaped characters, or missing protocols on external links. #### `Path ./pages/foo.mdx does not exist` A [`path:`](/learn/docs/configuration/navigation) in `docs.yml` points to a file that isn't on disk. Fix the path or create the missing file. Paths are relative to the YAML file that defines them. ## Preview links 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. ```bash # 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`** ```bash 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. ```bash fern generate --docs --preview --id my-feature --force ``` If your docs use [role-based access control](/learn/docs/authentication/features/rbac#preview-as-a-role), a role selector on the preview link lets you view the site as a specific viewer and verify role-gated visibility before publishing. ### Manage preview links Preview links persist indefinitely — Fern doesn't auto-expire them. Any organization member can use [`fern docs preview list`](/learn/cli-api-reference/cli-reference/docs-commands#fern-docs-preview-list) to see active previews and [`fern docs preview delete`](/learn/cli-api-reference/cli-reference/docs-commands#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](#clean-up-preview-links-when-prs-merge). ### 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](/learn/_fern-img/300faf30318245eb6b3fe81a969fd50046f77874a10078cf9c46670f45ae13c5.webp) If you set up your site using the [guided UI](https://dashboard.buildwithfern.com/get-started) or [CLI quickstart](/learn/docs/getting-started/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](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository). If you used the guided workflow, this secret is added automatically. Otherwise, generate an API key from the [API keys](/learn/dashboard/configuration/api-keys) page in the Dashboard or by running [`fern token`](/learn/cli-api-reference/cli-reference/general-commands#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 ```yaml 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<> $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 ``` #### For repositories that accept pull requests from forks 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 ```yaml 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<> $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 ``` #### Clean up preview links when PRs merge To clean up preview links automatically after a PR is merged, add this workflow alongside the one above. It calls [`fern docs preview delete`](/learn/cli-api-reference/cli-reference/docs-commands#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 ```yaml 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" ``` > Learn how to preview documentation changes with Fern using local development servers and shareable preview links before publishing.