Facet Template Syntax
Facet Template Syntax
Facet is the template language used by FaceFlow for dynamic rendering.
Use this page as the primary syntax guide. It covers the patterns technical users actually need when writing Component templates, Variable templates, List item output, and other runtime fragments inside FaceFlow.
This page is intentionally detailed. It is not only a quick-start guide. It is the main syntax document for official FaceFlow template authoring.
Syntax Families
Facet templates usually combine five syntax families:
| Syntax | Role | Typical Use |
|---|---|---|
{field} | Component field output | direct output from a Component field |
{{ expr }} | escaped expression output | page data, context objects, filters |
{{{ expr }}} | raw output | trusted rich content and controlled markup |
{{#block}}...{{/block}} | control flow | conditions, loops, page queries, deferred output |
[[variable-name]] | reusable fragment embedding | shared template fragments with optional attributes |
The most common source of confusion is mixing Component field shorthand with general Facet expressions. Keep that boundary clear:
- use
{field}when the owning Component field contract supports direct field output or field helpers - use
{{ ... }}when rendering general context-aware expressions - use
[[variable-name]]when the correct solution is reuse, not more template logic
Quick Start
The minimum useful Facet mental model is:
<section class="hero">
<h1>{{ page.title }}</h1>
{{#if summary}}
<p>{{ summary }}</p>
{{/if}}
[[sales-contact-badge]]
</section>That single example already shows the three most important building blocks:
- value output
- conditional structure
- reusable fragment composition
Output Syntax
Component Field Output
In Component templates, direct field output often uses single-brace syntax:
<h2>{title}</h2>
<p>{summary}</p>Use this when the Component field contract is simple and field-oriented.
For helper-capable fields, the same family extends into helper syntax:
<img src="{heroImage.webp}" alt="{heroImage.description}">
<div data-form-embed="{contactForm}"></div>
<div data-review-embed="{serviceReview}"></div>Do not assume every field supports helper chaining. Check the owning field reference first.
Escaped Expression Output
Use double braces for normal escaped output:
<h1>{{ page.title }}</h1>
<p>{{ summary | default("No summary available.") }}</p>This is the default choice for plain text, labels, URLs, dynamic scalar values, and all cases where HTML should be escaped.
Raw Output
Use triple braces when the source is trusted and should render as HTML:
<div class="prose">
{{{ body }}}
</div>This is appropriate for:
- managed rich text
- controlled HTML fragments
- trusted editorial content
Do not use raw output for arbitrary untrusted strings.
Comments
Facet supports short and long comments:
{{! short comment }}
{{!--
long comment
spanning multiple lines
--}}Comments never render into output.
Expression Syntax
Expressions are the foundation of dynamic template output. They let you access nested properties, call helper-style methods, apply filters, and use conditional inline logic.
Dot Path Notation
Access nested properties with dot notation:
{{ page.title }}
{{ page.parent.title }}
{{ page.parent.parent.name }}
{{ user.language.title }}Arrow notation is also accepted in places where authors naturally type object access:
{{ page->parent->title }}
{{#if page->parent->hasChildren}}
...
{{/if}}
{{#each page->children as="child"}}
...
{{/each}}Use whichever style is clearer inside the current template, but stay consistent within one file.
Method-like Calls
Some objects expose method-like transforms or lookups:
{{ page.image.width(800) }}
{{ page.image.size(600, 400) }}
{{ page.image.size(600, 400).webp.url }}These are common for:
- images
- collections
- page-localized URLs
- page-aware query helpers
Method-like calls are part of the Facet object model, not arbitrary runtime code execution.
Key-Value Parameters
Some helpers accept named parameters:
{{ page.field name="summary" }}
{{ page.children selector="limit=5" }}
{{ pages.count selector="template=blog-post" }}Use named parameters when the helper contract explicitly expects them. This keeps templates readable and avoids overloading positional arguments.
Ternary Expressions
Facet supports ternary output logic:
{{ user.isLoggedin ? "Welcome back!" : "Please sign in." }}
{{ page.summary ? page.summary : "No summary provided." }}Use ternaries for short output decisions. If the branching becomes structural, prefer a block helper such as {{#if}}.
Filter Chains
Use the pipe operator to transform values:
{{ page.title | upper }}
{{ page.body | striptags | truncate(200, "...") }}
{{ published | date("F j, Y") }}
{{ price | currency("$", 2) }}Filters chain from left to right.
Filter Argument Styles
Facet accepts both parenthesis style and colon style arguments:
{{ page.title | truncate(100, "...") }}
{{ page.title | truncate:100:... }}Both forms are supported. Prefer parenthesis style in official templates because it is clearer and easier to review.
Block Helpers
Block helpers provide structure, not just scalar output.
Facet supports these core block types:
| Block | Purpose |
|---|---|
{{#if}} | conditional rendering |
{{#each}} | loop over existing arrays or collections |
{{#pages}} | query a set of pages |
{{#page}} | query one page |
{{#languages}} | iterate available languages |
{{#deferred}} | cache-compatible deferred rendering |
{{#if}}
Use {{#if}} for conditional rendering:
{{#if summary}}
<p>{{ summary }}</p>
{{/if}}With else:
{{#if user.isLoggedin}}
<p>Hello, {{ user.name }}.</p>
{{else}}
<p>Please sign in.</p>
{{/if}}With elseif:
{{#if page.template == "blog-post"}}
<span class="badge">Blog</span>
{{elseif page.template == "case-study"}}
<span class="badge">Case Study</span>
{{else}}
<span class="badge">Page</span>
{{/if}}The else if variant is also accepted:
{{#if page.numChildren > 10}}
<p>Many children</p>
{{else if page.numChildren > 0}}
<p>Some children</p>
{{else}}
<p>No children</p>
{{/if}}Comparison Operators
| Operator | Meaning | Example |
|---|---|---|
== | equal | page.template == "blog-post" |
!= | not equal | page.status != 1 |
> | greater than | page.numChildren > 5 |
< | less than | page.numChildren < 3 |
>= | greater or equal | page.sort >= 0 |
<= | less or equal | page.sort <= 10 |
Logical Operators
| Operator | Meaning | Example |
|---|---|---|
&& | AND | user.isLoggedin && user.isSuperuser |
|| | OR | page.template == "blog-post" || page.template == "news" |
! | NOT | !user.isGuest |
Truthiness Rules
Facet truthiness follows a practical template-oriented model:
| Value | Truthy? |
|---|---|
null | falsy |
false | falsy |
"" | falsy |
0 | falsy |
"0" | falsy |
| empty array or empty collection | falsy |
| everything else | truthy |
If the condition logic starts carrying business rules instead of simple display rules, return to the object model. Do not hide weak content design behind nested if blocks.
{{#each}}
Use {{#each}} when the collection already exists in the current context:
{{#each page.children as="child"}}
<a href="{{ child.url }}">{{ child.title }}</a>
{{/each}}With shorthand this:
{{#each page.images}}
<img src="{{ this.url }}" alt="{{ this.description }}">
{{/each}}With else:
{{#each page.children as="child"}}
<div>{{ child.title }}</div>
{{else}}
<p>No child pages found.</p>
{{/each}}Loop Context Variables
Inside loop blocks, these values are available:
| Variable | Description |
|---|---|
loop.index | zero-based index |
loop.index1 | one-based index |
loop.first | true on first iteration |
loop.last | true on last iteration |
loop.length | total item count |
loop.even | true on even indices |
loop.odd | true on odd indices |
@index | shorthand alias of loop.index |
Example:
{{#each page.children as="child"}}
<div class="{{#if loop.odd}}row-alt{{/if}}">
<span>{{ loop.index1 }}.</span>
{{ child.title }}
</div>
{{/each}}this vs named aliases
Inside {{#each}}, this always points to the current item.
{{#each page.children}}
<a href="{{ this.url }}">{{ this.title }}</a>
{{/each}}If readability matters, prefer an explicit alias:
{{#each page.children as="child"}}
<a href="{{ child.url }}">{{ child.title }}</a>
{{/each}}{{#pages}}
Use {{#pages}} when the template needs to query content rather than render a collection already present in scope.
{{#pages selector="template=blog-post, sort=-created, limit=5" as="post"}}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<time>{{ post.created | date("F j, Y") }}</time>
<p>{{ post.summary | truncate(200) }}</p>
</article>
{{else}}
<p>No posts found.</p>
{{/pages}}Typical parameters:
| Parameter | Description |
|---|---|
selector | query selector string |
as | loop variable name |
Use {{#pages}} for:
- archives
- listing pages
- recent content widgets
- navigation collections
- query-driven content blocks
Use {{#each}} when the collection already exists. Use {{#pages}} when the template must fetch it.
{{#page}}
Use {{#page}} when only one page should be queried:
{{#page path="/about/"}}
<a href="{{ page.url }}">{{ page.title }}</a>
{{/page}}Supported lookup patterns include:
{{#page id="1024"}}...{{/page}}
{{#page path="/about/"}}...{{/page}}
{{#page name="contact"}}...{{/page}}
{{#page selector="template=homepage"}}...{{/page}}Inside a {{#page}} block, page refers to the queried page for the duration of that block.
{{#languages}}
Use {{#languages}} to build language-aware UI such as language switchers:
<nav class="lang-switch">
{{#languages as="lang"}}
<a href="{{ lang.url }}" {{#if lang.isCurrent}}class="active"{{/if}}>
{{ lang.title }}
</a>
{{/languages}}
</nav>With an empty fallback:
{{#languages as="lang"}}
<a href="{{ lang.url }}">{{ lang.title }}</a>
{{else}}
{{! single-language site }}
{{/languages}}Useful language properties include:
lang.idlang.namelang.titlelang.isDefaultlang.isCurrentlang.urllang.httpUrllang.locale
{{#deferred}}
Use {{#deferred}} when the page is cache-heavy but part of the content depends on per-user or rapidly changing runtime data.
{{#deferred}}
<span>Welcome, {{ user.name }}!</span>
{{/deferred}}With custom skeleton sizing:
{{#deferred skeleton-width="12rem" skeleton-height="2em"}}
<div class="user-panel">
<span>{{ user.displayName }}</span>
</div>
{{/deferred}}{{#deferred skeleton-width="9rem" skeleton-height="1em"}}
{{#if user.isLoggedin}}
<span>Welcome, {{user.name}}!</span>
{{else}}
<a href="/login">Sign In</a>
{{/if}}
{{/deferred}}Use it for:
- user-aware greetings
- account panels
- cache-safe personalization
- other content that should not be fixed into a full-page cached HTML response
deferred should be used intentionally. Do not wrap everything dynamic by default.
Context Objects
Facet exposes a set of root-level context objects in every supported render surface.
Common objects include:
pagepagesuserlanguages/langsitenowattrloopthis
Inside Component templates, Component field values are also exposed at root level:
<h1>{{ title }}</h1>
{{#if showGallery}}
<div class="gallery">
{{#each images as="img"}}
<img src="{{ img.url }}">
{{/each}}
</div>
{{/if}}The deep object reference is documented in:
Image Output Boundary
Image syntax depends on where the image object comes from.
Component image field helper syntax
<img src="{heroImage.width(1200).webp}" alt="{heroImage.description}">Page or query image object syntax
{{#each page.images as="img"}}
<img src="{{ img.width(800).webp.url }}" alt="{{ img.description }}">
{{/each}}Component images field loop syntax
{{#each gallery}}
<img src="{{this.width(1200).webp}}" alt="{{this.description}}">
{{/each}}Do not mix these forms casually. The owning field or runtime object decides which syntax is correct.
Map Helper Output
Map-style fields can expose helper-oriented output:
{officeLocation}
{officeLocation.width(600)}
{officeLocation.height(400)}
{officeLocation.size(800,500)}Use this only when the field contract explicitly documents it.
Variable Embedding
Variables are embedded with double-bracket syntax:
[[site-announcement]]
[[cta-badge label="New" tone="accent"]]Inside the Variable template, passed values are available through attr.
Variables are for reusable fragments, not for replacing proper object design. If the fragment grows into a section contract, return to Component design.
Managed Embed Markers
Forms and Reviews are typically rendered through managed embed markers.
Fixed model embed:
<div data-form-embed="enterprise-demo-request"></div>
<div data-review-embed="customer-success-reviews"></div>Field-backed embed inside a reusable Component:
<div data-form-embed="{contactForm}"></div>
<div data-review-embed="{serviceReview}"></div>Decision rule:
- fixed embed when the template owns one stable managed workflow
- field-backed embed when authors should choose the managed model at authoring time
Usage Guidance
- keep template logic readable
- prefer strong field contracts over clever template tricks
- use
{{#if}}for structure, not for hiding weak content design - use
{{#each}}when the collection already exists - use
{{#pages}}when the template must query the collection - use Variables for small reusable fragments
- return to Component or List design when template logic grows too complex
Common Mistakes
Avoid these patterns:
- using raw output where escaped output is safer
- mixing Component field shorthand and generic Facet expressions without understanding the difference
- solving a content-model problem only with conditionals
- building one giant nested template instead of clarifying object boundaries
- repeating the same fragment across templates instead of extracting a Variable
- using field helper syntax on fields that do not support it
Quick Decision Guide
need to show one value -> {field} or {{ expr }}
need trusted HTML output -> {{{ expr }}}
need to show content conditionally -> {{#if}}
need to loop existing items -> {{#each}}
need to query content -> {{#pages}} or {{#page}}
need language-aware navigation -> {{#languages}}
need cache-safe dynamic personalization -> {{#deferred}}
need one reusable fragment -> [[variable-name]]
need a field-specific helper output -> {field.helper}
need a full reusable section contract -> return to Component design