> ## Documentation Index
> Fetch the complete documentation index at: https://better-router.bansal.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Markdown pages

> Render .md and .mdx files as routes, with frontmatter, a table of contents, and optional custom compilers.

`.md` and `.mdx` files in `src/routes` are pages. They use the same layouts, middleware, loaders, actions, and SPA navigation as `.tsx` pages. The only difference is the page body is written in markdown.

```
src/routes/about.mdx            →  GET /about
src/routes/docs/index.md        →  GET /docs
src/routes/docs/index.server.tsx
src/routes/blog/[slug].mdx      →  GET /blog/:slug
```

A TypeScript page wins if both `about.tsx` and `about.mdx` exist.

## Frontmatter and `toc`

YAML frontmatter and a heading table of contents are passed as props, together with any loader data.

```mdx title="src/routes/guide.mdx" theme={null}
---
title: Deploying
description: Ship the worker.
---

# Deploying

{props.description}

## Prerequisites
```

```tsx theme={null}
type GuideProps = {
  title: string;
  description: string;
  frontmatter: { title: string; description: string };
  toc: Array<{ id: string; text: string; depth: number; children?: GuideProps["toc"] }>;
};
```

* Flattened frontmatter fields (`title`, `description`) sit next to loader props. Loader fields win on conflict.
* `frontmatter` is the full object.
* `toc` is a nested list of headings. Heading ids match the slugs rendered on the page (`hello-world` for `## Hello world`). Use static heading text so the slug stays readable — `# {props.title}` becomes `propstitle` because the TOC is built from source.

Read them from a layout or island with the same hooks used for loader data:

```tsx title="src/routes/_layout.tsx" theme={null}
import type { ReactNode } from "react";
import { useFrontmatter, useToc } from "better-router/react";

export default function Layout({ children }: { children: ReactNode }) {
  const { title } = useFrontmatter<{ title?: string }>();
  const toc = useToc();
  return (
    <div>
      {title ? <p>{title}</p> : null}
      {toc.length > 0 ? (
        <ol>
          {toc.map((item) => (
            <li key={item.id}>
              <a href={`#${item.id}`}>{item.text}</a>
            </li>
          ))}
        </ol>
      ) : null}
      {children}
    </div>
  );
}
```

`useLoaderData()` also includes `frontmatter` and `toc` on markdown routes.

## Layouts, middleware, loaders

Nothing else changes.

```ts title="src/routes/guide.server.tsx" theme={null}
import type { LoaderArgs } from "better-router";

export function loader({ params }: LoaderArgs) {
  return { slug: params.slug };
}

export const middleware = async (c, next) => {
  await next();
};
```

`src/routes/_layout.tsx`, `_layout.server.tsx`, `_middleware.ts`, and `src/middleware` wrap markdown pages the same way they wrap JSX pages. Island files work too: `notes.island.mdx`.

## MDX

`.mdx` can use JSX. Components from `src/components/markdown` are already in scope — do not import them. `.md` is markdown only (GFM tables, task lists, and strikethrough are on by default).

```mdx title="src/routes/docs/counter.mdx" theme={null}
# Interactive docs

<Counter />
```

`Counter` is the default export of `src/components/markdown/Counter.tsx`.

## Markdown components

Default exports in `src/components/markdown` are registered automatically.

| File                                  | Used as                         |
| ------------------------------------- | ------------------------------- |
| `src/components/markdown/h1.tsx`      | `<h1>`                          |
| `src/components/markdown/code.tsx`    | `<code>`                        |
| `src/components/markdown/Callout.tsx` | `<Callout>`                     |
| `src/components/markdown/wrapper.tsx` | MDX layout around the page body |

```tsx title="src/components/markdown/Callout.tsx" theme={null}
import type { ReactNode } from "react";

export default function Callout({ children }: { children?: ReactNode }) {
  return <aside className="callout">{children}</aside>;
}
```

```mdx theme={null}
<Callout>You do not import this. The plugin maps the file name.</Callout>
```

Change the folder with `markdown.componentsDir`:

```ts title="vite.config.ts" theme={null}
betterRouter({
  markdown: {
    componentsDir: "src/mdx-components",
  },
});
```

## Plugins

Pass [remark](https://github.com/remarkjs/remark) / [rehype](https://github.com/rehypejs/rehype) / recma plugins. `remark-gfm` and `rehype-slug` are already included.

```ts title="vite.config.ts" theme={null}
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import remarkToc from "remark-toc";
import { betterRouter } from "better-router/plugin";

export default defineConfig({
  plugins: [
    betterRouter({
      markdown: {
        remarkPlugins: [remarkToc],
        rehypePlugins: [rehypeAutolinkHeadings],
      },
    }),
  ],
});
```

## Custom renderer (comark)

The default compiler is [`@mdx-js/mdx`](https://mdxjs.com). Replace it when you want another engine.

`parse` is the HTML path: return markup and we wrap it as a React page. Frontmatter and `toc` still become props.

```ts title="vite.config.ts" theme={null}
import { parse as comark } from "comark";
import { defineConfig } from "vite";
import { betterRouter } from "better-router/plugin";

export default defineConfig({
  plugins: [
    betterRouter({
      markdown: {
        async parse(file) {
          const result = comark(file.content, {
            gfm: true,
            frontmatter: file.frontmatter,
          });
          return {
            html: result.html,
            frontmatter: result.frontmatter ?? file.frontmatter,
            toc: result.toc ?? file.toc,
          };
        },
      },
    }),
  ],
});
```

`file` is `{ id, filename, source, content, frontmatter, toc, format }`. `content` is the body with YAML already stripped.

`compile` replaces the whole module. Return JavaScript whose default export is a React component. Missing `frontmatter` / `toc` exports are filled in for you.

```ts theme={null}
betterRouter({
  markdown: {
    async compile(file) {
      const html = renderWithComark(file.content);
      return `
        export default function Page(props) {
          return <article dangerouslySetInnerHTML={{ __html: ${JSON.stringify(html)} }} />;
        }
      `;
    },
  },
});
```

`compile` wins if both are set.

## TypeScript

`*.md` / `*.mdx` default exports are typed as React pages with `frontmatter` and `toc` named exports. Add `better-router/client` to `tsconfig` `compilerOptions.types` or `include` `client.d.ts` as you already do for the other virtual modules.
