AI Web FeedsAI Web FeedsAI source feeds for humans and agents
Features
Documentation

Link Validation

Ensure all links in your documentation are correct and working

Source: apps/web/content/docs/features/link-validation.mdx

Automatically validate all links in your documentation to ensure they're correct and working.

Overview

Link validation uses next-validate-link to check:

Internal Links Links between documentation pages

Anchor Links Links to headings within pages

MDX Components Links in Cards and other components

Relative Paths File path references

Features

  • Automatic scanning - Finds all links in MDX files
  • Heading validation - Checks anchor links to headings
  • Component support - Validates links in MDX components
  • Relative paths - Checks file references
  • Exit codes - CI/CD friendly error reporting
  • Detailed errors - Shows exact location of broken links

Quick Start

Run Validation

pnpm lint:links

Uses the Node.js/tsx runtime (no additional installation required).

# Install Bun first (if not already installed)
curl -fsSL https://bun.sh/install | bash

# Run with Bun
pnpm lint:links:bun

Uses the Bun runtime for faster execution.

This will scan all documentation files and validate:

  • Links to other documentation pages
  • Anchor links to headings
  • Links in Card components
  • Relative file paths

Expected Output

All links valid:

🔍 Scanning URLs and validating links...

✅ All links are valid!

Broken links found:

🔍 Scanning URLs and validating links...

❌ /Users/.../content/docs/index.mdx
  Line 25: Link to /docs/invalid-page not found

❌ Found 1 link validation error(s)

How It Works

File Structure

apps/web/
├── bunfig.toml                # Bun runtime configuration (for Bun)
├── scripts/
│   ├── lint.ts               # Validation script (Bun runtime)
│   ├── lint-node.mjs         # Validation script (Node.js runtime)
│   └── preload.ts            # MDX plugin loader (for Bun)
└── package.json              # Scripts configuration

Validation Script

The scripts/lint-node.mjs file runs with tsx/Node.js:

scripts/lint-node.mjs
import {
  printErrors,
  scanURLs,
  validateFiles,
} from 'next-validate-link';
import { loader } from 'fumadocs-core/source';
import { createMDXSource } from 'fumadocs-mdx';
import { map } from '@/.map';

const source = loader({
  baseUrl: '/docs',
  source: createMDXSource(map),
});

async function checkLinks() {
  const scanned = await scanURLs({
    preset: 'next',
    populate: {
      'docs/[[...slug]]': source.getPages().map((page) => ({
        value: { slug: page.slugs },
        hashes: getHeadings(page),
      })),
    },
  });

  const errors = await validateFiles(await getFiles(), {
    scanned,
    markdown: {
      components: {
        Card: { attributes: ['href'] },
      },
    },
    checkRelativePaths: 'as-url',
  });

  printErrors(errors, true);

  if (errors.length > 0) {
    process.exit(1);
  }
}

The scripts/lint.ts file runs with Bun runtime:

scripts/lint.ts
import {
  type FileObject,
  printErrors,
  scanURLs,
  validateFiles,
} from 'next-validate-link';
import type { InferPageType } from 'fumadocs-core/source';
import { source } from '@/lib/source';

async function checkLinks() {
  const scanned = await scanURLs({
    preset: 'next',
    populate: {
      'docs/[[...slug]]': source.getPages().map((page) => ({
        value: { slug: page.slugs },
        hashes: getHeadings(page),
      })),
    },
  });

  const errors = await validateFiles(await getFiles(), {
    scanned,
    markdown: {
      components: {
        Card: { attributes: ['href'] },
      },
    },
    checkRelativePaths: 'as-url',
  });

  printErrors(errors, true);

  if (errors.length > 0) {
    process.exit(1);
  }
}

Requires Bun preload setup (see below).

Bun Runtime Loader

Only required if using the Bun runtime (pnpm lint:links:bun). The default Node.js version doesn't need this.

The scripts/preload.ts enables MDX processing in Bun:

scripts/preload.ts
import { createMdxPlugin } from "fumadocs-mdx/bun";

Bun.plugin(createMdxPlugin());

Bun Configuration

Only required for Bun runtime. Not needed for default Node.js execution.

The bunfig.toml loads the preload script:

bunfig.toml
preload = ["./scripts/preload.ts"]

What Gets Validated

Links to other documentation pages:

