fumadocs-mdx-cloudflare
v0.1.0
Published
Remote MDX rendering on Cloudflare Workers for fumadocs
Maintainers
Readme
fumadocs-mdx-cloudflare
Remote MDX rendering on Cloudflare Workers for fumadocs.
Cloudflare Workers don't allow new Function() or eval(), which breaks the standard fumadocs MDX rendering pipeline. This package works around that by compiling MDX into an ES module that workerd parses natively, then executes it as a Dynamic Worker via the WorkerLoader binding.
Install
bun add fumadocs-mdx-cloudflarePeer dependencies:
bun add @fumadocs/mdx-remote @cloudflare/worker-bundler react react-domFor the source adapter, also install:
bun add fumadocs-coreSetup
Add a WorkerLoader binding to your wrangler.jsonc:
{
"worker_loader": [
{ "binding": "RENDERER" }
]
}If using the cache wrapper, add a KV namespace:
{
"kv_namespaces": [
{ "binding": "CACHE", "id": "your-kv-namespace-id" }
]
}Usage
Basic rendering
import { createRenderer } from "fumadocs-mdx-cloudflare";
const renderer = createRenderer({
workerLoader: env.RENDERER,
compilerOptions: {
rehypeCodeOptions: {
themes: { light: "github-light", dark: "github-dark" },
},
},
});
const { html, toc } = await renderer.render(mdxSource);
// html: string — rendered HTML
// toc: TocItem[] — { title: string, url: string, depth: number }With caching
withCache wraps a renderer with KV read-through caching. It checks KV first, renders on cache miss, and stores the result.
import { createRenderer, withCache } from "fumadocs-mdx-cloudflare";
const renderer = createRenderer({
workerLoader: env.RENDERER,
});
const cached = withCache(renderer, {
kv: env.CACHE,
prefix: "blog",
});
// Checks KV for blog:html:{slug} and blog:toc:{slug}
// On miss: renders, stores, returns
const { html, toc } = await cached.render("my-post", mdxSource);
// Invalidate when content changes
await cached.invalidate("my-post");Use renderer.render(source) for uncached renders (e.g. draft previews) and cached.render(key, source) for production reads.
Configuration
const renderer = createRenderer({
// Required: WorkerLoader binding from wrangler.jsonc
workerLoader: env.RENDERER,
// Passed to @fumadocs/mdx-remote createCompiler
compilerOptions: {
rehypeCodeOptions: { themes: { light: "github-light", dark: "github-dark" } },
},
// Pinned React version for the worker bundle (fetched from npm at runtime)
react: { version: "19.2.4" },
// Prefix for worker loader IDs — avoids collisions with multiple renderers
namespace: "blog",
// Dynamic worker compatibility settings
worker: {
compatibilityDate: "2026-01-01",
compatibilityFlags: ["nodejs_compat"],
globalOutbound: null, // sandbox: no network access from renderer
},
// Forwarded to @cloudflare/worker-bundler createWorker
bundler: {
minify: false,
sourcemap: false,
},
});Source adapter
createR2Source maps MDX files in an R2 bucket to a fumadocs Source, which can be passed to the fumadocs loader() for page trees, URL generation, and querying.
import { createR2Source } from "fumadocs-mdx-cloudflare/source";
import { z } from "zod";
const frontmatterSchema = z.object({
title: z.string(),
description: z.string().optional(),
publishedAt: z.string(),
});
const source = await createR2Source({
bucket: env.CONTENT,
prefix: "blog/",
schema: frontmatterSchema,
slug: (key) => key.replace(/^blog\//, "").replace(/\.mdx?$/, ""),
});
// source is a fumadocs Source — use with loader()The schema option accepts a Zod schema (anything with .parse()) or a plain function (data: unknown) => T. Objects that fail validation are silently skipped.
How it works
fumadocs compiles MDX with outputFormat: 'function-body', which produces code like:
"use strict";
const { Fragment, jsxDEV } = arguments[0];
function MDXContent(props) { /* ... */ }
return { default: MDXContent, toc };Normally you'd evaluate this with new Function() — but Cloudflare Workers blocks dynamic code evaluation.
This package instead embeds the compiled body inside a regular function in a static ES module:
import * as jsxRuntime from "react/jsx-runtime";
import { renderToString } from "react-dom/server.browser";
function evaluateCompiled(_jsxRuntime) {
// compiled MDX body pasted here
// arguments[0] resolves to _jsxRuntime
}
const { default: Content, toc } = evaluateCompiled(jsxRuntime);
export default {
async fetch() {
return new Response(JSON.stringify({
html: renderToString(Content({})),
toc,
}));
},
};This module is bundled with React via @cloudflare/worker-bundler and loaded as a Dynamic Worker through the WorkerLoader binding. The compiled body is part of the module source at parse time — no eval() or new Function() at runtime.
Content-addressed hashing (SHA-256) of the compiled body deduplicates workers: identical MDX produces the same worker ID, so repeated renders reuse the existing worker.
API
fumadocs-mdx-cloudflare
| Export | Description |
|---|---|
| createRenderer(options) | Create a renderer that compiles and executes MDX on a Dynamic Worker |
| withCache(renderer, options) | Wrap a renderer with KV read-through caching |
| buildRendererModule(compiled) | Build the ES module source from compiled MDX (advanced) |
fumadocs-mdx-cloudflare/source
| Export | Description |
|---|---|
| createR2Source(options) | Create a fumadocs Source from R2 bucket content |
License
MIT
