Skip to content

MDX pitfalls

The four failures that cost the most time, and what to do instead.

A fenced code block inside a JSX element such as <Card>, <Step> or <Tab> works when it sits on its own lines, with a blank line before and after it. It fails with unparseable-mdx … Expected a closing tag for <Card> when the opening or closing tag shares a line with the fence or with prose. The failure is confusing because the same block works one line higher up.

The simplest way to never hit it is to keep fences at the top level:

mdx
<Steps>
<Step title="Install">Add the dependency.</Step>
<Step title="Build">Run the build.</Step>
</Steps>

…with the command as its own top-level block below:

sh
npm install

When a fence does belong inside a component, give the tags and the fence their own lines. When a build fails on a page you only added a code block to, this is the first thing to check. The exact forms that fail, and the fix for each, are in Fix “Expected a closing tag” for a code block inside a component.

Your frontmatter title is the page’s H1. Writing another one produces a DS-A02 error, because the page would render two competing top-level headings and a screen reader would announce both.

Start body headings at ##. The same diagnostic catches skipped levels — ## straight to ####.

On a site with base set, Reed does not rewrite links you write in prose. /quickstart compiles cleanly and 404s in production; you have to write /docs/quickstart.

This applies to component attributes too — a <Card href="/quickstart"> is wrong in exactly the same way, and is easier to miss because it does not look like a link.

Links inside docs.json follow the opposite rule: Reed adds the prefix there.

Check this mechanically. Eyes do not catch it, and neither does reed check — validating prose links is outside what schema and contract checks cover.

A name outside the catalog is a DS-S06 error. Usually it is a typo, or a component carried over from another platform that Reed has no equivalent for.

The message distinguishes the two cases by telling you it is neither a catalog member nor locally imported. If you genuinely need something outside the catalog, a local import is the escape hatch — but check the catalog first, since the names differ from other platforms more often than the capabilities do.

Read the contract name in the message. It tells you which set of rules Reed judged your element against, which is usually the answer when a prop you are sure is valid is being rejected — the element was mapped through a dialect you did not expect. See Content dialect.