Every site on our platform looks like its own business — its own colors, pages, navigation, voice. Underneath, every one of them renders through the same pipeline: one logic-less template language, a per-site file tree, and a handful of conventions that decide where markup ends and metadata begins. This post is about those conventions — the unglamorous choices that let one rendering system serve thousands of sites without becoming thousands of special cases.
TL;DR: A theme is a versioned file tree per site — templates, assets, and metadata. Templates are Handlebars: deliberately logic-less, which is what makes them safe to run at multi-tenant scale and safe for AI to write. Every page template travels with a JSON sidecar holding its SEO and social metadata — same path, different extension. Shared base templates render whole families of pages from data, and plugins surface their own views into the tree, so a booking page is themeable exactly like a homepage. The theme edits like code because it is code: branches, statuses, reviewable change.
The theme is a file tree, and that's the whole trick
The foundational choice sounds boring: a site's theme is a directory of files — templates, partials, assets, page metadata — held in a versioned working copy per site, with branches and per-file status (modified, new, deleted) like any codebase. Every nice property downstream falls out of this one decision. The theme is diffable, so "what changed?" has an answer. It's branchable, so structural change happens off the live path. It's a tree of artifacts, so both humans and AI edit the same real thing — the artifacts-over-configuration principle, applied to the most visible layer of the product.
Why the template language refuses to be powerful
Templates are Handlebars — a language whose defining feature is what it can't do. No arbitrary expressions, no side effects, no reaching into the system: a template can interpolate values, walk loops and conditionals, and call helpers — the platform's controlled vocabulary for anything smarter. At single-site scale this restraint is a style preference. At multi-tenant scale it's load-bearing three times over:
- Safety. Thousands of sites render in shared infrastructure. A logic-less template is data with placeholders, not a program — there is no template that can loop forever, exfiltrate another tenant's context, or take down a render node, because the language has no words for any of that.
- Predictability. Rendering cost stays proportional to output size. Performance work happens in helpers and data assembly — code we own and profile — never in ten thousand templates we don't.
- Writability — by machines. The same constraints that make templates safe to run make them tractable to generate: when AI edits a page, it's composing a constrained vocabulary, not writing open-ended code. The smaller the language, the smaller the space of plausible-but-wrong.
The recurring theme of this series, again: constraints installed upstream are what make speed safe downstream — the design-token argument, one layer lower.
The sidecar pattern: metadata beside, never inside
Every page template has a shadow: page.hbs renders the markup, and page.json — same path, different extension — carries everything about the page: titles, descriptions, social-sharing images, the OG fields search and social crawlers read. We call them sidecars, and the separation pays in both directions:
- Machines edit metadata without touching markup. Updating a page's SEO fields is a JSON edit — no template parsing, no risk of breaking layout to change a description. Our SEO audit goes further: when a page is scored, the score and findings persist into its sidecar, so quality data lives with the page it describes rather than in a report nobody reopens.
- Diffs stay honest. A metadata change and a design change are different files in the history — review reads the way the change actually happened.
The pattern generalizes past pages: the theme tree also carries manifest files for things like its animation catalog — structured declarations beside the artifacts they describe. Anywhere two kinds of change have two kinds of reviewer, two files beat one.
One template, a thousand pages
The file tree would bloat fast if every page were its own template. The leverage move is shared bases: a single family template renders every page of its kind, with data deciding the rest. Our help center is the worked example — hundreds of documentation pages, nested sections, per-topic landing pages, all rendered by a shared base for content collections plus the structure encoded in tags and paths. Add a help article and no template changes; the family template renders one more member.
This is the engineering reality under the product advice we've given about templates and reusable sections: a page type is a template; a page is data. When the shape improves, the family improves — the same one-edit-propagates-everywhere economics, enforced by the renderer rather than by discipline.
Plugins bring their own views
The subtlest convention is the one that keeps the platform extensible. Features like booking, ecommerce, and restaurant ordering ship as plugins — and a plugin's pages need to look like your site, not like a widget bolted on. The mechanism: active plugins surface their views and manifest data into the theme's file tree, alongside the site's own templates. The booking page is themeable exactly like the homepage, with the same tokens, the same language, the same editing experience.
The seam is clean and worth stating: the plugin owns behavior; the theme owns appearance. Availability logic, cart math, order state live in the plugin where they're maintained and upgraded for everyone — while every site styles the storefront as its own. The enrichment is dynamic, driven by which plugins are actually active, so the theme tree always reflects the site's real capabilities rather than a hardcoded list of what sites usually have.
What we'd tell anyone building a multi-tenant renderer
- Pick a template language for what it forbids. Every power you grant templates is a power you grant every tenant, forever. Helpers are the escape hatch you control; the language should have no others.
- Sidecar your metadata. Markup and meta have different editors, different reviewers, and different blast radii — give them different files and half your tooling problems dissolve.
- Make page types templates and pages data. The render path should enforce the reuse your design system promises.
- Let extensions join the tree. If a plugin's views live outside the theme, they'll look outside the theme. The integration point is the file tree itself.
- Treat the theme as code, because it is. Versions, branches, statuses, review — the editing model is the safety model.
Key takeaways
- One boring decision carries it all: the theme as a versioned file tree makes everything diffable, branchable, and editable by humans and AI alike.
- The template language is chosen for what it forbids: logic-less templates are data with placeholders — safe at multi-tenant scale, predictable to render, tractable for machines to write.
- Sidecars separate about from is: page.hbs renders, page.json describes — SEO meta and even audit scores live beside the page, and diffs read the way changes happened.
- Page types are templates; pages are data: shared family bases render hundreds of pages each, so improving a shape improves its whole family by construction.
- Plugins join the tree: active plugins surface views into the theme, so the booking page themes like the homepage — plugin owns behavior, theme owns appearance.
- The editing model is the safety model: branches, statuses, and review aren't process overhead — they're what makes thousands of live sites changeable.
Frequently asked questions
Doesn't a logic-less language just push complexity into helpers?
Yes — deliberately. The complexity exists either way; the question is whether it lives in ten thousand tenant templates or in one helper library the platform team owns, tests, versions, and profiles. Centralizing it means a performance fix or a security patch lands everywhere at once, and a template author (human or AI) composes reviewed capabilities instead of reinventing them. The trade is real — adding a genuinely new capability requires a platform change, not a template hack — and we consider that the system working.
How do thousands of sites stay fast if everything renders through one pipeline?
The constraint is the performance strategy: logic-less templates render in time proportional to their output, so the variable cost lives in data assembly — queries and helpers we control and can cache. One pipeline also means one place to optimize: a rendering improvement ships to every site simultaneously, which is the multi-tenant dividend. The pathological cases a powerful template language invites — the accidental N+1 in a loop, the template that computes — simply have no syntax to exist in.
What stops a theme edit from breaking a live site?
The same things that protect any codebase: the working copy is versioned, changes carry per-file status, structural work happens on branches, and the page-level publish flow gives outward changes a deliberate step. The deeper protection is architectural — because templates can't contain logic, the blast radius of a bad edit is visual, not behavioral: a broken layout, never a broken checkout. Behavior lives in plugins and helpers, behind their own review.
Why Handlebars specifically, rather than a newer component framework?
The honest answer: the properties we need — logic-less, mature, boring, renderable on the server at predictable cost — matter far more than novelty. Component frameworks optimize for application interactivity owned by one team; a theme layer optimizes for safe variation across thousands of tenants who never coordinate. Those are different problems. Interactivity on our pages comes from purpose-built layers (forms, plugins, motion) composed into templates — each powerful in its domain, none granted the run of the render path.
The rendering pipeline ships under every Faster site — one language, your face on it. More engineering notes: the engineering blog.