Technical Writing
How Lecta built its runtime MDX renderer
Why Lecta moved from markdown to runtime MDX, and what it took to make live, AI-generated slides fail clearly.
Markdown got us far in Lecta. It was simple, fast, familiar, and easy for both humans and AI to produce. But it also imposed a ceiling. The moment I wanted slides with stronger visual hierarchy, richer layouts, cards, charts, and something that felt designed instead of merely formatted, plain markdown stopped being enough.
That mattered because Lecta is not a static publishing tool. It’s a presentation app I was invited to build with the team at Orbit. Slides are edited live. AI generates content on the fly. The output has to feel visual, not just legible. I care about that probably more than is healthy. I’m the kind of person who will spend far too long adjusting a slide until it finally feels right. Walls of text lose attention quickly. Flat slides lose it even faster.
Lecta ships with its own MCP to help generate presentations, and in the beginning that flow was markdown-only. It worked, up to a point. You could generate a deck, the content could even be good, but every slide tended to fall into the same shape: a heading, a few bullets, maybe a code block if you were lucky. It was usable, but not expressive.
MDX was the obvious next step
MDX appealed to me for the same reason markdown did in the first place: it keeps content close to text, but it does not force you to give up expressiveness just because the source format is simple.
That made it a strong fit for Lecta. I could keep the basic authoring model people already understood, while adding the ability to render richer layouts and components when a slide needed more than headings and bullet points. In practice, that made the migration path fairly gentle. You can start with markdown, introduce MDX where it helps, and avoid rewriting everything at once.
I had already felt this tension in my own work. I was using HTML to present ideas internally because plain text often wasn’t enough. MDX felt like the cleaner version of that instinct: keep the authoring lightweight, but don’t cap the visual outcome.
This wasn’t an entirely original conclusion either. Tools like Docusaurus helped validate MDX as a serious content format. I didn’t treat that as a blueprint so much as proof that MDX could be more than a novelty. That distinction matters. The difference between a clever feature and a trustworthy authoring system is usually not power. It’s whether people feel safe depending on it.
Where runtime changed everything
That was the real challenge for me when designing the feature.
Docusaurus mostly lives in a build-time world. You run a build, it processes your files, and the output gets deployed. That model makes a lot of sense for documentation. It didn’t match our constraints.
In Lecta, rendering happens while the user is looking at the slide. The content can change at any moment. The AI can produce malformed JSX. A component can be referenced before it exists. Something valid at compile time can still fail during rendering. There is no comfortable build step to hide behind. I compile at runtime, and that changes the design priorities completely.
Once you accept that, the question stops being “how do we support MDX?” and becomes “how do we make sure bad input doesn’t ruin the experience?”
That shift shaped almost everything.
When MDX compilation fails, Lecta does not magically recover the slide. What it tries to do instead is fail in a way that is still useful. When possible, I fall back to plain text rendering and surface a clear error message so you can see both the problem and the underlying content instead of staring at an empty canvas.
When a missing component shows up, something like <FancyChart /> that hasn’t been implemented yet, I don’t want the whole render path to collapse around it. I use recma-mdx-escape-missing-components so unknown components can fail quietly instead of taking the slide down with them.
When runtime compilation becomes expensive, especially in a live editing flow, you cannot afford to redo work unnecessarily. So I cache the processor setup and cache compiled results by content hash. Same content should not be compiled twice if I can avoid it.
I also lazy-load the compiler. If someone never opens an .mdx slide, they never need to pay the cost of downloading the MDX compiler. That matters when the richer path is optional and markdown is still a first-class format.
And even if compilation succeeds, rendering can still fail later in React. That is why error boundaries matter too. A renderer like this cannot only be good at the happy path. It has to be calm when the unhappy path shows up.
The real feature was resilience
The deeper lesson for me was that runtime rendering is less about expressiveness than about failing clearly.
It is easy to get excited about what MDX makes possible: cards, layouts, inline styling, richer components, visually stronger slides. And that part is real. The jump from plain markdown to styled MDX content is meaningful.
Here’s a simple example.
.md — plain text:
# Dashboard
Revenue: $4.2M
Customers: 1,247
Uptime: 99.9%
.mdx — styled cards:
# Dashboard
<div style={{display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px'}}>
<div style={{background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
borderRadius: '14px', padding: '28px', color: 'white'}}>
<div style={{fontSize: '42px', fontWeight: 800}}>$4.2M</div>
<div style={{opacity: 0.7}}>Revenue</div>
</div>
<div style={{background: 'linear-gradient(135deg, #ec4899, #f43f5e)',
borderRadius: '14px', padding: '28px', color: 'white'}}>
<div style={{fontSize: '42px', fontWeight: 800}}>1,247</div>
<div style={{opacity: 0.7}}>Customers</div>
</div>
<div style={{background: 'linear-gradient(135deg, #14b8a6, #06b6d4)',
borderRadius: '14px', padding: '28px', color: 'white'}}>
<div style={{fontSize: '42px', fontWeight: 800}}>99.9%</div>
<div style={{opacity: 0.7}}>Uptime</div>
</div>
</div>
Same content. Very different experience.
But the more important part is what happens when that richer version is broken.
If the JSX has a syntax error, the best outcome is not that the system pretends everything is fine. The best outcome is that it shows as much plain text as it can, makes the error obvious, and keeps the user oriented. If a component doesn’t exist, it should fail quietly instead of taking the whole slide down. If something throws during render, the app should still make the problem legible. In a system sitting between AI output and a live presentation, graceful degradation is really about clarity, not invisibly recovering from bad code.
That, more than MDX itself, was the real design problem.
I should also give credit where it’s due: Pedro Ferreira helped me upgrade Lecta’s MCP server to support MDX and added resilience around the feature so the whole flow held up better in practice.
What comes next
MDX is the foundation, not the finish line.
I’d like to build a component library so the AI can reference higher-level presentation primitives instead of generating raw JSX for everything. Cards, charts, callouts, timelines, all the pieces that let a slide feel intentional without requiring every visual decision to be handwritten.
I’d also like to improve the authoring experience around it. Better syntax shortcuts. Better defaults. Better ways for AI-generated slides to feel visual without becoming fragile.
But the architectural lesson will probably stay the same: if the system is going to accept messy, live, AI-generated input, it has to earn trust by failing clearly.
MDX gave us expressive slides. Designing for failure is what made them usable.