This document provides a comprehensive analysis of VitePress and Docusaurus architectures, explaining how they work and how MarkoPress implements similar features using Marko.js v6.
- Overview
- VitePress Architecture
- Docusaurus Architecture
- Comparative Analysis
- MarkoPress Implementation
- Implementation Roadmap
- Framework: Vue 3 + Vite
- Build Tool: Vite with custom plugins
- Routing: File-based with dynamic route support
- Theme System: Vue component-based with slot overrides
- Plugin System: Extends Vite plugins with custom hooks
- Framework: React 18/19
- Build Tool: Webpack 5 (with Rspack experimental)
- Routing: MDX-based with plugin-driven route generation
- Theme System: React components with swizzling
- Plugin System: Hooks-based with lifecycle methods
- Framework: Marko.js v6
- Build Tool: @marko/run
- Routing: File-based with route generation
- Theme System: Marko components with slot overrides
- Plugin System: Hooks-based API
vitepress/
├── src/
│ ├── client/ # Vue 3 client code
│ │ ├── app/ # App root and navigation
│ │ ├── theme-default/ # Default theme
│ │ └── composables/ # Vue composables
│ └── node/ # Node.js build tools
│ ├── build/ # Build system
│ ├── cli/ # CLI commands
│ ├── markdown/ # Markdown processing
│ └── plugins/ # Build plugins
- Separation of Concerns: Client and server code are separate
- Plugin-Based: Extends Vite's plugin system
- Convention Over Configuration: File-based routing and content
- Performance-First: LRU caching, code splitting, lazy loading
// VitePress Build Pipeline
async function build() {
// 1. Load Configuration
const config = await resolveConfig();
// 2. Scan Content
const pages = await scanContent(config.srcDir);
// 3. Bundle Phase
const clientResult = await buildClient(config);
const serverResult = await buildServer(config);
// 4. Render Phase (SSG)
for (const page of pages) {
const html = await renderPage(page, serverResult);
await writeHTML(page.path, html);
}
// 5. Post-Processing
await generateSitemap(config);
await optimizeAssets();
}1. Client Bundle
// Uses Vite to bundle Vue app
- Entry: src/client/app/index.ts
- Output: dist/assets/*.js
- Features: Code splitting, tree shaking, HMR2. Server Bundle
- Bundles server-side rendering code
- Includes markdown transforms
- Generates virtual modules for routes3. Static Site Generation
- Pre-renders each route to HTML
- Injects page metadata
- Generates hydration dataFile Structure → Route Path
index.md → /
guide.md → /guide
guide/getting-started.md → /guide/getting-started
// With [slug].md pattern
blog/[slug].md → blog/hello-world
blog/another-post
// .paths.ts defines parameters
export default {
async paths() {
return [
{ slug: 'hello-world' },
{ slug: 'another-post' }
];
}
}export function dynamicRoutesPlugin() {
return {
name: 'vitepress:dynamic-routes',
resolveId(id) {
// Virtual module for route generation
if (id === '@siteData') return '\0siteData';
},
load(id) {
// Return route manifest
return `
export const routes = ${JSON.stringify(routes)};
`;
}
};
}interface VPPlugin {
name: string;
// Config Hooks
config?: (config: UserConfig) => UserConfig;
configResolved?: (config: ResolvedConfig) => void;
// Build Hooks
buildStart?: () => void;
buildEnd?: () => void;
// Content Hooks
transformPageIndex?: (page: PageData) => PageData;
extendFrontmatter?: (fm: Frontmatter) => Frontmatter;
// Markdown Hooks
extendMarkdown?: (md: MarkdownIt) => void;
// Route Hooks
extendRoutes?: (routes: Route[]) => Route[];
}// Markdown enhancement
markdownPlugin({
theme: {
light: 'github-light',
dark: 'github-dark'
},
lineNumbers: true
});
// Custom components
componentsPlugin({
components: {
'Badge': 'Badge.vue',
'VCard': 'VCard.vue'
}
});
// Static data
dataPlugin({
glob: '**/*.json'
});theme-default/
├── Layout.vue # Root layout
├── components/
│ ├── VPSidebar.vue # Sidebar navigation
│ ├── VPNavbar.vue # Top navigation
│ ├── VPDoc.vue # Documentation layout
│ └── VPFooter.vue # Footer
├── composables/
│ useNav.ts # Navigation logic
│ useSidebar.ts # Sidebar logic
└── styles/
└── index.css # Theme styles
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme';
import MyComponent from './MyComponent.vue';
export default {
extends: DefaultTheme,
enhanceApp({ app, router, siteData }) {
// Register global components
app.component('MyComponent', MyComponent);
},
Layout() {
// Override layout
return h(DefaultTheme.Layout, null, {
'nav-bar-content': () => h('div', 'Custom Nav')
});
}
};// 1. Create markdown-it instance
const md = markdownit({
html: true,
linkify: true,
typographer: true
});
// 2. Apply plugins
md.use(anchor); // Anchor links
md.use(attributes); // Custom attributes
md.use(emoji); // Emoji support
md.use(container); // Custom containers
// 3. Vue-specific plugins
md.use(mditVue);
md.use(markdownItVue);
// 4. Syntax highlighting
await setupShiki(md, config);
// 5. Process markdown
const result = md.render(source);::: tip
This is a tip
:::
::: warning
This is a warning
:::
::: danger
This is dangerous
:::<Badge text="v2.0" type="warning" />
<VCard>
Custom content here
</VCard>import { createServer as createViteServer } from 'vite';
import polka from 'polka';
import sirv from 'sirv';
export async function createDevServer() {
// 1. Create Vite dev server
const vite = await createViteServer({
configFile: false,
server: { middlewareMode: true }
});
// 2. Create Polka server
const app = polka();
// 3. Add middlewares
app.use((req, res, next) => {
// Markdown files → Vue transform
if (req.path.endsWith('.md')) {
return transformMarkdown(req.path);
}
next();
});
app.use(vite.middlewares);
app.use(sirv('public'));
// 4. Start server
app.listen(4173);
}- Instant HMR: Markdown changes trigger hot reload
- Virtual Modules: Routes and config injected as virtual modules
- Fast Refresh: Vue component changes preserve state
- Error Overlay: Build errors shown in browser
// 1. Resolve config file
const configFile = await resolveConfigFile(root);
// 2. Load with TypeScript support
const userConfig = await loadConfig(configFile);
// 3. Apply defaults
const config = mergeConfig(defaultConfig, userConfig);
// 4. Validate
await validateConfig(config);
// 5. Resolve references
config.theme = resolveTheme(config.theme);interface UserConfig {
// Site metadata
title: string;
description: string;
base: string;
url: string;
// Theme
theme: string;
themeConfig: ThemeConfig;
// Markdown
markdown: {
theme?: { light?: string; dark?: string };
lineNumbers?: boolean;
config?: (md: MarkdownIt) => void;
};
// Build
srcDir?: string;
srcExclude?: string[];
outDir?: string;
cacheDir?: string;
// Vite
vite?: ViteConfig;
// Appearance
appearance?: boolean | 'dark' | 'light';
// SEO
head?: HeadTag[];
// Locale
locales?: Record<string, LocaleConfig>;
}docusaurus/
├── packages/
│ ├── docusaurus/ # Core package
│ │ ├── src/
│ │ │ ├── client/ # React client code
│ │ │ ├── server/ # Node.js build tools
│ │ │ ├── commands/ # CLI commands
│ │ │ └── plugins/ # Plugin system
│ ├── docusaurus-preset-classic/ # Default preset
│ ├── docusaurus-theme-classic/ # Default theme
│ ├── docusaurus-plugin-content-docs/
│ ├── docusaurus-plugin-content-blog/
│ ├── docusaurus-plugin-content-pages/
│ └── docusaurus-plugin-sitemap/
- Plugin-First: Everything is a plugin
- Preset-Based: Curated plugin bundles
- MDX-Centric: MDX for React component embedding
- Versioning: Built-in docs versioning
- i18n: Comprehensive internationalization
async function build() {
// 1. Load Site
const site = await loadSite();
// 2. Build Plugins
await loadPlugins(site);
// 3. Load Content
await pluginsLoadContent(site);
// 4. Create Webpack Configs
const clientConfig = createClientConfig(site);
const serverConfig = createServerConfig(site);
// 5. Bundle
const clientStats = await webpack(clientConfig);
const serverStats = await webpack(serverConfig);
// 6. SSG
await generateSite(site);
// 7. Post Build
await pluginsPostBuild(site);
}// Client Config
{
entry: {
main: './src/client/index.js'
},
output: {
path: 'build/assets',
filename: 'js/[name].[contenthash:8].js'
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /node_modules/,
name: 'vendor'
}
}
}
}
}// Docs Plugin
{
id: 'docs',
routes: [
{
path: '/docs',
component: '@theme/DocPage',
routes: [
{ path: '/docs/intro', component: '@theme/DocItem' },
{ path: '/docs/guide', component: '@theme/DocItem' }
]
}
]
}
// Blog Plugin
{
id: 'blog',
routes: [
{
path: '/blog',
component: '@theme/BlogListPage',
routes: [
{ path: '/blog/first-post', component: '@theme/BlogPostPage' }
]
}
]
}
// Pages Plugin
{
id: 'pages',
routes: [
{ path: '/', component: './src/pages/index.js' },
{ path: '/about', component: './src/pages/about.js' }
]
}// 1. Plugins scan content
const content = await plugin.loadContent();
// 2. Plugins generate routes
const routes = plugin.contentLoaded(content);
// 3. Routes are merged
const allRoutes = mergeRoutes(plugins);
// 4. React Router config generated
const routerConfig = generateRouterConfig(allRoutes);
// 5. Routes are registered
app.routes = routerConfig;interface Plugin {
name: string;
version: string;
// Lifecycle Hooks
extendCli?: (cli: CLI) => void;
getPathsToWatch?: () => string[];
// Content Hooks
loadContent?: () => Promise<Content>;
contentLoaded?: (params: ContentLoadedParams) => void;
// Build Hooks
configureWebpack?: (config: WebpackConfig) => WebpackConfig;
configurePostCss?: (config: PostCssConfig) => PostCssConfig;
// HTML Hooks
injectHtmlTags?: () => HtmlTags;
// Post Build Hooks
postBuild?: (params: PostBuildParams) => void;
// Theme Hooks
getThemePath?: () => string;
getTypeScriptThemePath?: () => string;
}// Load plugins from config
const plugins = loadPlugins(config);
// Initialize plugins
for (const plugin of plugins) {
// Load plugin module
const module = await import(plugin.path);
// Call initialization
if (module.init) {
await module.init(plugin.options);
}
// Register hooks
registerHooks(plugin.name, module);
}// Example: Docs Plugin
class DocsPlugin {
name = 'docusaurus-plugin-content-docs';
async loadContent() {
// Scan docs directory
const files = await glob('docs/**/*.md');
// Parse frontmatter
const docs = files.map(file => ({
id: getDocId(file),
path: getDocPath(file),
frontmatter: parseFrontmatter(file),
content: await readFile(file)
}));
return { docs };
}
contentLoaded({ content, actions }) {
const { docs } = content;
const { addRoute, createData } = actions;
// Create route data
for (const doc of docs) {
const docData = await createData(
`doc-${doc.id}.json`,
doc
);
// Add route
addRoute({
path: doc.path,
component: '@theme/DocItem',
exact: true,
modules: {
doc: docData
}
});
}
}
}1. Eject (Full Override)
docusaurus swizzle [theme-name] [component-name] --eject2. Wrap (Partial Override)
docusaurus swizzle [theme-name] [component-name] --wrap3. TypeScript Support
docusaurus swizzle [theme-name] [component-name] --typescript// Original component
function DocItem({ content }) {
return (
<div className="doc">
<h1>{content.title}</h1>
<div>{content.body}</div>
</div>
);
}
// Wrapped component
function WrappedDocItem(props) {
// Add custom logic
useEffect(() => {
trackPageView(props.content.id);
}, []);
// Call original
return (
<div className="custom-wrapper">
<OriginalDocItem {...props} />
</div>
);
}// 1. Parse MDX
const mdxAst = parseMDX(source);
// 2. Transform with Remark
const remarkPlugins = [
remarkParse,
remarkFrontmatter,
remarkGfm,
remarkEmoji,
remarkMath
];
const hast = await remark()
.use(remarkPlugins)
.run(mdxAst);
// 3. Transform with Rehype
const rehypePlugins = [
rehypeSlug,
rehypeAutolinkHeadings,
rehypePrism
];
const html = await rehype()
.use(rehypePlugins)
.run(hast);
// 4. Compile to React
const jsx = compileMDX(html);import Button from '@site/src/components/Button';
# Hello World
<Button label="Click me" />
Some content...:::tip
This is a tip
:::
:::warning
This is a warning
:::
:::danger
This is dangerous
:::docs/
├── current/ # Latest version (symlink)
├── v2.0.0/ # Version 2.0.0
├── v1.9.0/ # Version 1.9.0
└── versions.json # Version metadata
// /docs/intro → latest
// /docs/v2.0.0/intro → v2.0.0
// /docs/v1.9.0/intro → v1.9.0
const routes = [
{
path: '/docs/:version/:path*',
component: '@theme/DocItem'
},
{
path: '/docs/:path*',
component: '@theme/DocItem'
}
];function VersionDropdown() {
const versions = useVersions();
const activeVersion = useActiveVersion();
return (
<Dropdown>
{versions.map(version => (
<DropdownItem
href={version.path}
active={version === activeVersion}
>
{version.label}
</DropdownItem>
))}
</Dropdown>
);
}i18n/
├── en/
│ └── docusaurus-plugin-content-docs/
│ └── current/
│ ├── intro.md
│ └── guide.md
├── fr/
│ └── docusaurus-plugin-content-docs/
│ └── current/
│ ├── intro.md
│ └── guide.md
└── ja/
└── docusaurus-plugin-content-docs/
└── current/
// docusaurus.config.ts
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr', 'ja'],
localeConfigs: {
en: {
label: 'English',
direction: 'ltr'
},
fr: {
label: 'Français',
direction: 'ltr'
},
ja: {
label: '日本語',
direction: 'ltr'
}
}
}
};// translations.json
{
"theme.common.skipToContent": "Skip to main content",
"theme.navbar.home": "Home"
}
// Usage
<Translate id="theme.navbar.home">Home</Translate>| Feature | VitePress | Docusaurus | MarkoPress (Target) |
|---|---|---|---|
| Framework | Vue 3 | React 18/19 | Marko.js v6 |
| Build Tool | Vite | Webpack 5 | @marko/run |
| Markdown | markdown-it | MDX | markdown-it |
| Syntax Highlighting | Shiki | Prism.js | Shiki |
| Routing | File-based + Dynamic | Plugin-driven | File-based + Dynamic |
| Theme System | Vue Components | React Components | Marko Components |
| Plugin System | Vite Ext + Hooks | Hooks-based | Hooks-based |
| Versioning | ❌ No | ✅ Yes | ✅ Planned |
| i18n | ✅ Yes | ✅ Yes | ✅ Planned |
| HMR | ✅ Fast | ✅ Instant | |
| Build Speed | ⚡ Fastest | 🐌 Slowest | ⚡ Fast |
| Bundle Size | 📦 Medium | 📦 Largest | 📦 Minimal |
| Aspect | VitePress | Docusaurus | MarkoPress |
|---|---|---|---|
| Routing | File convention | Plugin generation | Plugin generation |
| Content Loading | File scanning | Plugin hooks | Plugin hooks |
| Markdown Processing | markdown-it plugins | MDX + Remark/Rehype | markdown-it plugins |
| Theme Override | Component extends | Swizzling | Slot overrides |
| State Management | Vue composables | React hooks | Marko state |
| Code Splitting | Vite automatic | Webpack chunks | @marko/run automatic |
MarkoPress already implements several core features:
-
File-Based Routing
- Content scanning from
content/directories - Automatic route generation (
content/pages/→/,content/docs/→/docs/*) - Dynamic routes via plugins
- Content scanning from
-
Markdown Processing
- markdown-it with Shiki syntax highlighting
- Custom containers (
::: tip,::: warning,::: danger) - Frontmatter parsing
- GitHub Flavored Markdown
-
Plugin System
- Hooks API (
contentLoaded,beforeBuild,extendMarkdown, etc.) - Built-in content plugins (pages, docs, blog)
- Plugin configuration support
- Hooks API (
-
Theme System
- Slot-based overrides
- Default theme with components
- Layout customization
-
Build System
- @marko/run integration
- Static site generation
- Asset optimization
-
Configuration
- TypeScript config loading
- Site metadata
- Theme configuration
Implementation Plan
// packages/plugin-content-docs/src/versioning.ts
export interface Version {
name: string;
label: string;
path: string;
isLast: boolean;
}
export class VersionManager {
private versions: Version[] = [];
async loadVersions() {
// Scan versioned content directories
const versionDirs = await glob('content/docs/v*');
this.versions = versionDirs.map(dir => ({
name: basename(dir),
label: basename(dir),
path: `/docs/${basename(dir)}`,
isLast: false
}));
// Add current version
this.versions.push({
name: 'current',
label: 'Next',
path: '/docs',
isLast: true
});
}
getVersions(): Version[] {
return this.versions;
}
getCurrentVersion(): Version {
return this.versions[this.versions.length - 1];
}
}Route Generation
// Generate versioned routes
export function generateVersionedRoutes(versions: Version[]) {
const routes = [];
for (const version of versions) {
const docs = await loadDocs(version.name);
for (const doc of docs) {
routes.push({
path: version.isLast
? `/docs/${doc.slug}`
: `/docs/${version.name}/${doc.slug}`,
component: 'DocPage',
meta: {
version: version.name,
doc: doc.id
}
});
}
}
return routes;
}UI Component
<!-- theme-default/src/components/VersionDropdown.marko -->
<dropdown>
<button slot="trigger">
${state.activeVersion.label}
<svg class="caret" />
</button>
<ul slot="content">
<for|version of input.versions|>
<li>
<a href=version.path>
${version.label}
</a>
</li>
</for>
</ul>
</dropdown>Implementation Plan
// packages/markopress/src/i18n/index.ts
export interface LocaleConfig {
label: string;
lang: string;
direction: 'ltr' | 'rtl';
}
export class I18nManager {
private locales: Map<string, LocaleConfig> = new Map();
private defaultLocale: string;
constructor(config: I18nConfig) {
this.defaultLocale = config.defaultLocale;
for (const [locale, cfg] of Object.entries(config.locales)) {
this.locales.set(locale, cfg);
}
}
getLocale(path: string): string {
// Extract locale from path
const match = path.match(/^\/([a-z]{2})\//);
return match ? match[1] : this.defaultLocale;
}
getLocales(): LocaleConfig[] {
return Array.from(this.locales.entries()).map(([code, config]) => ({
code,
...config
}));
}
async loadTranslationFile(locale: string, domain: string) {
const path = `i18n/${locale}/${domain}.json`;
return JSON.parse(await readFile(path));
}
}Translation Function
// src/i18n/translate.ts
export function createTranslator(locale: string) {
const translations = loadTranslations(locale);
return function translate(
id: string,
args: Record<string, string> = {}
): string {
let message = translations[id] || id;
// Replace placeholders
for (const [key, value] of Object.entries(args)) {
message = message.replace(`{${key}}`, value);
}
return message;
};
}Marko Component
<!-- Translate Component -->
<translate|id, args={}|>
${$global.t(id, args)}
</translate>
<!-- Usage -->
<translate id="theme.navbar.home" />
<translate id="theme.common.welcome" args="{name: 'User'}" />Table of Contents
// packages/markopress/src/markdown/toc.ts
export interface TocItem {
level: number;
title: string;
anchor: string;
children: TocItem[];
}
export function extractTOC(markdown: string): TocItem[] {
const headings = markdown.match(/^#{1,6}\s+(.+)$/gm) || [];
return headings.map(heading => {
const level = heading.match(/^#{1,6}/)![0].length;
const title = heading.replace(/^#{1,6}\s+/, '');
const anchor = slugify(title);
return { level, title, anchor, children: [] };
});
}Code Group
:::code-group
```bash [npm]
npm install markopresspnpm install markopressyarn add markopress:::
**Implementation**
```typescript
// Markdown-it plugin
export function codeGroupPlugin(md: MarkdownIt) {
md.use(container, 'code-group', {
render: (tokens, idx) => {
const token = tokens[idx];
if (token.nesting === 1) {
return '<div class="code-group">';
} else {
return '</div>';
}
}
});
// Parse code block titles [label]
md.renderer.rules.fence = function(..., info) {
const match = info.match(/\[([^\]]+)\]$/);
const label = match ? match[1] : '';
// Render code block with label
};
}
Implementation Plan
// packages/plugin-search/src/index.ts
interface SearchDocument {
id: string;
title: string;
content: string;
path: string;
}
export class SearchIndexer {
async buildIndex(docs: SearchDocument[]) {
// Build MiniSearch index
const index = new MiniSearch({
fields: ['title', 'content'],
storeFields: ['title', 'path']
});
index.addAll(docs);
// Write to file
await writeFile('public/search-index.json', JSON.stringify(index));
}
}Search Component
<!-- theme-default/src/components/Search.marko -->
<div class="search">
<input
type="search"
placeholder="Search docs..."
on-input("handleInput")
/>
<if(input.results.length > 0)>
<ul class="search-results">
<for|result of input.results|>
<li>
<a href=result.path>
<h3>${result.title}</h3>
<p>${result.excerpt}</p>
</a>
</li>
</for>
</ul>
</if>
</div>
<script>
import { loadIndex } from 'search:mini-search';
let index = null;
function onMount() {
index = loadIndex();
}
function handleInput(event) {
const query = event.target.value;
const results = index.search(query);
// Update UI
}
</script>Lifecycle Hooks
// packages/markopress/src/plugin/types.ts
export interface MarkoPressPlugin {
name: string;
// Configuration hooks
config?: (config: UserConfig) => UserConfig;
configResolved?: (config: ResolvedConfig) => void;
// Content hooks
contentLoaded?: (ctx: ContentContext) => void | Promise<void>;
// Build hooks
beforeBuild?: (ctx: BuildContext) => void | Promise<void>;
afterBuild?: (ctx: BuildContext) => void | Promise<void>;
buildEnd?: (ctx: BuildContext) => void | Promise<void>;
// Markdown hooks
extendMarkdown?: (md: MarkdownIt) => void;
// Route hooks
extendRoutes?: (routes: RouteManifest) => RouteManifest;
// HTML hooks
transformHead?: (tags: HeadTag[]) => HeadTag[];
// Dev server hooks
configureDevServer?: (server: DevServer) => void;
// Theme hooks
extendTheme?: (theme: ThemeConfig) => ThemeConfig;
}Plugin Example
// Example: Image optimization plugin
export const imageOptPlugin: MarkoPressPlugin = {
name: 'image-optimization',
async beforeBuild(ctx) {
// Find all images
const images = await glob('content/**/*.{png,jpg,jpeg,webp}');
// Optimize images
for (const image of images) {
await sharp(image)
.resize(1920, 1080, { fit: 'inside' })
.webp({ quality: 80 })
.toFile(image.replace(/\.\w+$/, '.webp'));
}
},
extendMarkdown(md) {
// Transform image links
md.renderer.rules.image = function(tokens, idx) {
const src = tokens[idx].attrGet('src');
const webpSrc = src.replace(/\.\w+$/, '.webp');
return `<picture>
<source srcset="${webpSrc}" type="image/webp">
<img src="${src}" loading="lazy">
</picture>`;
};
}
};- File-based routing
- Markdown processing with Shiki
- Plugin system with hooks
- Theme system with slots
- Build system with @marko/run
- Enhanced markdown features (code groups, tabs)
- Table of contents generation
- Prev/next navigation
- Versioning system for docs
- Internationalization (i18n)
- Search functionality
- Tag and category system for blog
- Reading time estimation
- Content pagination
- Dark mode toggle
- Responsive navigation
- Scroll progress indicator
- Breadcrumb navigation
- Print-friendly styles
- Keyboard shortcuts
- TypeScript strict mode
- Content validation
- Broken link checker
- Linting for markdown
- CLI improvements
- Performance monitoring
- Multiple content sources (Git, CMS)
- Incremental build
- Edge deployment support
- API routes
- Server components
- Analytics integration
VitePress and Docusaurus both provide excellent static site generation with different approaches:
- VitePress prioritizes simplicity and performance with Vite's instant HMR
- Docusaurus offers enterprise features like versioning and comprehensive i18n
MarkoPress combines the best of both worlds with Marko.js v6's exceptional performance:
- ✅ Fast builds via @marko/run
- ✅ Instant HMR with Marko's reactivity
- ✅ Minimal bundles with Marko's compiled output
- ✅ Plugin system inspired by Docusaurus
- ✅ File-based routing like VitePress
The implementation roadmap ensures MarkoPress achieves feature parity while maintaining superior performance characteristics.