Templates
Experimental.
experimentalUseTemplate()is stable in behavior but the name signals its API may still change before it drops the prefix.
markdown-layout shows one way to reuse a .page.md companion: split it on a divider and wrap chunks in <mark-down>. experimentalUseTemplate() is a different tool for a different shape of problem — parse once, fill many times, for repeating structures like cards in a list or rows in a table. It lives on Component (this.experimentalUseTemplate(...)), so both pages and widgets get it for free.
String templates (SSR)
Define a template in a companion file, then call experimentalUseTemplate(source, id) to get a fill function back. The source and slot syntax differ by file type — the method auto-detects which one it's looking at.
HTML companion: <template id> + <slot name>
<template id="card">
<div class="card">
<h3><slot name="title"></slot></h3>
<p><slot name="body"></slot></p>
</div>
</template>
override renderHTML({ data, context }: this['RenderArgs']) {
if (!data) return '';
const card = this.experimentalUseTemplate(context.files!.html!, 'card');
return data.projects
.map((p) => card({ title: escapeHtml(p.title), body: escapeHtml(p.body) }))
.join('');
}
Markdown companion: `template:id ` + slot:name
```template:card
### slot:title
slot:body
```
override renderMarkdown({ data, context }: this['RenderArgs']) {
if (!data) return '';
const card = this.experimentalUseTemplate(context.files!.md!, 'card');
return data.projects.map((p) => card({ title: p.title, body: p.body })).join('\n\n');
}
Both signatures return (slots?: Record<string, string>) => string. Calling the returned function with no slots returns the raw skeleton — useful for previewing the template shape. Calling it with a partial slot map only fills the slots you provide; unmatched <slot>/slot: markers are left as-is.
DOM templates (browser hydrate())
In the browser, call experimentalUseTemplate(id) with just the id — no source argument. It looks for <template id="..."> inside the widget's own shadow DOM (put the <template> in your .widget.html companion) and returns a function that clones and fills it, producing a DocumentFragment instead of a string:
override hydrate({ data }: this['RenderArgs']) {
const card = this.experimentalUseTemplate('card');
const list = this.element?.shadowRoot?.querySelector('.list');
for (const p of data?.projects ?? []) {
list?.appendChild(card({ title: p.title, body: p.body }));
}
}
This is the one signature that only works client-side — this.element is undefined during SSR, so calling the single-argument form on the server throws.
Markdown-only companions need <mark-down>
If your companion is .md but you're producing renderHTML() output, wrap the filled result in <mark-down> so it goes through your configured markdown renderer, same as any other markdown content:
override renderHTML({ data, context }: this['RenderArgs']) {
if (!data) return '';
const card = this.experimentalUseTemplate(context.files!.md!, 'card');
const filled = data.projects.map((p) => card({ title: escapeHtml(p.title), body: escapeHtml(p.body) })).join('\n\n');
return `<mark-down>${escapeHtml(filled)}</mark-down>`;
}
Errors are loud on purpose
Calling experimentalUseTemplate(source, id) with an id that isn't found in source throws immediately — it does not silently return an empty string. Same for the DOM signature when the <template> isn't in the shadow root. Treat a thrown error here as "the companion file and the code requesting a template have drifted apart," not something to catch and paper over.
When to reach for this vs. plain string building
You don't need experimentalUseTemplate() for one-off HTML — a template literal is simpler. Reach for it when:
- The same fragment repeats many times per render (a list of cards, table rows) and you want the shape to live in a companion file instead of inline strings in
.ts. - You want the shape editable without touching TypeScript — a
.page.htmlor.page.mdcompanion can be handed to someone who doesn't write code. - You're already using companion files for the rest of the page/widget and want the repeating structure to live there too, rather than splitting the content source across a companion file and inline template literals.
Next: Widgets