Setting Up emkoma
This guide uses @emkodev/emkoma, a markdown renderer built for emroute. It natively handles
```router-slot
```
and
```widget:name
{"key": "value"}
```
fenced blocks — no custom renderer code needed.
emkoma is currently pre-release. It works well but the API may change.
1. Install
bun add @emkodev/emkoma
2. Server setup
// server.ts
import { Emroute } from '@emkodev/emroute/server';
import { BunFsRuntime } from '@emkodev/emroute/runtime/bun/fs';
import { renderMarkdown } from '@emkodev/emkoma/render';
const appRoot = import.meta.dirname!;
const runtime = new BunFsRuntime(appRoot);
const emroute = await Emroute.create({
markdownRenderer: { render: renderMarkdown },
}, 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 });
},
});
3. Client setup
The default SPA mode is 'root', which ships a JS bundle to the browser. If your main.ts imports @emkodev/emkoma/render, the browser also needs to resolve that specifier — and emroute's auto-generated import map only includes its own packages.
Add an importmap.json so the browser can resolve the bare @emkodev/emkoma/ specifier:
{
"imports": {
"@emkodev/emkoma/": "https://esm.sh/@emkodev/emkoma/"
}
}
buildClientBundles() merges this with the framework's externals. See Browser JS for details on import maps.
Then write main.ts:
// main.ts
import { bootEmrouteApp, MarkdownElement } from '@emkodev/emroute/spa';
import { renderMarkdown } from '@emkodev/emkoma/render';
MarkdownElement.setRenderer({ render: renderMarkdown });
await bootEmrouteApp();
setRenderer() must be called before any <mark-down> elements are connected to the DOM.
SSR-only? If you don't need a SPA, pass
spa: 'none'toEmroute.create()and skip the client setup entirely — nomain.ts, no import map needed. The server still renders markdown via the configuredmarkdownRendererfor/html/*and serves raw markdown at/md/*.
Why emkoma?
With marked or markdown-it, you write a renderer.ts module that overrides fence rules to handle router-slot and widget:* blocks. emkoma handles these conventions out of the box — just import renderMarkdown and pass it directly. No shared renderer module needed.
The trade-off is that emkoma is pre-release and has a smaller community than established parsers. If you need a mature, battle-tested solution, use marked or markdown-it.