Introduction

A tool for validating links in Markdown files

Setup

Install it.

npm i next-validate-link

Scan available URLs, it predicts them from your file-system based routing.

import { scanURLs } from 'next-validate-link';

const scanned = await scanURLs({
  preset: 'next'
});

Choose a preset according to your framework:

  • next
  • waku
  • tanstack-start
  • astro
  • nuxt
  • react-router: also pass your router config to the routes option.

Validate Markdown files and print errors.

import { printErrors, validateFiles, readFiles } from 'next-validate-link';

printErrors(
  await validateFiles(await readFiles('content/**/*.{md,mdx}'), {
    scanned,
  }),
  // exit with code 1 if errors detected
  true,
);

The validateFiles function accepts a list of file paths or file objects containing its path and content.

You can run this script during lint process, or before builds.

bun ./lint.ts
You can use other TypeScript executors like tsx.

Configurations

Meta

Specify allowed queries and fragments of a page.

import { scanURLs } from 'next-validate-link';

const scanned = await scanURLs({
  meta: {
    '/': {
      // allowed fragment strings
      hashes: ['fragment'],
      // allowed queries
      queries: [
        {
          search: 'fumadocs',
        },
      ],
    },
  },
});

Static Params

Use populate to populate dynamic routes into static.

import { scanURLs } from 'next-validate-link';

const scanned = await scanURLs({
  populate: {
    // you can generate them too! (e.g. from file system or CMS)
    '(home)/blog/[slug]': [{ value: 'blog-1' }, { value: 'blog-2' }],
    'docs/[...slug]': [
      {
        value: ['hello', 'world'],
        // allowed fragment strings
        hashes: ['fragment'],
        // allowed queries
        queries: [
          {
            search: 'fumadocs',
          },
        ],
      },
    ],
  },
});

Each param object indicates a page, allowed hashes and queries can be specified individually.

Multiple Params

When you have multiple dynamic routes, you need to use an object instead.

import { scanURLs } from 'next-validate-link';

const scanned = await scanURLs({
  populate: {
    '[lang]/blog/[slug]': [
      {
        value: {
          lang: 'en',
          slug: 'blog-1',
        },
      },
      {
        value: {
          lang: 'cn',
          slug: 'blog-1',
        },
      },
    ],
  },
});

External Urls

To validate external urls, enable it.

import { validateFiles, readFiles } from 'next-validate-link';

await validateFiles(await readFiles('content/**/*.{md,mdx}'), {
  scanned,
  checkExternal: true,
});

Relative Paths

To check relative paths such as:

[Test](./another-file.md)

You can specify the checkRelativePaths option, allowed values listed below:

exists

Check if the referenced file exists.

import { readFiles, validateFiles } from "next-validate-link";

await validateFiles(await readFiles('content/**/*.{md,mdx}'), {
  checkRelativePaths: 'exists',
});

as-url

Resolve the file path as URL, you need to specify a function that generates URL based on file path.

import path from "node:path";
import { readFiles, validateFiles } from "next-validate-link";

await validateFiles(await readFiles('content/**/*.{md,mdx}'), {
  checkRelativePaths: 'as-url',
  pathToUrl: (file) => {
    // just an example, it should return a pathname/url, like `/slug/of/page`
    return path.dirname(file);
  },
});

Then, the relative path will be checked as a regular URL.

false (default)

Ignore relative paths.

Relative URLs

By default, it checks relative URLs such as:

[Test](./my-page)

When a relative URL is detected, you need to define either:

  • file.url in input file objects.
  • pathToUrl option.
  • baseUrl option.

Or you can disable the check:

import { readFiles, validateFiles } from "next-validate-link";

await validateFiles(await readFiles('content/**/*.{md,mdx}'), {
  checkRelativeUrls: false,
});

Utilities

readFiles

Read files for given glob patterns, and output a list of file objects.

import { readFiles } from 'next-validate-link';

const files = await readFiles('content/docs/**/*.{md,mdx}');

You can use it in conjunction with validateFiles.

Advanced

pages

By default, it looks for the pages from your file system. You can override the obtained pages by passing an array of urls.

This also allows you to parse available pages from non-Next.js routing.

import { scanURLs } from 'next-validate-link';

const scanned = await scanURLs({
  pages: ['/', '(home)/overview', 'docs/[[...slug]]'],
});