mdxe
Zero-config CLI for developing, testing, building, and deploying MDX apps
MDXE: MDX Execution Engine
MDXE is a zero-config CLI for building full applications using pure MDX - no JavaScript bundler config, no framework setup, just MDX files.
What is MDXE?
Write .mdx files, run mdxe dev, and you have a running application. MDXE handles:
- Zero Config: No webpack, vite, or bundler configuration needed
- Development: Hot reload dev server with instant updates
- Testing: Built-in test runner for MDX components
- Building: Optimized production builds automatically
- Deployment: One command deploy to Cloudflare Workers
- Pure MDX: Build entire apps with just MDX files - no separate JS/TS needed
Why MDXE?
Traditional web development requires configuring bundlers, frameworks, and build tools. MDXE eliminates all of that:
# Traditional approach
npm init
npm install react webpack babel typescript vite ...
# Configure webpack.config.js, tsconfig.json, etc.
# Write components, setup routing, etc.
# MDXE approach
mdxe dev ./content
# That's it. Your app is running.Installation
pnpm install mdxeAuthentication
To deploy to the .do platform, you need to authenticate:
DO_TOKEN (API Token)
Set your platform API token:
# Export token
export DO_TOKEN="your-api-token-here"
# Or in .env file
echo "DO_TOKEN=your-api-token-here" >> .env
# Now you can deploy
mdxe deployGet your token from platform.do/settings/tokens.
OAuth via OAuth.do
For interactive deployment:
# Login via OAuth
mdxe login
# Opens browser to oauth.do for authentication
# Credentials stored securely
# Now authenticated
mdxe deployAuthentication in Code
import { mdxe } from 'mdxe'
// With DO_TOKEN
await mdxe.deploy('./content', {
token: process.env.DO_TOKEN,
name: 'my-site',
})
// With OAuth
await mdxe.deploy('./content', {
auth: 'oauth',
name: 'my-site',
})Required Scopes
OAuth scopes for deployment:
workers:read- View deployed workersworkers:write- Deploy/update workersworkers:delete- Delete deploymentsassets:write- Upload static assets
# Login with specific scopes
mdxe login --scope "workers:write assets:write"Quick Start
Create Your First App
# Create a directory with an MDX file
mkdir my-app
cd my-app
echo "# Hello World\n\nMy first MDX app!" > index.mdx
# Start dev server
mdxe dev
# Open http://localhost:3000That's it! No package.json, no config files, no build setup.
Add More Pages
# Create more pages - routes are automatic
echo "# About\n\nAbout page" > about.mdx
echo "# Blog\n\nBlog posts" > blog/index.mdx
echo "# Post 1" > blog/post-1.mdx
# Routes automatically created:
# / → index.mdx
# /about → about.mdx
# /blog → blog/index.mdx
# /blog/post-1 → blog/post-1.mdxCLI Commands
All commands are zero-config with sensible defaults:
Development
Start dev server with hot reload:
mdxe dev # Current directory, port 3000
mdxe dev ./content # Specific directory
mdxe dev --port 8080 # Custom portTesting
Test your MDX components and pages:
mdxe test # Run all tests
mdxe test --watch # Watch mode
mdxe test blog/post.mdx # Test specific fileWrite tests inline in MDX:
---
$type: BlogPost
title: My Post
---
# My Post Content
export function test() {
// Test frontmatter
assert(data.$type === 'BlogPost')
assert(data.title === 'My Post')
// Test rendering
const rendered = render()
assert(rendered.includes('My Post Content'))
}Building
Build for production:
mdxe build # Build current directory
mdxe build ./content # Build specific directory
mdxe build --output ./dist # Custom output directoryDeployment
One-command deploy to Cloudflare Workers:
mdxe deploy # Deploy with auto-generated name
mdxe deploy --name my-app # Custom name
mdxe deploy --prod # Deploy to productionPreview
Preview production build locally:
mdxe preview # Preview ./dist on port 8080
mdxe preview ./dist # Preview specific build
mdxe preview --port 3000 # Custom portConfiguration (Optional)
MDXE works zero-config out of the box. Create mdxe.config.ts only if you need to customize:
import { defineConfig } from 'mdxe'
export default defineConfig({
// Source directory
source: './content',
// Output directory
output: './dist',
// Cloudflare Workers config
worker: {
name: 'my-mdx-site',
routes: ['example.com/*'],
compatibility_date: '2025-10-27',
},
// MDX options
mdx: {
remarkPlugins: [],
rehypePlugins: [],
},
// Asset handling
assets: {
publicPath: '/assets',
optimize: true,
},
})Execution Context
Pass context to MDX files:
import { mdxe } from 'mdxe'
const result = await mdxe.execute('./content/page.mdx', {
// Props accessible in MDX
props: {
user: { name: 'Alice' },
items: [1, 2, 3],
},
// Context for server-side code
context: {
db: database,
env: process.env,
},
})In your MDX file:
---
title: Welcome {props.user.name}
---
# Welcome, {props.user.name}!
You have {props.items.length} items.
export async function loadData(context) {
const data = await context.db.query('SELECT * FROM items')
return data
}
export const items = await loadData(context)Static Assets
MDXE uses Cloudflare Workers Static Assets (not deprecated Pages):
// mdxe.config.ts
export default defineConfig({
assets: {
// Public directory for static files
publicDir: './public',
// Asset optimization
optimize: true,
// Asset URL prefix
publicPath: '/assets',
},
})Directory structure:
content/
page.mdx
public/
images/
hero.png
styles/
main.cssReference assets in MDX:

