npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mdx-validate

v0.2.0

Published

Catches broken content before your readers do: links that go nowhere, pages that won't compile, and frontmatter your code can no longer read. A pre-deploy check for MDX sites.

Downloads

190

Readme

mdx-validate

Catches broken content before your readers do: links that go nowhere, pages that will not load, images that were never created, and frontmatter your code can no longer read.

The Problem

Some of your content is wired to other things. A link is wired to another page. The body of a page is wired to whatever renders it. A frontmatter field is wired to the code that reads it by name. An image tag is wired to a file on disk.

Any of those wires can break. The painful part is that they break quietly. The page does not crash and nothing turns red. It just ships wrong, and you find out from a reader, or from Google Search Console, weeks later.

  • A page gets renamed, and every old link to it now leads to a dead end.
  • Someone leaves a stray character in a page, and that one page stops loading entirely.
  • Someone renames a frontmatter field, and the code that built that part of the page can no longer find it, so the page ships with a blank section.

A spell-checker or a style linter cannot catch any of this, because the words still look fine. The only way to catch a broken wire is to check both ends of it. That is the whole job of this tool: it checks both ends of every wire in your content, before you deploy.

What it checks

| Check | In plain words | The silent failure it prevents | | --- | --- | --- | | Links resolve | Every internal link points to a page you actually have, whether it lives in the body or in a frontmatter field like relatedGuides | A renamed or deleted page leaves dead links and dead "related reading" cards | | Pages render | Every page actually compiles | One stray character takes a page down at render time | | Frontmatter parses | The settings block at the top of each file is valid | A typo up top breaks the build | | Required fields present | The fields every page must carry are there and not empty | A page ships without a title or description and nobody notices | | Fields match | Each field your code reads is named and shaped the way the code expects | You rename a field, the code cannot find it, and that part of the page ships blank | | Assets exist | Every referenced image or file is actually on disk | A path nobody ever created ships as a broken image |

The first four are the obvious ones. "Fields match" is the one nothing else catches, and it is the reason this tool exists.

Here is the bug that made me build this. A site I run keeps each guide's FAQs in its frontmatter, and the code turns that list into two things: the FAQ section readers see, and the hidden markup that makes those questions show up directly in Google results. The code expected each entry to be labeled q and a. One day a batch of translated guides arrived labeled question and answer instead. Nothing crashed. The guides looked perfect in the editor and in preview. But every FAQ section shipped empty, and the questions quietly fell out of Google. I did not notice for weeks, until Google Search Console reported the markup had broken across a dozen pages.

A spell-checker would not catch it. A link-checker would not catch it. The labels were simply wrong, and only the code knew which labels were right. So this tool lets you write the right labels down once, "the faq field must have q and a," and it catches the mismatch the moment it appears, before anything ships. You decide which fields matter and what shape they take, and the tool holds your content to it.

Why these checks exist

That FAQ bug was not a one-off. Each check here is in the tool because something like it broke in production on a real multi-language content site:

  • A leftover review note took whole pages down. A translator left an HTML comment (<!-- check this -->) in a guide. MDX does not allow HTML comments, so the entire guides listing for two languages returned a 500. (Pages render.)
  • Renamed guides left dead links across five languages. Internal links kept pointing at slugs that no longer existed, and they 404'd for weeks, burning crawl budget. (Links resolve.)
  • A "related guides" list pointed at pages that were long gone. The slugs lived in frontmatter, not the body, so no link checker ever saw them; the widget just rendered dead cards. (Links resolve.)
  • A "HowTo" with no steps quietly became a plain article. A page declared schema: HowTo but the steps were dropped in translation, so the Google rich result silently downgraded. An empty steps list downgrades exactly the same way, so the tool treats both as failures. (Fields match.)
  • A batch of pages referenced images that never existed. The filenames looked plausible, the pages compiled, and every one of them shipped a broken image tile. (Assets exist.)
  • A stray quote in the frontmatter broke the build. An unescaped " inside a YAML string crashed the parser. (Frontmatter parses.)

Demo

The demo runs against examples/content/, which deliberately contains broken files so you can see what a catch looks like. It exits non-zero on purpose:

mdx-validate: FAIL. 8 error(s) across 7 of 8 file(s).

examples/content/broken-faq.mdx
  [SHAPE] faq[0] is missing q, a (it has: question, answer); your code reads { q, a } and will silently drop this entry

examples/content/broken-related.mdx
  [BROKEN-REF] frontmatter "relatedGuides" references slug(s) with no matching content: guide-that-was-deleted

examples/content/dead-link.mdx
  [BROKEN-LINK] links to "/guides/this-page-was-renamed" but no content resolves to slug "this-page-was-renamed"

examples/content/howto-empty-steps.mdx
  [SHAPE-EMPTY] "steps" has 0 item(s) but at least 1 of { name, text } required here; an empty list ships the same blank section as a missing field

examples/content/howto-no-steps.mdx
  [SHAPE-MISSING] "steps" is required here but missing (expected an array of { name, text })

