How to use @temples as a static site generator

The native HTML syntax of temples fits static site generation: plain HTML pages rendered with data from external sources (like a collection of markdown files). The site you are reading is generated this way. Its source lives in packages/docs.

Install

bun add @temples/ssr

Note: The engine operates on a DOM. In the browser that is the native document. On the server there is no DOM, so @temples/ssr (ssr stands for Server Side Rendering) wires the engine to linkedom, a fast DOM implementation, for parsing and serialization in server environments. linkedom is the only dependency of the package.

Prepare a template

prepare(source) wires the engine to linkedom and returns an async render function:

import { prepare } from "@temples/ssr";

const render = prepare("<h1 data-bind='article.title'>Title</h1>");

const html = await render({ article: { title: "The Great Race" } });
console.log(html); // <h1>The Great Race</h1>

Each render call installs the linkedom globals for its duration, renders, and restores the previous globals after. The main @temples/engine entry never imports linkedom, so browser bundles stay small.

Static site generation

For SSG, prepare the layout once, render one page per data set, and write the returned strings to files:

import { prepare } from "@temples/ssr";

const layout = await Bun.file("layout.html").text();
const renderLayout = prepare(layout, { removeDataBindings: false });

for (const [slug, data] of Object.entries(pages)) {
    await Bun.write(`dist/${slug}.html`, await renderLayout(data));
}

This is exactly how the documentation site is built — the complete pipeline lives in src/build.ts.

prepare() — reusable render functions

prepare(source, options) compiles a template source into an async render function. prepare itself is synchronous; the render call is the async boundary. Every call re-parses the source and renders independently, so partial data from one call never leaks into the next:

import { prepare } from "@temples/ssr";

const render = prepare("<p data-bind='message'>Hello</p>");

console.log(await render({ message: "Hello world" })); // <p>Hello world</p>

The engine consumes the data-* control attributes during the render. Rendered components add one more step: with the default options, each component tag is unwrapped to its children, and the result is plain static markup.

Options

OptionDefaultEffect
removeDataBindingstrueUnwrap every rendered component to its children, so no custom tag remains in the output. The engine always consumes the data-* control attributes during the render.
templesComponents[]TemplesComponent classes used by the source (see below).
rehydratefalseReserved: include the component library so custom elements mount and activate in the browser.

The data passed to each render call also seeds the components' global store: an attribute absent from the markup resolves from the render data, and an explicit attribute on the tag wins.

Full pages

prepare accepts a full HTML document (doctype, <html>, <head>, <body>) and returns the complete document — this site is built exactly this way. The source must have a single root element. Multi-root sources throw.

const renderPage = prepare(layoutHtml);

const html = await renderPage({
    site: { pages: [{ title: "Home", url: "index.html" }] },
    page: { title: "My page", content: "<p>Hello</p>" },
});

Server-side rendering of components

Pass the component classes through templesComponents, and each custom tag in the source renders to its component markup. Every component's css is concatenated into one <style> tag in the output, and the global store seeds attributes the markup does not set:

import { prepare } from "@temples/ssr";
import { ArticleCard } from "./article-card.js";

const render = prepare("<article-card></article-card>", {
    templesComponents: [ArticleCard]
});

const html = await render({ title: "Store title" });
// <h4>Store title</h4> ... plus the component's css in a <style> tag

Each call registers a fresh subclass of every component class — linkedom allows a class to be defined only once per window, and every render parses into a fresh window. Component instances are unwrapped to their children, and with the default options the output contains no data-* control attributes and no custom tags: plain, static markup.

Runtime requirements

@temples/ssr is developed and tested on Bun, the runtime of this repository. The shipped build also targets Node (dist/ssr.js), but Node.js and Deno are untested.

Each render call installs the linkedom globals only for its duration and restores the previous values afterwards.