remark plugin to validate links to headings and files in Git repositories
npx @tessl/cli install tessl/npm-remark-validate-links@13.1.0remark-validate-links is a unified remark plugin that validates local markdown links and images in Git repositories. It checks that links point to existing files and headings, working offline for fast and reliable link validation. The plugin integrates with the remark ecosystem and provides Git-aware validation for hosted services like GitHub, GitLab, and Bitbucket.
npm install remark-validate-linksimport remarkValidateLinks from "remark-validate-links";For CommonJS:
const remarkValidateLinks = require("remark-validate-links");Note: This package exports only a default export function - no named exports are available. TypeScript types Options and UrlConfig are defined via JSDoc comments in the source code for TypeScript support, but cannot be imported at runtime.
import remarkValidateLinks from "remark-validate-links";
import { remark } from "remark";
import { read } from "to-vfile";
// Basic usage with default options
const file = await remark()
.use(remarkValidateLinks)
.process(await read("example.md"));
// Usage with configuration options
const file = await remark()
.use(remarkValidateLinks, {
repository: "https://github.com/user/repo",
root: "./docs",
skipPathPatterns: [/\/temp\//, "ignore-me.md"]
})
.process(await read("example.md"));remark-validate-links operates as a remark plugin that:
Creates a remark transformer that validates local links and images in markdown documents.
/**
* Check that markdown links and images point to existing local files and headings in a Git repo.
*
* ⚠️ **Important**: The API in Node.js checks links to headings and files
* but does not check whether headings in other files exist.
* The API in browsers only checks links to headings in the same file.
* The CLI can check everything.
*
* @param {Options | null | undefined} [options] - Configuration (optional)
* @param {FileSet | null | undefined} [fileSet] - File set (optional)
* @returns {Function} Transform function
*/
function remarkValidateLinks(options, fileSet);Usage Examples:
import remarkValidateLinks from "remark-validate-links";
import { remark } from "remark";
// Basic usage
const processor = remark().use(remarkValidateLinks);
// With options
const processor = remark().use(remarkValidateLinks, {
repository: false, // disable repository detection
skipPathPatterns: [/node_modules/]
});
// For CLI usage with file set - enables cross-file validation
const fileSet = new FileSet();
const processor = remark().use(remarkValidateLinks, {}, fileSet);
// When fileSet is provided:
// - Plugin discovers referenced files and adds them for processing
// - Cross-file heading validation becomes available
// - Directory references resolve to readme files (.md, .markdown, .mdown, .mkdn)The plugin validates these types of links:
#heading-id./path/to/file.md, ../other.js./file.md#heading/path/from/repo/root.md (when repository is configured)file.js#L123 (when lines: true)Error Detection:
Configuration object for the plugin.
/**
* Configuration options for the plugin.
*/
interface Options {
/**
* URL to hosted Git (default: detected from Git remote);
* if you're not in a Git repository, you must pass `false`;
* if the repository resolves to something npm understands as a Git host such
* as GitHub, GitLab, or Bitbucket, full URLs to that host (say
* `https://github.com/remarkjs/remark-validate-links/readme.md#install`) are
* checked.
*/
repository?: string | false | null | undefined;
/**
* Path to Git root folder (default: local Git folder);
* if both `root` and `repository` are nullish, the Git root is detected;
* if `root` is not given but `repository` is, `file.cwd` is used.
*/
root?: string | null | undefined;
/**
* List of patterns for *paths* that should be skipped;
* each absolute local path + hash will be tested against each pattern and
* will be ignored if `new RegExp(pattern).test(value) === true`;
* example values are then `/Users/tilde/path/to/repo/readme.md#some-heading`.
*/
skipPathPatterns?: ReadonlyArray<RegExp | string> | null | undefined;
/**
* Config on how hosted Git works (default: detected from repo);
* `github.com`, `gitlab.com`, or `bitbucket.org` work automatically;
* otherwise, pass `urlConfig` manually.
*/
urlConfig?: UrlConfig | null | undefined;
}Configuration for hosted Git services like GitHub, GitLab, and Bitbucket.
/**
* Hosted Git info configuration.
*
* For this repository (`remarkjs/remark-validate-links` on GitHub)
* `urlConfig` looks as follows:
*
* ```js
* {
* // Domain of URLs:
* hostname: 'github.com',
* // Path prefix before files:
* prefix: '/remarkjs/remark-validate-links/blob/',
* // Prefix of headings:
* headingPrefix: '#',
* // Hash to top of markdown documents:
* topAnchor: '#readme',
* // Whether lines in files can be linked:
* lines: true
* }
* ```
*
* If this project were hosted on Bitbucket, it would be:
*
* ```js
* {
* hostname: 'bitbucket.org',
* prefix: '/remarkjs/remark-validate-links/src/',
* headingPrefix: '#markdown-header-',
* lines: false
* }
* ```
*/
interface UrlConfig {
/** Prefix of headings (example: `'#'`, `'#markdown-header-'`) */
headingPrefix?: string | null | undefined;
/** Domain of URLs (example: `'github.com'`, `'bitbucket.org'`) */
hostname?: string | null | undefined;
/** Whether absolute paths (`/x/y/z.md`) resolve relative to a repo */
resolveAbsolutePathsInRepo?: boolean | null | undefined;
/** Whether lines in files can be linked */
lines?: boolean | null | undefined;
/** Path prefix before files (example: `'/remarkjs/remark-validate-links/blob/'`, `'/remarkjs/remark-validate-links/src/'`) */
prefix?: string | null | undefined;
/** Hash to top of readme (example: `#readme`) */
topAnchor?: string | null | undefined;
}Pre-configured services:
headingPrefix: '#', lines: true, topAnchor: '#readme', prefix: '/owner/repo/blob/', resolveAbsolutePathsInRepo: trueheadingPrefix: '#', lines: true, topAnchor: '#readme', prefix: '/owner/repo/blob/'headingPrefix: '#markdown-header-', lines: false, prefix: '/owner/repo/src/'Automatic detection: The plugin uses the hosted-git-info library to automatically detect the Git service type from the repository URL and apply appropriate configuration.
Types used internally by the plugin for reference tracking and validation.
/** Map of file paths to their available headings/anchors */
type Landmarks = Map<string, Map<string, boolean>>;
/** Reference to a file and optional heading */
interface Reference {
/** Absolute path to the referenced file */
filePath: string;
/** Hash/anchor portion of the reference */
hash?: string | undefined;
}
/** VFile from vfile package for file representation */
type VFile = import('vfile').VFile;
/** FileSet from unified-engine package for CLI batch processing */
type FileSet = import('unified-engine').FileSet;
/** Nodes type from mdast package */
type Nodes = import('mdast').Nodes;
/** Resource type from mdast package (link and image nodes) */
type Resource = import('mdast').Resource;
/** Link and image nodes from mdast AST */
type Resources = Extract<Nodes, Resource>;
/** Information about a reference including source context */
interface ReferenceInfo {
/** Source file containing the reference */
file: VFile;
/** The reference details */
reference: Reference;
/** AST nodes that contain this reference */
nodes: ReadonlyArray<Resources>;
}
/** Internal state passed between validation functions */
interface State {
/** Directory of the current file */
base: string;
/** Absolute path to the current file */
path: string;
/** Path to Git root directory */
root?: string | null | undefined;
/** Compiled skip patterns */
skipPathPatterns: ReadonlyArray<RegExp>;
/** URL configuration for the repository */
urlConfig: UrlConfig;
}
/** Plugin constants for error reporting and data storage */
interface Constants {
/** Rule ID for missing file errors */
fileRuleId: 'missing-file';
/** Rule ID for missing heading in other file errors */
headingInFileRuleId: 'missing-heading-in-file';
/** Rule ID for missing heading in current file errors */
headingRuleId: 'missing-heading';
/** Data key for storing landmarks in VFile data */
landmarkId: 'remarkValidateLinksLandmarks';
/** Data key for storing references in VFile data */
referenceId: 'remarkValidateLinksReferences';
/** Source identifier for error messages */
sourceId: 'remark-validate-links';
}Full functionality with platform-specific implementations:
fs.access() to verify file existenceFileSetgit remote -v and git rev-parse --show-cdup commandsskipPathPatternsLimited functionality with browser-safe implementations:
checkFiles() function is a no-op stubfindRepo() function is a no-op stubrepository and root options explicitly#heading links within the same documentThe plugin generates detailed error messages with specific rule IDs:
missing-file: Referenced file does not existmissing-heading: Heading anchor not found in current filemissing-heading-in-file: Heading anchor not found in referenced fileError messages include:
#heading-name" or "Cannot find file path/to/file.md"propose library with edit distance algorithms (70% similarity threshold) to suggest correctionshttps://github.com/remarkjs/remark-validate-links#readmeExample error output:
example.md:5:10-5:32: Cannot find heading for `#non-existent` in `readme.md`; did you mean `#installation`? [missing-heading-in-file](https://github.com/remarkjs/remark-validate-links#readme)# Check single file
npx remark example.md --use remark-validate-links --quiet
# Check multiple files
npx remark . --ext md --use remark-validate-links --quietimport remarkValidateLinks from "remark-validate-links";
import { remark } from "remark";
import { unified } from "unified";
import { reporter } from "vfile-reporter";
import { read } from "to-vfile";
// Process single file
const file = await remark()
.use(remarkValidateLinks)
.process(await read("document.md"));
console.log(reporter(file));
// Process multiple files with unified-engine
import { engine } from "unified-engine";
engine({
processor: remark().use(remarkValidateLinks),
files: ["*.md"],
extensions: ["md"],
pluginPrefix: "remark",
quiet: true
}, (error, code) => {
if (error) throw error;
process.exit(code);
});