Skip to content

Conversation

@dannobytes
Copy link
Contributor

@dannobytes dannobytes commented Nov 26, 2025

PR App Fix CX-2543

🧰 Changes

Discovered through this ticket
that the TOC generated by the MDX renderer would become broken with
miscalculated nesting depths and exposing h3s or other heading depths that shouldn't be included.

This happened when a jsx element was embedded inside the MDX body and
the custom component name did not match the exported jsx elements.

For example:
image

I'm not entirely sure whether this is an edge-case we should be guarding
against from inside our custom component editor. Like, adding validation to
ensure that the component name matches what's being exported from it. My
gut tells me it'd be really hard to enforce that.

So, the fix (for now) is to simply take this edge-case into account and add
protections for the cases where this happens.

When a component name doesn't match the jsx element name, it previously
was making its way into the plugin/toc flow. But b/c it has no
tagName, the getDepth() calculation would break and return a NaN
instead of a valid integer. This would break the nesting logic in
toctoHast() and cause all headers to render at the first level.

Fixed this by:

  1. filtering away these mismatched components that make it through
  2. additionally updating the getDepth() function to be more fail-proof
    and handle the case where tagName is undefined
before after
image image

🧬 QA & Testing

Tests should pass

Discovered through [this ticket](https://linear.app/readme-io/issue/CX-2543/tabapay-toc-does-not-respect-headers-nesting)
that the TOC generated by the MDX renderer would become broken with
miscalculated nesting depths.

This happened when a jsx element was embedded inside the MDX body and
the custom component name did not match the exported jsx elements.

For example:
image here

I'm not entirely sure whether this is an edge-case we should be guarding
against from our custom component editor. Like, adding validation to
ensure that the component name matches what's being exported from it. My
gut tells me it'd be really hard to enforce that.

So, the fix here is to simply take this edge-case into account and add
protections for the cases where this happens.

When a component name doesn't match the jsx element name, it previously
was making its way into the `plugin/toc` flow. But b/c it has no
`tagName`, the `getDepth()` calculation would break and return a `NaN`
instead of a valid integer. This would break the nesting logic in
`toctoHast()` and cause all headers to render at the first level.

Fixed this by:
1. filtering away these mismatched components that make it through
1. additionally updating the `getDepth()` function to be more fail-proof
   and handle the case where `tagName` is undefined

CX-2543
@dannobytes dannobytes requested a review from a team November 26, 2025 06:49
@dannobytes dannobytes changed the title fix(toc): prevent misnamed mdx components from breaking TOC nesting Nov 26, 2025
Comment on lines +197 to +198
// 👇🏼 this is what we're guarding against
CompDoesNotMatchExportedModule: compModule,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the edge-case that was causing the malformed TOCs in some projects.

when they have custom components that exports a JSX element that differs from the saved file name, this is what happens. the components hash keys are something different than the exported elements.

Comment on lines +207 to +209
<ul>
<li><a href="#subheading">SubHeading</a></li>
</ul>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when everything works properly, the subheading should be indented one level like this

Comment on lines +125 to +127
if (node.type === 'mdxJsxFlowElement') {
return components[node.name] || [];
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the exported JSX elements are the nodes (i.e. node.name) and the components is a hash of custom component files. previously, it was matching node.name in components which would be false when the filename didn't match the exported module. otherwise, it'd return the mdxJsxFlowElement node, which would then make its way into the getDepth() function above on L74.

now, if the node type is an mdx jsx element, we'll return an empty array instead, which essentially filters it out completely. i think this is the desired result, but i put in a patch in the getDepth() function just in case something other than a heading element makes it in

Copy link
Contributor

@kevinports kevinports left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

@dannobytes dannobytes merged commit fcb5f7d into next Dec 1, 2025
11 checks passed
@dannobytes dannobytes deleted the daniels/cx-2543-tabapay-toc-does-not-respect-headers-nesting branch December 1, 2025 19:51
rafegoldberg pushed a commit that referenced this pull request Dec 1, 2025
## Version 11.7.6
### 🛠 Fixes & Updates

* **deps:** bump actions/checkout from 5 to 6 ([#1241](#1241)) ([2c36df2](2c36df2)), closes [actions/checkout#2248](actions/checkout#2248) [actions/checkout#2286](actions/checkout#2286) [actions/checkout#2298](actions/checkout#2298) [actions/checkout#2311](actions/checkout#2311) [actions/checkout#2301](actions/checkout#2301) [actions/checkout#2286](actions/checkout#2286) [actions/checkout#2248](actions/checkout#2248) [actions/checkout#2301](actions/checkout#2301) [actions/checkout#2226](actions/checkout#2226) [actions/checkout#2305](actions/checkout#2305) [actions/checkout#1971](actions/checkout#1971) [actions/checkout#1977](actions/checkout#1977) [actions/checkout#2043](actions/checkout#2043) [actions/checkout#2044](actions/checkout#2044) [actions/checkout#2194](actions/checkout#2194) [actions/checkout#2224](actions/checkout#2224) [actions/checkout#2236](actions/checkout#2236) [actions/checkout#1941](actions/checkout#1941) [actions/checkout#1946](actions/checkout#1946) [actions/checkout#1924](actions/checkout#1924) [actions/checkout#1180](actions/checkout#1180) [actions/checkout#1777](actions/checkout#1777) [actions/checkout#1872](actions/checkout#1872) [actions/checkout#1739](actions/checkout#1739) [actions/checkout#1697](actions/checkout#1697) [actions/checkout#1774](actions/checkout#1774) [actions/checkout#1776](actions/checkout#1776) [actions/checkout#1732](actions/checkout#1732) [actions/checkout#1703](actions/checkout#1703) [actions/checkout#1694](actions/checkout#1694) [actions/checkout#1696](actions/checkout#1696) [actions/checkout#1695](actions/checkout#1695) [#2311](https://github.com/readmeio/markdown/issues/2311) [#2298](https://github.com/readmeio/markdown/issues/2298) [#2286](https://github.com/readmeio/markdown/issues/2286) [#2248](https://github.com/readmeio/markdown/issues/2248)
* **toc:** prevent misnamed mdx components from breaking TOC ([#1242](#1242)) ([fcb5f7d](fcb5f7d))

<!--SKIP CI-->
@rafegoldberg
Copy link
Contributor

This PR was released!

🚀 Changes included in v11.7.6

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

Labels

5 participants