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.

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.