Component Architecture
This document explains the architectural decisions behind our custom component system and why we chose the parser transform approach over alternatives.
The Problem
Documentation sites need reusable components like:
- Code blocks with syntax highlighting
- Collapsible sections
- Callouts and alerts
- Tabbed content
- File embeddings
The question: How do we enable custom components in markdown?
Options Considered
We evaluated three approaches:
Option 1: MDX with JSX Components
How it works: MDX compiles markdown + JSX together, allowing React/Astro component imports.
import CodeBlock from '@/components/CodeBlock';
import Callout from '@/components/Callout';
# My Doc
<CodeBlock language="python" title="example.py">
{`print("hello")`}
</CodeBlock>
<Callout type="warning">
Be careful!
</Callout>
Pros:
- Most flexible and powerful
- Full component ecosystem
- Can use any React/Astro component
Cons:
- Slowest build times (full AST parsing and compilation)
- Largest bundle sizes (may include React runtime)
- Complex syntax (imports, JSX rules, curly braces)
- Mixing markdown and JSX is error-prone
- Hard for AI to generate correctly
- Requires
.mdxextension
Option 2: Web Components (Custom Elements)
How it works: Define custom HTML elements with JavaScript that the browser upgrades at runtime.
# My Doc
<code-block language="python" title="example.py">
print("hello")
</code-block>
<callout-box type="warning">
Be careful!
</callout-box>
// Separate JS file
class CodeBlock extends HTMLElement {
connectedCallback() {
// Render component
}
}
customElements.define('code-block', CodeBlock);
Pros:
- Browser standard (works everywhere)
- Clean HTML-like syntax
- No build step for the components themselves
Cons:
- Requires JavaScript at runtime
- Custom elements must have hyphens (
code-block, notcodeblock) - Need separate JS files to define each component
- Behavior defined outside of markdown
- Hydration complexity for SSG sites
Option 3: Parser Transform (Chosen)
How it works: Custom tags in markdown are transformed to plain HTML during the build process.
# My Doc
<codeblock language="python" title="example.py">
print("hello")
</codeblock>
<callout type="warning">
Be careful!
</callout>
Gets transformed at build time to:
<div class="codeblock">
<div class="codeblock-header">example.py</div>
<pre><code class="language-python">print("hello")</code></pre>
</div>
<div class="callout callout-warning">
<p>Be careful!</p>
</div>
Pros:
- Zero runtime JavaScript needed
- Fastest builds (simple string transforms)
- Smallest output (plain HTML + CSS)
- Cleanest syntax for authors
- Works in plain
.mdfiles - Most AI-friendly (simple, predictable patterns)
- Full control over output HTML
- Easy to customize and extend
Cons:
- Components are static (no client interactivity without adding JS)
- Need to implement transforms manually
- Less flexible than full JSX
Comparison Summary
| Criteria | MDX + JSX | Web Components | Parser Transform |
|---|---|---|---|
| Build Performance | Slow | Fast | Fast |
| Runtime Performance | Varies | JS overhead | Zero overhead |
| Bundle Size | Large | Medium | Small |
| Syntax Simplicity | Complex | Medium | Simple |
| AI-Friendly | Poor | Good | Excellent |
| File Extension | .mdx only |
.md or .mdx |
.md or .mdx |
| Interactivity | Full | Full | CSS-only* |
| Learning Curve | Steep | Medium | Low |
*Interactive features like collapsible sections use CSS (:target, <details>) or minimal JS.
Why We Chose Parser Transform
For a documentation system, the priorities are:
- Fast builds - Docs can have hundreds of pages
- Simple authoring - Writers shouldn't need to learn JSX
- AI compatibility - LLMs should easily generate correct syntax
- Minimal JS - Documentation is mostly static content
- Plain markdown - Keep files portable and tool-agnostic
The parser transform approach excels at all of these.
Performance Wins
- No MDX compilation overhead
- No component tree reconciliation
- Output is pure HTML that browsers render instantly
- Smaller page sizes = faster load times
Authoring Wins
// Simple and clean
<codeblock language="python">
[[./assets/example.py]]
</codeblock>
// vs MDX complexity
import { CodeBlock } from '@/components';
<CodeBlock language="python">
{fs.readFileSync('./assets/example.py')}
</CodeBlock>
AI Wins
When asking an AI to write documentation:
- Parser syntax: AI writes simple HTML-like tags ✓
- MDX syntax: AI must handle imports, JSX rules, escaping ✗
Implementation
The transform happens in src/loaders/data.ts:
- Content is loaded from
.mdor.mdxfiles [[path]]syntax is replaced with file contents- Custom tags are transformed to semantic HTML
- Result is passed through markdown renderer
- Final HTML is rendered in the page
This keeps the complexity in one place (the data loader) and gives authors a simple, consistent syntax.
When to Use MDX Instead
MDX is still valuable when you need:
- Highly interactive components (forms, live code editors)
- Components with complex state management
- Integration with React ecosystem libraries
- Dynamic content based on props/context
For these cases, use .mdx files with imported components.
Conclusion
The parser transform approach gives us the best balance of:
- Performance (fast builds, zero runtime JS)
- Simplicity (clean syntax, works in
.md) - Maintainability (transforms in one place)
- AI compatibility (predictable patterns)
For documentation, this is the right trade-off. Save MDX for when you truly need its power.