.do
Mdxmdxe

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 mdxe

Authentication

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 deploy

Get 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 deploy

Authentication 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 workers
  • workers:write - Deploy/update workers
  • workers:delete - Delete deployments
  • assets: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:3000

That'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.mdx

CLI 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 port

Testing

Test your MDX components and pages:

mdxe test                   # Run all tests
mdxe test --watch           # Watch mode
mdxe test blog/post.mdx     # Test specific file

Write 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 directory

Deployment

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 production

Preview

Preview production build locally:

mdxe preview                # Preview ./dist on port 8080
mdxe preview ./dist         # Preview specific build
mdxe preview --port 3000    # Custom port

Configuration (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.css

Reference assets in MDX:

![Hero Image](/assets/images/hero.png)

<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-started

Dynamic Routes

Create dynamic routes with brackets:

content/
  blog/
    [slug].mdx        → /blog/:slug
  products/
    [category]/
      [id].mdx        → /products/:category/:id

In [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.mdx

Deploy with:

mdxe deploy ./docs --name my-docs

Marketing 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', '') },
  }))
}

Next Steps