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.html or .page.md companion 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