Setting Up marked
This guide uses marked, a fast and lightweight markdown parser with a custom renderer API.
1. Install
bun add marked
2. Create a shared renderer module
Create a single module shared by both client and server:
// renderer.ts
import { marked } from 'marked';
/**
* Convert a JSON string to HTML attributes.
* Used by widget and router-slot fenced blocks.
*/
function jsonToAttrs(text: string): string {
const trimmed = text.trim();
if (!trimmed) return '';
try {
const obj = JSON.parse(trimmed) as Record<string, unknown>;
return Object.entries(obj)
.map(([k, v]) => ` ${k}="${String(v).replace(/"/g, '"')}"`)
.join('');
} catch {
return '';
}
}
marked.use({
renderer: {
// Pass through raw HTML unchanged (default escapes it).
// Only enable this if you trust your markdown content source.
html: ({ text }) => text,
code: ({ text, lang }) => {
// ```widget:counter → <widget-counter start="42">
if (lang?.startsWith('widget:')) {
const name = lang.slice('widget:'.length);
const attrs = jsonToAttrs(text);
return `<widget-${name}${attrs}></widget-${name}>`;
}
// ```router-slot → <router-slot>
if (lang === 'router-slot') {
const attrs = jsonToAttrs(text);
return `<router-slot${attrs}></router-slot>`;
}
// Regular code blocks
const cls = lang ? ` class="language-${lang}"` : '';
return `<pre><code${cls}>${text}</code></pre>`;
},
},
});
export function render(markdown: string): string {
return marked.parse(markdown, { async: false }) as string;
}
The code override intercepts fenced blocks:
```router-slot
```
becomes <router-slot>, and
```widget:counter
{"key": "value"}
```
becomes <widget-counter>. The JSON body (if present) is parsed into HTML attributes.
3. Server setup
// server.ts
import { Emroute } from '@emkodev/emroute/server';
import { BunFsRuntime } from '@emkodev/emroute/runtime/bun/fs';
import { render } from './renderer.ts';
const appRoot = import.meta.dirname!;
const runtime = new BunFsRuntime(appRoot);
const emroute = await Emroute.create({
markdownRenderer: { render },
}, runtime);
Bun.serve({
port: 1420,
async fetch(req) {
const response = await emroute.handleRequest(req);
if (response) return response;
return new Response('Not Found', { status: 404 });
},
});
4. Client setup
// main.ts
import { bootEmrouteApp, MarkdownElement } from '@emkodev/emroute/spa';
import { render } from './renderer.ts';
MarkdownElement.setRenderer({ render });
await bootEmrouteApp();
setRenderer() must be called before any <mark-down> elements are connected to the DOM.
5. HTML passthrough
The html renderer override controls how raw HTML tags in markdown are handled:
// Pass through raw HTML (trusted content only)
html: ({ text }) => text,
// Escape raw HTML (safe for untrusted content — marked's default)
// Simply remove the html override
When passthrough is enabled, any HTML in the markdown source is included verbatim. This is useful for embedding custom elements in .page.md files, but is a security risk with untrusted content.