Skip to content

fix(portal): report content pages that are not valid MDX before the build or preview reaches them - #447

Open
mehnoorsiddiqui wants to merge 1 commit into
devfrom
fix/portal-invalid-mdx
Open

mehnoorsiddiqui wants to merge 1 commit into
devfrom
fix/portal-invalid-mdx

Conversation

@mehnoorsiddiqui

@mehnoorsiddiqui mehnoorsiddiqui commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Closes #443

The problem

A content page that is not valid MDX (an unclosed <Cards>, a mismatched closing tag, a broken {…} expression) reached the author too late, or in a form they could not act on:

  • portal serve started without a word. The browser showed "Failed to fetch dynamically imported module", and the real cause was only in the body of a 500 response. Vite did log it, but the CLI does not show the dev server's output.
  • portal generate failed, but showed the raw tail of the build log: only the first broken page, under the project's temporary path, with bundler internals such as [plugin fumadocs-mdx:mdx], ?macro_id=… and errors: [Getter/Setter].

What changes

The CLI already parses every content page the way the build does, to check its front matter and images. It now also reports where an .mdx page stops being valid MDX:

  • portal generate and portal serve refuse such pages at startup, before Vite is involved.

  • serve checks again on every save in content/. A refused save leaves the last version the preview accepted in place.

  • Every broken page is listed in one run, with its path relative to src, and the line and column. MDX's own wording is kept, without the parser's internal names:

    ■  Pages in '…/src' are not valid MDX:
         • 'content/components/cards.mdx', line 55, column 1: Expected a closing tag for `<Cards>`
         • 'content/components/steps.mdx', line 9, column 12: Could not parse expression: Unexpected token
       MDX stops at the first mistake in a page, so a page can have more after the one shown.
    
  • MDX only notices an unclosed tag at the end of the page. It reports no location on the error and puts the tag's position in the message text instead, so the CLI reads the position from there. The page is reported at the tag's own line.

Not covered

  • Only the first mistake per page. MDX's parser stops at the first error in a page, so a page with three unclosed <Cards> reports one, and the next appears once that one is fixed. The message says so. Listing every unclosed tag would need a tag-matching pass of our own, outside MDX, which could disagree with it.
  • serve does not start over broken content. It reports the problem and exits, as it already does for other content problems.

How it was tested

  • Unit tests for the parse (unclosed tag, mismatched closing tag, broken expression, HTML comment, valid .mdx and .md pages), for the content check, for PortalSourceContext at startup and on a save that is later fixed, and for the message.
  • npm test passes, ESLint is clean, and APIMATIC_E2E=1 passes for portal-build and portal-serve, so every page in the existing fixtures passes the new check.
  • The CLI parses with remark-mdx 3.1.1 and the same micromark extensions that @mdx-js/mdx 3.1.1 uses in the build, so both find the same syntax errors.

🤖 Generated with Claude Code

…uild or preview reaches them

An `.mdx` page that is not valid MDX, such as an unclosed `<Cards>` or a mismatched closing tag,
is now refused by `portal generate` and `portal serve`, at startup and on save. Every broken page
is listed in one run, by its path in `src`, line and column, in MDX's own words without the
bundler's internals. MDX stops at the first mistake in a page, and the message says so.

Closes #443

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

portal: MDX syntax errors (e.g. missing </Cards>) are not reported by 'portal serve', and 'portal generate' reports only the first one

1 participant