Markdown Layout
A single .page.md companion is the source of truth, but renderHTML() doesn't have to render it as one flat document. Insert a custom divider (an HTML comment is a safe pick), split on it inside renderHTML(), and arrange the resulting chunks however you want. Strip the divider in renderMarkdown() and the /md/* view stays a clean, continuous document for LLMs and curl. This page is rendered exactly that way — the cards below, the callout, and the footer are all virtual chunks from one .page.md file.
The divider
Any HTML comment works — pick a string that's distinctive enough that you won't accidentally type it in content.
# Page title
Intro paragraph above the cards.
<!-- chunk -->
## First card
card body
HTML comments survive in most markdown renderers but never render as visible content, so they're a clean anchor.
renderHTML splits
Read context.files.md, split on the divider, wrap each chunk in <mark-down> so the renderer turns it back into HTML:
const DIVIDER = '<!-- chunk -->';
override renderHTML({ context }: this['RenderArgs']) {
const md = context.files?.md ?? '';
const [lead, ...cards] = md.split(DIVIDER);
return `
<div class="lead"><mark-down>${escapeHtml(lead.trim())}</mark-down></div>
<div class="cards">
${cards.map((c) => `<div class="card"><mark-down>${escapeHtml(c.trim())}</mark-down></div>`).join('')}
</div>
`;
}
Each chunk goes through the configured markdown renderer independently — headings still get IDs, lists still work, anything markdown does works.
renderMarkdown strips
Replace the divider with a blank line so the /md/* endpoint reads as one continuous document — no marker litter:
override renderMarkdown({ context }: this['RenderArgs']) {
const md = context.files?.md ?? '';
return md.split(DIVIDER).map((s) => s.trim()).filter(Boolean).join('\n\n');
}
LLMs and shell scripts read /md/my-page as a single page — they never see the layout machinery.
Skip markers inside code blocks
A naive split() is a literal string match: if your divider appears inside a fenced code block — for example, a page that documents the technique — it splits there too, breaking the demo. Walk the markdown line-by-line and toggle an inCodeBlock flag on lines that start with ` , then ignore markers while the flag is on. This page uses that approach so the example snippets can mention the marker freely without self-splitting.
Named chunks
If you want richer layouts, give markers names — <!--==chunk:intro==-->, <!--==chunk:card==-->, <!--==chunk:note==-->. The parser captures the name; renderHTML() then picks chunks by role and assembles them into a specific layout instead of a generic grid. That's how this page is rendered — the intro at the top, the three cards above, the callout, and this footer live in the same .page.md file, each tagged with a name.
The parser is one scan of the file. Lines that match the marker pattern become chunk boundaries; everything else accumulates into the current chunk. The inCodeBlock flag makes it robust against markers inside fenced examples.
const CHUNK_LINE = /^<!--==chunk(?::([a-z][a-z0-9-]*))?==-->$/;
function parseChunks(md: string): { name: string; content: string }[] {
const lines = md.split('\n');
const chunks: { name: string; content: string }[] = [];
let currentName = '';
let buffer: string[] = [];
let inCodeBlock = false;
for (const line of lines) {
if (line.startsWith('```')) {
inCodeBlock = !inCodeBlock;
buffer.push(line);
continue;
}
if (!inCodeBlock) {
const match = line.match(CHUNK_LINE);
if (match) {
const content = buffer.join('\n').trim();
if (content) chunks.push({ name: currentName, content });
currentName = match[1] ?? '';
buffer = [];
continue;
}
}
buffer.push(line);
}
const tail = buffer.join('\n').trim();
if (tail) chunks.push({ name: currentName, content: tail });
return chunks;
}
override renderHTML({ context }: this['RenderArgs']) {
const chunks = parseChunks(context.files?.md ?? '');
const byName = (n: string) => chunks.filter((c) => c.name === n);
const intro = byName('intro')[0]?.content ?? '';
const cards = byName('card');
// ... compose layout from named chunks
}
The trick respects emroute's three rendering contexts: /html/* gets the visual layout, /md/* stays semantically clean, and the SPA reuses the same <mark-down> elements after navigation. There's no second source of truth, no templating language — just one markdown file, one parse, and CSS.
This divider technique is for splitting one document into named regions. For the opposite problem — one small template filled many times (cards in a list, table rows) — see Templates.
Next: Templates