Skip to content

A folder with frontend: { stack: "mdx" }. Pages are .mdx, layouts are layout.mdx. Routing, fetch clients and validation behave the same for every frontend. MDX folders ›

No TypeScript in .mdx

This is the MDX-specific trap, and it has no workaround. .mdx is parsed as plain JavaScript with JSX - no type annotations, no type arguments, no type imports:

pages/users/[id]/index.mdx
mdx
{/* MDX: pages/users/[id]/index.mdx */}
import { useLoaderData } from "_/use";

export const Profile = () => {
  // no type argument - the result is untyped
  const user = useLoaderData();
  return <p>{user?.name}</p>;
};

<Profile />

useParams<"users/[id]">() and import type { X } are both syntax errors here. Keep typed code in a .tsx component and import it into the page.

Hooks run during render

_/use exists in MDX folders with the full set, plus useFrontmatter. export const x = useHook() at module scope runs on import, not during render - always call hooks inside a component function:

mdx
export const P = () => useParams();   // runs during render

A loader cannot use hooks either; it receives the resolved route object, which carries paramsEntries and frontmatter.

Frontmatter drives head and static params

staticParams is declared in frontmatter rather than as an export, since a .mdx page has no typed export surface:

pages/docs/[slug]/index.mdx
mdx
---
title: Documentation
staticParams:
  - [getting-started]
  - [routing]
---

Layouts

layout.mdx
mdx
<!-- MDX: layout.mdx -->
<nav>
  <a href="/">Home</a>
  <a href="/docs">Docs</a>
</nav>

<main>
  {props.children}
</main>

<footer>
  Built with KosmoJS
</footer>

Layouts must be .mdx. A plain .md file cannot render {props.children} and will not work as one.

Worth knowing

  • SSR is string-only. renderMode: "stream" is not available for MDX folders.
  • MDX has no third-party router, so KosmoJS supplies the matcher and emits its own RawRoute shape. Routing ›
  • The seeded Link component is typed in Link.tsx, but .mdx call sites are not type-checked, so a wrong route name surfaces at runtime.
  • Curly braces in prose are parsed as JSX - wrap them in backticks.
  • The default plugin is @mdx-js/rollup with a Preact JSX runtime.

Released under the MIT License.