Facet Template Engine
A sandboxed, Handlebars-style template engine built for visual page builders and SaaS platforms.
What Is Facet?
Facet is the rendering engine behind FaceFlow. It lets designers and front-end developers write dynamic templates using a familiar Handlebars / Mustache syntax — directly inside a visual editor, with zero server-side coding required.
Unlike traditional CMS templates (which are server-side code files that can execute arbitrary logic), Facet parses every template into an Abstract Syntax Tree (AST) before execution. No eval(), no code injection, no surprises.
The Facet syntax is the universal template engine of FaceFlow, so it can be used in any HTML editing area of FaceFlow, such as variable, layout, component, list-page, and others.
When to Use Facet
| Scenario | Why Facet Fits |
|---|---|
| SaaS / Multi-tenant | Users edit templates in the browser. Facet's sandbox guarantees one tenant can never affect another. |
| Visual page builders | Designers drag-and-drop components — Facet renders them without touching server code. |
| Headless CMS front-ends | Pull page data, user state, and site settings into clean templates with dot-path expressions. |
| Cache-critical sites | Built-in {{#deferred}} block lets you serve fully cached pages while still personalizing per-user content via AJAX. |
| Teams with mixed skill levels | Front-end developers and designers can build templates without learning a server-side language. |
Syntax at a Glance
Facet follows the Handlebars / Mustache convention: double curly braces for output, # for block openers, / for closers.
Output
{{ page.title }} {{! HTML-escaped output }}
{{{ page.body }}} {{! Raw / unescaped output }}
{{ page.title | upper }} {{! Filter chain }}
{{ user.isLoggedin ? "Hi!" : "Guest" }} {{! Ternary }}Blocks
{{#if page.hasChildren}} {{! Conditional }}
...
{{/if}}
{{#each page.images as="img"}} {{! Loop }}
<img src="{{ img.url }}">
{{/each}}
{{#pages selector="template=blog, limit=5" as="post"}} {{! Query }}
<a href="{{ post.url }}">{{ post.title }}</a>
{{/pages}}Comments
{{! stripped from output }}
{{!-- multi-line
comment --}}Variables (Reusable Fragments)
[[pricing-card plan="Pro" price="29"]]That's the entire surface area. If you know Handlebars, you already know Facet.
Key Differences from Handlebars / Mustache
While Facet borrows the {{ }} / {{# }} syntax, it is not a Handlebars or Mustache implementation. Here are the intentional differences:
| Feature | Handlebars / Mustache | Facet |
|---|---|---|
| Execution model | JavaScript runtime | Server-side AST evaluator (no eval()) |
| Filter syntax | Custom helpers | Pipe \| chains: {{ val \| upper \| truncate(50) }} |
| Data queries | External (fetch/API) | Built-in {{#pages}} / {{#page}} with selector syntax |
| Image transforms | Not built-in | {{ img.size(600, 400) }}, .webp, .width(), .height() |
| Loop metadata | @index, @first, @last | loop.index, loop.first, loop.last, loop.even, loop.odd, loop.length + @index sugar |
| Deferred rendering | Not available | {{#deferred}} for full-page cache compatibility |
| Security sandbox | Partial (no built-in policy) | Full: root whitelist, filter whitelist, field blocking, selector sanitization, loop limits |
| Ternary expressions | {{#if}} block only | Inline {{ cond ? "a" : "b" }} |
| Raw output | {{{ }}} | {{{ }}} (same), policy-controlled |
| Partials | {{> partial}} | [[variable-name attr="value"]] with attributes, CSS/JS isolation, and cache modes |
Built-in Context Objects
Every template automatically has access to:
| Object | Description |
|---|---|
page | Current page — title, URL, fields, images, children, parent, etc. |
pages | Query API — find pages by selector from anywhere in the site |
user | Current user — name, login state, roles |
languages / lang | Language list — build multilingual switchers |
site | Site info — name, URL, settings |
now | Current date/time — now.year, now.date, now.datetime |
attr | Attributes passed to a variable/component |
loop | Loop metadata — index, first, last, even, odd, length |
43 Built-in Filters
Filters transform output with the pipe operator. A few highlights:
{{ title | upper }} {{! "HELLO WORLD" }}
{{ title | truncate(50) }} {{! "Hello Wor..." }}
{{ price | currency("€", 2) }} {{! "€29.00" }}
{{ page.created | date("M d, Y") }} {{! "Mar 06, 2026" }}
{{ page.modified | relative }} {{! "3 hours ago" }}
{{ tags | pluck("title") | join(", ") }} {{! "News, Tech, Design" }}
{{ page.image | size(600, 400) }} {{! Resized image URL }}
{{ data | json }} {{! XSS-safe JSON }}
{{ text | slug }} {{! "my-page-title" }}Full list: escape / e, raw, upper, lower, capitalize, trim, truncate, nl2br, striptags, replace, split, slice, pad, wrap, slug, default, number, currency, abs, round, ceil, floor, date, datetime, relative, url_encode, url, json, join, first, last, count, length, reverse, sort, pluck, unique, batch, keys, values, merge, size, md5.
Security Model
| Layer | What It Does |
|---|---|
No eval() | Templates are lexed → parsed → evaluated as an AST tree. No dynamic code execution. |
| Root whitelist | Only approved objects (page, user, site, …) can be accessed. |
| Filter whitelist | Only registered filters can be called. |
| Block whitelist | Only approved block types (if, each, pages, …). |
| Field blocking | Sensitive fields (password, roles, permissions, …) always return null. |
| Selector sanitization | Dangerous patterns (include=, check_access=0) are stripped; limits enforced. |
| Loop limits | Max depth: 3 nested loops. Max items: 500 total per render. |
| Raw output control | {{{ }}} requires explicit policy opt-in. |
Cache-Compatible by Design
Facet integrates with full-page caching out of the box via {{#deferred}}:
{{#deferred skeleton-width="8rem" skeleton-height="1.5em"}}
Welcome, {{ user.name }}!
{{/deferred}}- Server outputs an animated skeleton placeholder.
- Template body is stored in server-side cache.
- After page load, a single AJAX call resolves all deferred blocks at once.
- DOM is hydrated with personalized content.
Result: fully cached pages with per-user dynamic content — no compromises.
Quick Comparison
| Feature | Facet | Twig | Blade | Handlebars.js | Mustache |
|---|---|---|---|---|---|
| Sandbox safety | ✅ Full | ⚠️ Config needed | ❌ Raw code | ✅ | ✅ |
| Server-side | ✅ | ✅ | ✅ | ❌ Client-side | ❌ Client-side |
| Data query built-in | ✅ {{#pages}} | ❌ | ❌ | ❌ | ❌ |
| Image transforms | ✅ .size() .webp | ❌ | ❌ | ❌ | ❌ |
| Full-page cache compat | ✅ {{#deferred}} | ❌ | ❌ | ❌ | ❌ |
| Visual editor integration | ✅ | ❌ | ❌ | ❌ | ❌ |
| Filter pipe syntax | ✅ \| filter | ✅ \| filter | ❌ | ❌ | ❌ |
| Learning curve | Low | Medium | Medium | Low | Very Low |
Learn More
- Full Syntax Reference → — Complete developer guide with all 6 block types, 43 filters, context objects, cookbook recipes, and FAQ.
Facet Template Engine · Part of the FaceFlow platform




Discussion
Join the conversation and share your thoughts
No comments yet. Be the first to share your thoughts!