AI Web FeedsAIWebFeeds
Features

Link Validation

Ensure all links in your documentation are correct and working

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