Markdown Renderers

.page.md files need a markdown renderer to become HTML. Configure one on the server (markdownRenderer on Emroute.create()) and the matching one on the client (MarkdownElement.setRenderer()) — same module on both sides gives identical output everywhere.

emkoma

Built for emroute. Handles ` `router-slot and `widget:name ` fences natively — no custom renderer code. Pre-release but stable.

Setting up emkoma →

marked

Fast and lightweight (~129KB). Custom renderer API lets you intercept fence blocks and convert them into the elements emroute expects.

Setting up marked →

markdown-it

Full CommonMark, large plugin ecosystem (~362KB). Use when you want a mature parser and you don't mind writing the fence adapter.

Setting up markdown-it →

Interface

interface MarkdownRenderer {
  render(markdown: string): string;
}

Any function that takes a markdown string and returns an HTML string works. Other parsers (micromark, unified/remark, showdown) also work — you just need to implement the fenced block conventions below.

Fenced block conventions

emroute uses fenced code blocks with special language identifiers:

Router slots — ` `router-slot ` :

```router-slot
```

Must render as <router-slot></router-slot>. Without this, pages using only .page.md can't nest child routes.

Widgets — ` `widget:<name> ` with optional JSON body:

```widget:counter
{"start": "42"}
```

Must render as <widget-counter start="42"></widget-counter>. The JSON body becomes HTML attributes on the custom element.

Why both client and server?

ContextWhat renders markdownWhen it runs
SPA (/app/*)MarkdownElement in the browserClient navigates to a .page.md route
SSR HTML (/html/*)markdownRenderer on the serverServer handles an /html/* request
SSR Markdown (/md/*)Nothing — returns raw markdownServer returns plain text as-is

Without the client-side renderer, SPA navigation to a markdown page shows raw text. Without the server-side renderer, /html/* routes wrap markdown in <mark-down> tags instead of rendering it to HTML. Use one shared module from both server.ts and main.ts so output stays identical.

Security

The output of render() is assigned to innerHTML in the browser and served as HTML from the server. Your renderer is responsible for sanitizing its output.

ScenarioRecommendation
Trusted content (your own .page.md files)Enable HTML passthrough for full flexibility
Untrusted content (user-submitted markdown)Keep raw HTML escaping enabled (parser default)
MixedSanitize the output of render() before returning it

Most markdown parsers escape raw HTML by default. Only enable HTML passthrough when you control the markdown source.

Next: Runtime