In the world of modern web development, content is king. But what if your content could be more than just static text and images? What if it could be alive, interactive, and seamlessly integrated with your application's components? This is the power of MDX, and with mdx.do, you can now bring this power to any application, on-demand.
This tutorial will guide you step-by-step through building a real-time MDX blog post editor. As you type your MDX content—complete with frontmatter and custom components—you'll see a live preview render instantly. We'll accomplish this without complex build configurations or local compilation, thanks to the simple and powerful mdx.do API.
Let's dive in and build something amazing.
Before we start coding, let's clarify the "what" and the "why."
What is MDX? MDX is a powerful authoring format that lets you write JSX directly inside Markdown files. This means you can embed interactive React components (<MyChart />, <InfoBox />) right alongside your regular text, turning static documents into dynamic experiences.
Why compile it on-demand? Typically, MDX content is compiled into HTML or React components during your website's build step. This is great for static sites. But what about dynamic scenarios?
In these cases, you can't pre-compile the content. You need to compile it on-the-fly. This is where mdx.do shines. It's MDX Compilation as a Service, offloading the heavy lifting to a dedicated API so your application stays light and fast.
First, let's get a basic React application up and running. We'll use Vite for a fast and modern setup.
# Create a new React project with Vite
npm create vite@latest mdx-live-editor -- --template react-ts
# Navigate into the project directory
cd mdx-live-editor
# Install dependencies
npm install
Next, we need the mdx.do SDK. This is a lightweight wrapper that makes calling the API incredibly simple.
npm install @do-sdk/mdx
Now, open the project in your favorite code editor and start the development server.
npm run dev
You should see the default Vite React page at http://localhost:5173.
Let's clean up src/App.tsx and create a simple two-pane layout: a textarea for our MDX input and a div for our live preview.
Replace the contents of src/App.tsx with the following:
// src/App.tsx
import { useState } from 'react';
import './App.css'; // We'll add some styles here later
function App() {
const [mdxContent, setMdxContent] = useState('');
return (
<div className="container">
<header>
<h1>Live MDX Editor</h1>
<p>Powered by <a href="https://mdx.do" target="_blank" rel="noopener noreferrer">mdx.do</a></p>
</header>
<main className="editor-layout">
<section className="editor-pane">
<h2>Editor</h2>
<textarea
value={mdxContent}
onChange={(e) => setMdxContent(e.target.value)}
placeholder="Type your MDX here..."
/>
</section>
<section className="preview-pane">
<h2>Live Preview</h2>
<div className="preview-content">
{/* Our compiled component will go here! */}
</div>
</section>
</main>
</div>
);
}
export default App;
Now, add some basic styling to src/App.css:
/* src/App.css */
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
background-color: #242424;
color: rgba(255, 255, 255, 0.87);
}
.container {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 2rem;
}
header a {
color: #61dafb;
}
.editor-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
height: 70vh;
}
.editor-pane, .preview-pane {
display: flex;
flex-direction: column;
}
h2 {
margin-top: 0;
border-bottom: 1px solid #444;
padding-bottom: 0.5rem;
}
textarea {
width: 100%;
height: 100%;
flex-grow: 1;
background: #1a1a1a;
border: 1px solid #444;
border-radius: 8px;
padding: 1rem;
font-family: 'Courier New', Courier, monospace;
font-size: 16px;
color: #fff;
resize: none;
}
.preview-content {
border: 1px solid #444;
border-radius: 8px;
padding: 1rem;
flex-grow: 1;
background: #1a1a1a;
overflow-y: auto;
}
.preview-content h1, .preview-content h2, .preview-content h3 {
margin-top: 0;
}
Your app should now display a clean, side-by-side editor and preview area.
This is where the magic happens. We'll use the mdx SDK to send our textarea content to the API and get a compiled component back.
First, let's import the SDK and add state to hold the compiled result.
// src/App.tsx (top of the file)
import React, { useState, useEffect } from 'react';
import { mdx } from '@do-sdk/mdx'; // Import the SDK
import './App.css';
// ... inside the App component
interface CompilationResult {
component: string | null; // The compiled component code
frontmatter: Record<string, any>; // The parsed frontmatter
error: string | null;
}
function App() {
const [mdxContent, setMdxContent] = useState('');
// State for our compiled result
const [result, setResult] = useState<CompilationResult>({
component: null,
frontmatter: {},
error: null,
});
// ... rest of the component
Now, we'll use a useEffect hook to call the mdx.compile function whenever our input mdxContent changes.
// src/App.tsx (inside the App component)
useEffect(() => {
// Basic debounce to avoid too many API calls while typing
const handler = setTimeout(() => {
if (!mdxContent) {
setResult({ component: null, frontmatter: {}, error: null });
return;
}
const compileMdx = async () => {
try {
const { component, frontmatter } = await mdx.compile({
content: mdxContent
});
setResult({ component, frontmatter, error: null });
} catch (e: any) {
setResult({ component: null, frontmatter: {}, error: e.message });
}
};
compileMdx();
}, 500); // Wait 500ms after the user stops typing
return () => clearTimeout(handler);
}, [mdxContent]);
We've added a simple debounce to be kind to the API. Now, our result state will update with the compiled component code, frontmatter, or any compilation errors.
The mdx.do API returns the compiled React component as a JavaScript string. To render this, we need a secure way to evaluate it and turn it into a real, renderable React component.
Let's create a new component called MdxPreview for this task.
Create src/MdxPreview.tsx:
// src/MdxPreview.tsx
import React, { useState, useEffect, useMemo } from 'react';
// Define any custom components you want to be available in your MDX
const CustomTag = ({ children }: { children: React.ReactNode }) => (
<strong style={{ color: '#ffb86c', fontVariant: 'small-caps' }}>
{children}
</strong>
);
const MdxPreview = ({ code }: { code: string | null }) => {
const customComponents = { CustomTag };
const Component = useMemo(() => {
if (!code) return null;
// The component is wrapped in an IIFE to get the module exports
const getComponent = new Function('React', 'components', code);
const mdxModule = getComponent(React, customComponents);
return mdxModule.default;
}, [code]);
if (!Component) {
return <div>Start typing to see a preview...</div>;
}
// Pass custom components so MDX can use them
return <Component components={customComponents} />;
};
export default MdxPreview;
This MdxPreview component does the critical work:
Now, let's update App.tsx to use our new MdxPreview component and display the results. We'll also add some default content to get us started.
// src/App.tsx (updated)
import React, { useState, useEffect } from 'react';
import { mdx } from '@do-sdk/mdx';
import MdxPreview from './MdxPreview'; // Import the new component
import './App.css';
const defaultContent = `---
title: Hello MDX!
author: '.do'
date: '${new Date().toISOString().split('T')[0]}'
---
# This is a heading
And this is **Markdown** with a <CustomTag>Custom Component</CustomTag>!
- List item 1
- List item 2
\`\`\`javascript
console.log('Hello, world!');
\`\`\`
`;
interface CompilationResult {
component: string | null;
frontmatter: Record<string, any>;
error: string | null;
}
function App() {
const [mdxContent, setMdxContent] = useState(defaultContent);
const [result, setResult] = useState<CompilationResult>({
component: null,
frontmatter: {},
error: null,
});
useEffect(() => {
const handler = setTimeout(() => {
if (!mdxContent) {
setResult({ component: null, frontmatter: {}, error: null });
return;
}
const compileMdx = async () => {
try {
const { component, frontmatter } = await mdx.compile({ content: mdxContent });
setResult({ component, frontmatter, error: null });
} catch (e: any) {
setResult({ component: null, frontmatter: {}, error: e.message });
}
};
compileMdx();
}, 500);
return () => clearTimeout(handler);
}, [mdxContent]);
return (
<div className="container">
<header>
<h1>Live MDX Editor</h1>
<p>Powered by <a href="https://mdx.do" target="_blank" rel="noopener noreferrer">mdx.do</a></p>
</header>
<main className="editor-layout">
<section className="editor-pane">
<h2>Editor</h2>
<textarea
value={mdxContent}
onChange={(e) => setMdxContent(e.target.value)}
placeholder="Type your MDX here..."
/>
</section>
<section className="preview-pane">
<h2>Live Preview</h2>
<div className="preview-content">
{result.error ? (
<pre style={{ color: '#ff5555' }}>{result.error}</pre>
) : (
<>
{Object.keys(result.frontmatter).length > 0 && (
<div className="frontmatter">
<h3>Frontmatter:</h3>
<pre>{JSON.stringify(result.frontmatter, null, 2)}</pre>
</div>
)}
<hr />
<MdxPreview code={result.component} />
</>
)}
</div>
</section>
</main>
</div>
);
}
export default App;
Finally, add a touch more CSS to App.css for the frontmatter display:
/* src/App.css (add to the end) */
.frontmatter {
background: #2d2d2d;
border-radius: 4px;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
}
.frontmatter h3 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
color: #aaa;
}
.frontmatter pre {
margin: 0;
font-size: 0.8rem;
color: #bbb;
}
hr {
border: none;
border-top: 1px solid #444;
margin-bottom: 1rem;
}
And that's it!
You now have a fully functional, live MDX editor. Try changing the text, modifying the frontmatter, or even breaking the syntax to see the error handling in action. Notice how our <CustomTag> component is rendered correctly in the preview, demonstrating the power of embedding interactive elements.
You've just witnessed how mdx.do transforms dynamic content workflows. We built a sophisticated, real-time editor with just a few React components and a single API endpoint. By offloading the complex and resource-intensive compilation process, we kept our front-end application simple, fast, and powerful.
This is just the beginning. Imagine integrating this into a headless CMS for live previews, building a collaborative documentation platform, or allowing users to create their own rich profiles. The possibilities are endless.
Ready to bring your content to life? Get started with mdx.do today!