---
title: Render MDX and TOC
description: >-
  Set up runtime MDX components, stable heading IDs, and a table of contents
  from the generated navigation manifest.
group: docs-site
lastModified: '2026-05-11T20:02:32-07:00'
lastAuthor: 'github-actions[bot]'
---
# Render MDX and TOC

Leadtype does not render your website. Your docs app owns MDX runtime components, layout, accessibility, and the "On this page" UI. Leadtype defines the contracts that keep the rendered HTML and generated markdown aligned.

## Register MDX components

Use the component names Leadtype's remark stack knows how to flatten:

```tsx
import { mdxComponents } from "@/components/docs-mdx";

export const components = {
  ...mdxComponents,
};
```

The naming contract is documented in [Components](/docs/authoring/components). If your app uses different component names, add a custom remark plugin that maps them back before Leadtype flattens MDX to markdown.

## Use the same heading slugs

`resolveDocsNavigation` extracts a table of contents from page headings. Your rendered headings need matching `id` attributes or sidebar links will miss.

Import `slugifyDocsHeading` from the fs-free readability entry point:

```tsx
import { slugifyDocsHeading } from "leadtype/llm/readability";
import { type ComponentPropsWithoutRef, isValidElement } from "react";

type HeadingProps = ComponentPropsWithoutRef<"h1">;

function textFromChildren(children: unknown): string {
  if (typeof children === "string" || typeof children === "number") {
    return String(children);
  }
  if (Array.isArray(children)) {
    return children.map(textFromChildren).join("");
  }
  if (isValidElement(children)) {
    const elementProps = children.props as { children?: unknown };
    return textFromChildren(elementProps.children);
  }
  return "";
}

function createHeading(level: 1 | 2 | 3 | 4 | 5 | 6) {
  return ({ children, id, ...props }: HeadingProps) => {
    const Component = `h${level}` as const;
    const headingId = id ?? slugifyDocsHeading(textFromChildren(children));
    return (
      <Component id={headingId || undefined} {...props}>
        {children}
      </Component>
    );
  };
}
```

Authors can still pin an anchor by passing an explicit `id`.

## Generate navigation with TOC data

`resolveDocsNavigation` includes `toc` on every page. The default range is `h2` to `h3`.

```ts
import { resolveDocsNavigation } from "leadtype/llm";
import docsConfig from "../docs/docs.config";

const navigation = await resolveDocsNavigation({
  srcDir: ".",
  baseUrl: "https://example.com",
  groups: docsConfig.groups,
  toc: { minLevel: 2, maxLevel: 4 },
});
```

Write the navigation object to a generated JSON file and import it from your sidebar.

## Render the sidebar

Look up the current page in the manifest and pass `currentPage.toc` to your sidebar component. The example app includes a complete React implementation with scroll-spy, hash sync, active highlighting, and sticky positioning:

[`apps/example/src/components/table-of-contents.tsx`](https://github.com/inthhq/leadtype/blob/main/apps/example/src/components/table-of-contents.tsx)

## Troubleshooting

* **TOC links do not scroll anywhere.** Your heading IDs do not match the extracted slugs. Use `slugifyDocsHeading` in the rendered heading components.
* **A page TOC is empty.** All headings are outside the configured `minLevel` and `maxLevel` range. Defaults exclude `h1` and headings deeper than `h3`.
* **Sidebar order differs from llms.txt.** The app is not using the same `docs.config.ts` groups as the generation step.