examples/content/html-comment.mdx
  [HTML-COMMENT] body contains an HTML comment <!-- ... -->; MDX rejects it (use {/* ... */} or remove)
  [MDX-COMPILE] Unexpected character `!` (U+0021) before name, expected a character that can start a name, such as a letter, `$`, or `_` (note: to create a comment in MDX, use `{/* text */}`)

examples/content/missing-image.mdx
  [MISSING-ASSET] references "/images/guides/registration-desk-hero.jpg" but no file exists at examples/public/images/guides/registration-desk-hero.jpg

The one file that passes, good-guide.mdx, is not listed. Clean files stay quiet. It is worth a look anyway: it contains a locale-prefixed link (/vi/guides/...) that resolves and an image path containing /guides/ that the link check correctly leaves alone.

Quick Start

See it work:

git clone https://github.com/hwajongpark/mdx-validate
cd mdx-validate
npm install
npm run demo

Use it on your own content:

npm install --save-dev mdx-validate

# write a starter mdx-validate.config.json into your project root
npx mdx-validate --init

# edit the config to match your content, then:
npx mdx-validate

Wire it into your build so a broken page fails the deploy, not the reader:

{
  "scripts": {
    "prebuild": "mdx-validate"
  }
}

Configuration

One mdx-validate.config.json at your project root. npx mdx-validate --init writes a starter; the included examples/mdx-validate.config.example.json is the demo config with every option in use.

{
  "contentDir": "content",
  "extensions": [".mdx"],
  "requiredFrontmatter": ["title", "description"],
  "shapeContracts": [
    { "field": "faq", "itemShape": ["q", "a"] },
    { "field": "sources", "itemShape": ["label", "url"] },
    { "field": "steps", "itemShape": ["name", "text"], "when": { "field": "schema", "equals": "HowTo" } }
  ],
  "internalLink": {
    "pattern": "(?<![a-z0-9-])/(?:[a-z]{2,3}/)?guides/([a-z0-9-]+)(?![a-z0-9-])",
    "resolveTargetsFrom": "content",
    "targetExtensions": [".mdx"],
    "extraValidSlugs": [],
    "frontmatterFields": ["relatedGuides"]
  },
  "assets": {
    "pattern": "(/images/[A-Za-z0-9/._-]+)",
    "resolveFrom": "public"
  }
}
  • contentDir: the folder to check. No glob needed; it walks the tree.
  • requiredFrontmatter: fields that must be present and not empty.
  • shapeContracts: the "fields match" check. Each line says "this field must be a list of items with these keys." Add "when" to make it conditional, so steps is only required when schema is HowTo. A conditional contract must carry at least one item once its condition matches: an empty array ships the same blank section as a missing field, so it fails too. Override with "minItems" (set 0 to allow empty, or a higher floor if your code needs one).
  • internalLink: a pattern with one capture group for the slug, and where to find valid slugs (the filenames of your content). The pattern above is boundary-guarded: the lookbehind keeps it from matching inside longer paths like /images/guides/photo.jpg, and the optional [a-z]{2,3}/ group also catches locale-prefixed links like /vi/guides/... on multi-language sites. extraValidSlugs covers pages served by a redirect instead of a file. frontmatterFields lists frontmatter fields that hold slugs (a string or an array of strings, like relatedGuides); they are checked against the same slug set as body links, because a dead slug there ships a dead widget card without ever rendering a visible link.
  • assets (optional): a pattern with one capture group for a site-absolute asset path, and the folder those paths resolve against (for most sites, public). Every captured path must exist on disk.

CLI flags: --config <path> points at a config somewhere else; --init writes a starter config and exits.

How It Works

It checks both ends of every wire. A link is only good if the page it points to exists. A field is only good if it is shaped the way the code that reads it expects. An image is only good if the file is on disk. The tool knows both ends and compares them.

You define what matters, not the tool. Your fields and shapes are yours to declare. The tool has no opinion about your content, only that your content matches the rules you wrote down. And it holds your config to the same standard: a malformed contract is reported as a config error up front, not a crash halfway through.

It compiles the real MDX, it does not guess. The render check runs the actual MDX compiler, so it catches anything that would throw on a real page, not just patterns a regex anticipated.

It reports, you fix. It finds the break and tells you exactly where. Fixing is your call, because sometimes a change is intentional. It never rewrites your content.

Almost no moving parts. It walks folders with the standard library, reads frontmatter with gray-matter, and compiles with @mdx-js/mdx. That is the entire toolchain. Exit codes are built for CI: 0 clean, 1 something broke, 2 a config or internal error.

What It Does Not Do

  • It is not a style or spelling checker. For formatting and prose, use a style linter or Prettier. This checks that things are wired correctly, not that they read well.
  • It does not check that external websites are up. It only checks that your own internal links point to content you have, and that your own asset files exist.
  • It does not auto-fix. It shows you every break; you decide the fix.

Contributing

Contributions are welcome, and the most useful kind is a class of silent break this tool does not yet catch. If you have hit one, open an issue describing it, or a pull request. The fastest way to land a fix is a reproduction in examples/content/: a small broken file plus the catch you expected, with a matching case in test/. Bug reports and false positives are welcome too. npm test runs the suite.

License

MIT