[Getting Started](/docs)
[PDF Export](/docs/features/pdf-export)
[Testing Guide](/docs/guides/testing)

Links to headings within pages:

[Quick Start](#quick-start)
[Configuration](#configuration)

Links in special components:

<Card href="/docs/features/rss-feeds" />
<Card href="/docs/guides/quick-reference" />

Relative Paths

File references:

[Scripts Documentation](./scripts/README.md)
[Source Code](../../packages/ai_web_feeds/src)

CI/CD Integration

GitHub Actions

Add to your workflow:

.github/workflows/validate.yml
name: Validate Links

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  validate-links:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest

      - name: Install dependencies
        run: pnpm install

      - name: Validate links
        run: pnpm lint:links

Exit Codes

The script exits with appropriate codes:

  • 0 - All links valid ✅
  • 1 - Broken links found ❌

Customization

Add More Components

Validate links in additional MDX components:

scripts/lint.ts
markdown: {
  components: {
    Card: { attributes: ['href'] },
    CustomCard: { attributes: ['link', 'url'] },
    Button: { attributes: ['href'] },
  },
}

Custom Validation Rules

Add custom validation logic:

scripts/lint.ts
const errors = await validateFiles(await getFiles(), {
  scanned,
  markdown: {
    components: {
      Card: { attributes: ["href"] },
    },
  },
  checkRelativePaths: "as-url",

  // Custom filter
  filter: (file) => {
    // Skip draft files
    return !file.data?.draft;
  },
});

Exclude Patterns

Skip certain files or paths:

scripts/lint.ts
async function getFiles(): Promise<FileObject[]> {
  const allPages = source.getPages();

  // Filter out test files
  const pages = allPages.filter((page) => !page.absolutePath.includes("/test/"));

  const promises = pages.map(
    async (page): Promise<FileObject> => ({
      path: page.absolutePath,
      content: await page.data.getText("raw"),
      url: page.url,
      data: page.data,
    }),
  );

  return Promise.all(promises);
}

Common Issues

False Positives

Some links may be valid but flagged as errors:

External Links

<!-- External links are not validated by default -->

[GitHub](https://github.com/user/repo)

Dynamic Routes

<!-- May need manual configuration for complex routes -->

[User Profile](/users/[id])

API Routes

<!-- API routes may not be scanned -->

[Search API](/api/search)

Bun Not Installed

The default pnpm lint:links command uses Node.js/tsx and doesn't require Bun.

If you want to use the faster Bun runtime, install it:

curl -fsSL https://bun.sh/install | bash

Then use: pnpm lint:links:bun

Script Errors

If the script fails to run:

# Clear cache
rm -rf .next/
rm -rf node_modules/
pnpm install

# Verify Bun is installed
bun --version

# Run with verbose output
DEBUG=* pnpm lint:links

Best Practices

1. Run Before Commits

Add to your pre-commit hook:

.husky/pre-commit
#!/bin/sh
pnpm lint:links

2. Validate on Build

Add to build process:

package.json
{
  "scripts": {
    "build": "pnpm lint:links && next build"
  }
}

3. Regular Checks

Run validation regularly:

# Daily cron job
0 0 * * * cd /path/to/project && pnpm lint:links

Keep a consistent link style:

<!-- Good: Absolute paths -->

[Features](/docs/features/pdf-export)

<!-- Avoid: Relative paths for internal links -->

[Features](../features/pdf-export)

Link to specific sections:

[Configuration Section](/docs/features/rss-feeds#configuration)

Testing

Manual Test

Create a broken link to test:

content/docs/test.mdx
---
title: Test Page
---

This link is broken: [Invalid Page](/docs/does-not-exist)

Run validation:

pnpm lint:links

Expected output:

❌ /Users/.../content/docs/test.mdx
  Line 6: Link to /docs/does-not-exist not found
This anchor is broken: [Missing Section](#does-not-exist)
<Card href="/docs/invalid-page" />

Performance

Optimization Tips

  1. Cache Results

    • Validation results can be cached between runs
    • Only re-validate changed files
  2. Parallel Processing

    • Script processes files in parallel
    • Scales with CPU cores
  3. Incremental Validation

    • Only validate modified files in CI
    • Use git diff to find changed files

Benchmark

Typical validation times:

PagesTime
10~2s
50~5s
100~10s
500~30s

External Resources