MDX & Components#
Ox Content lets you embed framework components inside Markdown and .mdx files.
It is worth understanding how this works, because it differs from "classic" MDX:
.mdand.mdxare parsed identically — both go through the same Rust Markdown parser (CommonMark + GFM). The parser does not parse JSX. The.mdxextension is simply recognised as a content file.- Components are resolved by a framework plugin, not the parser. The React/Vue/Svelte plugins scan the content for PascalCase component tags, replace them with island placeholders, and hydrate them on the client.
So you get Markdown's speed for prose plus real interactive components where you need them — without shipping a JavaScript bundle for pages that have none.
Setup#
Add the plugin for your framework alongside its official Vite plugin and point it at your components:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { oxContentReact } from "@ox-content/vite-plugin-react";
export default defineConfig({
plugins: [
react(),
oxContentReact({
srcDir: "docs",
// Auto-discover components by glob…
components: "./src/components/*.tsx",
// …or map names explicitly:
// components: { Counter: "./src/components/Counter.tsx" },
}),
],
});
Vue, Svelte, and Solid work the same way via @ox-content/vite-plugin-vue
(oxContentVue), @ox-content/vite-plugin-svelte (oxContentSvelte), and
@ox-content/vite-plugin-solid (oxContentSolid). When components is a glob,
the component name is the PascalCased file name.
The Solid integration additionally has to run before vite-plugin-solid, which
must be given the Markdown extensions — see
its reference page.
Authoring components in Markdown#
Write components as PascalCase tags in your Markdown — self-closing or with children:
# My Page
Regular **Markdown** prose.
<Counter initial={5} />
<Callout type="tip">
This child content is passed to the component.
</Callout>
Only tags that start with an uppercase letter are treated as components, so your
ordinary HTML (<div>, <span>, …) is left untouched. Tags inside fenced code
blocks are not transformed, so you can document component usage without it
being executed.
Props#
Props use JSX-like syntax. The following forms are recognised:
| Syntax | Parsed as |
|---|---|
prop="text" |
string |
prop={42} |
number / JSON value |
prop={true} |
boolean |
prop={ {"a":1} } |
object (JSON) |
prop |
boolean true |
Props are serialized to a data-ox-props attribute on the island element and
handed to your component at hydration time.
How islands hydrate#
Each component becomes an island wrapper in the generated HTML — a block-level
component renders as a <div data-ox-island="Name" …> and an inline one as a
<span data-ox-island="Name" …>. The matching framework runtime mounts the real
component into that element on the client.
Hydration timing is controlled by a load strategy (see
@ox-content/islands):
| Strategy | Hydrates… |
|---|---|
eager |
immediately on load (default) |
idle |
during requestIdleCallback (≈200 ms fallback) |
visible |
when the element scrolls into view (IntersectionObserver) |
media |
when a media query matches (matchMedia) |
Because the server output is plain HTML, pages render and are readable before (or entirely without) hydration; the island JavaScript is only loaded for the components a page actually uses.
Static JSX in themes#
Separately from component islands, Ox Content ships a small static JSX
runtime (jsx, jsxs, Fragment, renderToString, raw, when, each)
used to author themes and layouts that render to HTML strings with no
client-side JavaScript. Configure it in tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@ox-content/vite-plugin"
}
}
That resolves @ox-content/vite-plugin/jsx-runtime, and
@ox-content/vite-plugin/jsx-dev-runtime when jsx is react-jsxdev. Both
render to HTML strings; there is no React and no dev-only behavior to opt into.
See Theming for using it to build a custom layout.