<link rel="stylesheet" href="/assets/styles/main.css" />Environment Variables
Access environment variables:
// mdxe.config.ts
export default defineConfig({
env: {
API_URL: process.env.API_URL,
DATABASE_URL: process.env.DATABASE_URL,
},
})In MDX:
export const apiUrl = context.env.API_URL
<div>API URL: {apiUrl}</div>Routing
MDXE automatically generates routes from file structure:
content/
index.mdx → /
about.mdx → /about
blog/
index.mdx → /blog
post-1.mdx → /blog/post-1
docs/
getting-started.mdx → /docs/getting-startedDynamic Routes
Create dynamic routes with brackets:
content/
blog/
[slug].mdx → /blog/:slug
products/
[category]/
[id].mdx → /products/:category/:idIn [slug].mdx:
export async function getStaticPaths() {
const posts = await fetchPosts()
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}))
}
---
title: {props.post.title}
---
# {props.post.title}
{props.post.content}API Routes
Create API endpoints with .api.ts files:
// content/api/hello.api.ts
export default async function handler(request: Request) {
return new Response(
JSON.stringify({ message: 'Hello, World!' }),
{
headers: { 'Content-Type': 'application/json' },
}
)
}Middleware
Add middleware for authentication, logging, etc:
// mdxe.config.ts
export default defineConfig({
middleware: [
// Logging middleware
async (request, next) => {
console.log(`${request.method} ${request.url}`)
return next()
},
// Auth middleware
async (request, next) => {
const token = request.headers.get('Authorization')
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
return next()
},
],
})Use Cases
Documentation Sites
Build comprehensive docs with MDX:
docs/
index.mdx
getting-started.mdx
api-reference/
index.mdx
endpoints.mdx
guides/
authentication.mdx
deployment.mdxDeploy with:
mdxe deploy ./docs --name my-docsMarketing Sites
Create marketing pages with components:
---
title: Welcome to Our Product
---
<Hero
title="Build Amazing Things"
subtitle="The fastest way to ship"
cta={<Button href="/get-started">Get Started</Button>}
/>
<Features items={features} />
<Pricing plans={plans} />
<Testimonials reviews={reviews} />Blog
Dynamic blog with MDX:
blog/
[slug].mdx # Dynamic post route
index.mdx # Blog listing
export async function getStaticPaths() {
const posts = await import.meta.glob('./posts/*.mdx')
return Object.keys(posts).map(path => ({
params: { slug: path.replace('./posts/', '').replace('.mdx', '') },
}))
}