MDX Content Format

MDX lets you use JSX components directly inside your Markdown content. It's the hybrid format between pure Markdown (.md) and pure JSX (.tsx).

Format Spectrum

Dune supports three content formats:

  • .md — Pure Markdown, rendered through theme templates. Best for blog posts, documentation.
  • .mdx — Markdown + JSX. Write prose in Markdown, embed interactive components. Best for tutorials with live examples.
  • .tsx — Pure JSX. The component IS the content. Best for landing pages, dashboards.

Writing MDX

MDX files use the same frontmatter as Markdown files:

---
title: "My Tutorial"
taxonomy:
  tag: [tutorial, interactive]
---

# Welcome

Regular **markdown** works normally.

You can also embed JSX components inline.

Key Differences from Markdown

  1. JSX expressions — You can use JSX syntax inline with markdown
  2. Standard HTML always works — All standard HTML elements (<div>, <aside>, <details>, etc.) can be used directly in MDX without any setup
  3. Components from two sources — Import post-specific components with relative import statements, or use theme-wide components from the registry (see below)
  4. Same frontmatter — YAML between --- delimiters, identical to .md
  5. Same routing — Folder = page, numeric prefixes for ordering
  6. Same collections — MDX pages appear in collections and taxonomy queries
  7. Same API — REST API returns format: "mdx" for MDX pages

Rendering pipeline

When a .mdx file is requested:

  1. Parse frontmatter — YAML between --- delimiters extracted with gray-matter
  2. Media resolution — Relative image/link paths rewritten to absolute URLs (same as Markdown)
  3. Co-located imports — Relative import statements resolved against the MDX file's directory, loaded server-side, stripped from source
  4. Compile MDX@mdx-js/mdx compiles the import-stripped source to a JavaScript module
  5. Evaluate — The compiled module is evaluated with a merged component scope (registry + co-located imports)
  6. Render — The component is rendered to an HTML string via Preact SSR

The output is a static HTML string — MDX content is server-rendered, not client-side.

Co-located component imports

MDX files can import components using relative paths, placing post-specific components right alongside the content file:

content/02.blog/01.my-post/
├── post.mdx
└── DataChart.tsx        ← co-located component

Use a standard ES import statement at the top of the MDX file:

---
title: "My Post"
---

# My Post

Here is the data visualisation:

<DataChart data={[1, 4, 9, 16, 25]} />

The chart above shows...

Dune resolves the relative import against the MDX file's directory, loads the module server-side, and makes it available as a component in the MDX scope. The import statement is stripped from the source before MDX compilation.

All three standard import forms are supported:

// default export
// named exports
// namespace import

Co-located imports take precedence over theme registry components — if both define a component with the same name, the co-located import wins. This lets a post override a theme component for its own use.

When to use co-located imports vs. the registry

Use co-located imports for components that belong to a specific post — charts with embedded data, one-off diagrams, post-specific interactive elements. The component and its data live alongside the content.

Use the component registry (themes/{name}/mdx-components.ts) for reusable components shared across many posts — callout boxes, code playgrounds, design system elements.

Both can be used in the same MDX file.

Component registry

MDX content can use custom JSX components that aren't standard HTML elements. Components are loaded from a convention file in your active theme.

Theme convention (recommended)

Create themes/{your-theme}/mdx-components.ts with a default export mapping component names to Preact component functions:

// themes/my-theme/mdx-components.ts

export default { Alert, Chart, Callout };

Dune loads this file automatically at startup. No other configuration is needed. In debug mode, the loaded component names are logged to the console.

MDX content can then use registered components by name:

---
title: "My Tutorial"
---

# Getting Started

<Alert type="warning">
  Make sure you've installed Deno 2 before continuing.
</Alert>

Programmatic API

For custom bootstrap setups outside the standard dune serve / dune dev flow, use the programmatic API:

import { createMdxComponentRegistry, MdxHandler, FormatRegistry } from "@dune/core";

const registry = createMdxComponentRegistry({ Alert });
const formats = new FormatRegistry();
formats.register(new MdxHandler({ components: registry }));
// Pass formats to createDuneEngine...

Any component name not in the registry falls back to the equivalent HTML element, or renders nothing if there is no matching HTML tag.

Output sanitisation

Rendered MDX output goes through the same HTML sanitiser as raw HTML in Markdown (unless trusted_html: true is set site-wide or per page — see Site Configuration). The default allowlist covers common prose markup, which means component markup that uses less common tags (<aside>, inline <svg>, custom elements) or ARIA attributes would normally be stripped.

Theme-declared sanitizer allowances

Instead of requiring sites to set trusted_html: true, a theme can declare the extra tags and attributes its MDX components emit via a named sanitize export in mdx-components.ts:

// themes/my-theme/mdx-components.ts

export default { Alert, Tabs };

export const sanitize = {
  tags: ["aside", "svg", "path", "my-tabs"],
  attributes: {
    "*": ["role", "aria-*", "data-*", "tabindex", "hidden"],
    svg: ["viewBox", "width", "height", "fill", "style"],
    path: ["d", "fill", "fill-rule", "clip-rule"],
  },
};
  • tags — extra element names allowed in sanitised output, in addition to the default allowlist.
  • attributes — extra attributes per tag name; the "*" key applies to all tags. Entries ending in * are prefix wildcards (aria-*, data-*).

The allowances are additive and bounded: they widen the allowlist but cannot reopen the sanitiser's hard rules. Event handler attributes (on*), is, and xmlns are always stripped regardless of what a theme declares; style is only allowed on a tag where it is listed literally — wildcards never match it. <script> and unsafe URL schemes stay blocked.

Because the allowances come from the theme author (who also writes the components), authors of .mdx content still can't inject scripts or event handlers — the trust boundary stays with the theme, not the content.

For programmatic setups, pass the same shape to MdxHandler directly:

new MdxHandler({ components: registry, sanitize: { tags: [...], attributes: {...} } });

The type is exported as MdxSanitizeExtension from @dune/core.

When to Use MDX

Choose MDX when you need the simplicity of Markdown for most of your content but want to embed interactive elements or custom components in specific sections.

For pure prose, use .md. For fully programmatic pages, use .tsx. For the middle ground, use .mdx.