Markdown forgives. MDX compiles.
Write a stray angle bracket in a Markdown file and the renderer prints something reasonable. Write the same character in MDX and the build stops with a parse error pointing at a line you thought was ordinary prose. The reason is structural: MDX hands your document to a JavaScript parser, so a document is also a module. Formatting here means two jobs at once, making the JSX readable and telling you which characters the parser will refuse.
The angle bracket that ends a build
Any < followed by a letter starts a tag. Follow one with a space or a digit and the parser still tries, then reports an unexpected character further down the file. Comparisons in prose are where this bites most often, because nothing looks wrong while you type.
Any cost below 100 is fine,so treat 40 < 100 as a pass.Any cost below 100 is fine,so treat `40 < 100` as a pass.Backticks are the fix worth reaching for first, since inline code is exempt from JSX parsing and reads better anyway. The HTML entity works too. The checks panel flags bare brackets with a line number rather than rewriting them, because a bracket in prose might be a tag you forgot to finish, and guessing between those two would corrupt your document.
Curly braces are executable
In MDX a brace opens a JavaScript expression, in prose as much as inside a tag. That makes {count} a variable lookup and {use the flag} a syntax error. Documentation about templating languages, shell scripts, or CSS suffers the most here, since those subjects put braces in sentences constantly.
Escape a literal brace with a backslash, written as \{, or wrap the passage in backticks. The panel above reads the contents of every brace pair and only warns when what is inside fails to look like an expression. A brace holding props.title passes without comment. A brace holding a sentence gets flagged.
Comments changed spelling in MDX 2
The HTML comment syntax was valid in MDX 1 and is a parse error in MDX 2, which is the single most common breakage when an old docs folder moves to a current toolchain. JSX comments live inside an expression instead.
<!-- migrated from the old docs site -->{}Leave the first toggle on and every comment converts during formatting, with a count in the checks list so you know how many the file carried.
Imports belong at column one
An import statement is only a statement when nothing precedes on the line. Indent one by two spaces, drop one inside a list item, and MDX treats the line as paragraph text. Your component then renders as literal characters, or the page fails on a missing reference, with no error naming the import.
Formatting collects every single line import and export, dedupes them, normalises the specifier spacing and quote style, then places the block directly under the frontmatter. Order is preserved rather than sorted alphabetically, because import order carries meaning when a module has side effects.
A statement spread across several lines stays exactly where you wrote it, character for character. Collapsing one onto a single line would destroy a trailing comment inside the braces, and no amount of tidy import blocks is worth that.
Attributes are props with JavaScript names
Anything inside a tag follows JSX naming, not HTML naming. These four rewrites happen automatically. The fifth needs a human, since converting a CSS string into an object involves choices a text pass should not make on your behalf.
| Written as HTML | Required by MDX | What happens otherwise |
|---|---|---|
class="grid" | className="grid" | React drops the attribute and logs a warning |
for="email" | htmlFor="email" | The label stops pointing at its field |
tabindex="0" | tabIndex="0" | Keyboard focus order silently ignores the element |
<br> | <br /> | Parse error, since JSX has no void tags |
style="gap: 12px" | style={{ gap: 12 }} | Flagged for you to convert, never rewritten |
Why prose stays flush left
Most formatters indent everything inside a component, prose included. That reads nicely until nesting reaches two levels, where four leading spaces turn a paragraph into an indented code block. Markdown rules still apply inside MDX, so the text renders in a grey box and nobody knows why.
This formatter indents structural lines only, meaning tags and expressions. Paragraphs, list items and headings inside a component stay at column one. Diffs come out smaller and no amount of nesting depth changes how your prose renders. If your team runs Prettier on MDX in CI, expect a difference on exactly this point and pick one of the two rather than alternating.
What the checks panel reports
- Unclosed and mismatched tags. A missing closing tag makes every line below into children of the open element. The panel names the tag and the line where the opening sits.
- Components used without an import. Capitalised tags are resolved as JavaScript identifiers. A tag with no matching import fails at build time, which is the error most often produced by copying a block between two pages.
- Imports nobody uses. Listed quietly, since a module imported for a side effect is legitimate.
- Braces holding something other than an expression. Reported with the offending text so you decide between escaping and rewriting.
- Bare angle brackets in prose. Reported with a line number, never rewritten.
- String style attributes. Flagged with the object form to write instead.
- Repairs already applied. Comment conversions, attribute renames, self closed void tags and hoisted imports are counted so the diff holds no surprises.
Where this page stops
- Text analysis, not compilation. There is no MDX compiler running in the page. Multi line expressions, JSX spread attributes and template literals spanning lines are read line by line, so a badly broken file may produce a report with false entries. Trust the compiler over this panel when the two disagree.
- JavaScript inside expressions is left alone. An inline arrow function keeps whatever spacing it had. Run the Babel formatter on component source instead.
- Frontmatter passes through untouched. The YAML block is preserved exactly as written, so a broken key stays broken. Check it in the YAML validator before publishing.
- Code fences are copied character for character. Examples showing broken MDX stay broken, which is the point of writing them.
- Long attribute lists are not wrapped. A component with nine props keeps them on one line. Breaking them across lines changes whitespace children in ways worth deciding by hand.
- Your plugin set is invisible here. Directives, footnotes, math blocks and custom remark syntax come from your config. Formatting preserves them as text without understanding them.
