DATA BINDING: the template HTML is rendered with Handlebars against the document data, so bind every value to a field instead of hardcoding it:
- Text:
{{ document.hero.title }}(or{{ hero.title }}) — double braces, HTML-escaped. Rich-text values (the HTML fragment string an editor:"html" text field stores, block-editor values) render their markup through{{ }}as well, so double braces are always the safe default for text. - BRACES ↔ FIELD KIND:
{{{ … }}}(triple, raw) ONLY for values that ARE markup — an editor:"html" text field (its value is the HTML fragment as a plain string — a single line bare,"Firma v oblasti <em>recyklace</em>", so it fits a heading or any inline slot;"<p>…</p><p>…</p>"only for several blocks; never a{type:"html"}wrapper), a block-editor field, an icon field (inline<svg>), and the.htmlof a rendered reference / element placement. Everything else — plain text, numbers, enums, dates, URLs — binds with{{ … }}. A text string that carries an HTML tag renders as markup under BOTH brace forms, so a plain-TEXT field must never hold tags (the renderer would show them as formatting, and click-to-edit opens the field as rich text): when a value needs bold/links/line breaks, declare the fieldeditor:"html"and store the fragment. Never bind a plain-text field raw: it is a raw-HTML hole the click-to-edit surface cannot address (the write tools warn on brace/kind mismatches). - Image field → a URL string:
<img src="{{ document.hero.cover }}" alt="">. - Gallery (
kind:"gallery") → an ARRAY of image values, each addressed exactly like a single image field:{{#each document.gallery}}<img src="{{ this.thumb }}" srcset="{{ this.srcset }}" alt="{{ this.alt }}">{{#if this.caption}}<figcaption>{{ this.caption }}</figcaption>{{/if}}{{/each}}.{{this}}alone is the picture's URL. - File field (
kind:"file"— a PDF, a document, an archive) → a URL string at the field key, like an image:<a href="{{ document.manual }}" download>{{ document.manual.name }}</a>. The file INFO is addressable off the value:{{ document.manual.name }}(original filename),{{ document.manual.ext }}("pdf"),{{ document.manual.type }}(MIME),{{ document.manual.size }}(bytes) and{{ document.manual.size_text }}(localized, "15.3 MB" / "15,3 MB") — a download row is<a href="{{ this.file }}"><span>{{ this.file.name }}</span><small>{{ this.file.ext }} · {{ this.file.size_text }}</small></a>. Those names never resolve to the URL (an old node without them renders empty); per-usage metadata ({{ document.manual.title }}) binds exactly as on an image. NEVER write the name, type or size into neighbouring text fields — they come off the file value. A download BUTTON the visitor clicks is one object{label, file}(the GROUPING RULE of the field-kinds rule):<a href="{{ download.file }}" download>{{ download.label }} <small>{{ download.file.size_text }}</small></a>. - ALT TEXT AND CAPTIONS COME OFF THE IMAGE VALUE, not off a neighbouring field:
{{ document.cover.alt }},{{ document.cover.caption }}({{ document.cover.metadata }}is the whole map) — the per-usage metadata the editor typed for THAT picture, declared by the field'smetadata:[…]schema. Those names never resolve to the image URL, so an image with no alt typed rendersalt="". NEVER writealt="{{ document.cover_alt }}"against a separate text field, and never bind a plain{{ image.src }}— an image field IS the URL at the field key (see IMPORTANT below). - Array of objects:
{{#each document.shoes.products}}<div>{{ this.title }}</div>{{/each}}. - LINK FIELDS (
kind:"link") — the way a page links to ANOTHER store document (a button, a CTA, a menu item, a footer link): the field's value is either a verbatim string (#anchor,tel:,https://…) or an envelope{type:"link", collection, document, hash?}that the renderer resolves to the target's public URL in the page's locale (slug + scope + collection route; the homepage →/; a deleted target → NOTHING). Bind it as<a href="{{ cta.href }}">— DOUBLE braces, it is a plain value — and ALWAYS guard:{{#if cta.href}}<a href="{{cta.href}}">{{cta.label}}</a>{{/if}}.{{ cta.href.title }}is the target's title in the page's locale (a button labelled by its target). The link and what stands for it are ONE object field in the schema (cta: {label, href}, a linked picture{image, href}, an icon link{icon, label, href}— the GROUPING RULE of the field-kinds rule), so bind both off that object ({{cta.label}}+{{cta.href}}), never a label field and a link field that sit side by side at the top level. Never hand-build the path from the target's slug when a link field exists — the field already did it, locale and routes included (see the field-kinds rule). The{{route}}idiom below stays for documents you reach THROUGH a reference (cards of a listing). - LINKING to a referenced document — a
referencefield resolves at render to the target doc(s), so acollection/listreference (e.g.products_preview) is an array of full docs and adocumentreference is one doc. Link fields INSIDE those docs (this.href) come back resolved to URL strings too. Asiblingvalue (the dynamic prev/next form of a document reference — see the reference kind in the field-kinds rule) is ONE doc or NOTHING (the first/last document of the sequence): ALWAYS guard it —{{#if next}}<a href="…">{{next.title}}</a>{{/if}}— then link it exactly like any resolved document reference (withrender:trueit gains the same.htmlkey as an embedded component). When you render such an item as a card/link, point it at the item's OWN page: prefer its slug, fall back to the reference route. Never usehref="#"or an invented path for these.{{#each document.products_preview}}<a href="{{#if this.slug}}/{{this.slug}}{{else}}/ref/preview/{{this.id}}{{/if}}">{{ this.title }}</a>{{/each}}— use the item'sthis.slug(orthis.seo.slug) when present, else/ref/{collection}/{{this.id}}where {collection} is the reference field's TARGET collection (herepreview). Every resolved item carriesthis.id, so the/ref/…fallback always works. PREFER the{{route}}helper for the slug branch:{{route "preview" this.seo.slug}}renders/{{slug}}, or/{segment}/{{slug}}when that collection has ROUTING enabled (setup/datacollections[].route.enabled— its documents then live under the collection's own URL segment, its locale slug or key). Same fallback shape:<a href="{{#if this.seo.slug}}{{route "preview" this.seo.slug}}{{else}}/ref/preview/{{this.id}}{{/if}}">. A CATEGORIZED document (itsseo.categorynames one of the collection'sroute.categories) lives at/{segment}/{category}/{slug}— pass the category as the THIRD argument:{{route "recipes" this.seo.slug this.seo.category}}(an empty or unknown category gives the plain/{segment}/{slug}, so pass it whenever the collection may have categories). JS-built cards (load-more) read the same map fromwindow.DROP_ROUTES({recipes: "recepty"}, routed collections only) and the category paths fromwindow.DROP_ROUTE_CATEGORIES({recipes: {soups: "polevky"}}, collection → category key → its path in the page language; absent when nothing is categorized), so a card's path is'/' + DROP_ROUTES[c] + (cat && DROP_ROUTE_CATEGORIES?.[c]?.[cat] ? '/' + DROP_ROUTE_CATEGORIES[c][cat] : '') + '/' + slug, and build a page link aswindow.DROP_BASE + (window.DROP_LOCALE_BASE || '') + path—DROP_LOCALE_BASEis the page language's URL prefix (/en) on a store that keeps the language in the URL, absent otherwise. The general element kit's runtime exposes this asDropElements.pageHref(path). - EMBEDDING a referenced document as a COMPONENT — a
documentreference whose stored value carriesrender: trueis rendered by the server through the TARGET's own schema template, and the resolved value gains anhtmlkey holding that markup (its CSS/JS are hoisted into the page for you). Drop it in with{{{ document.contact_form.html }}}and do NOT re-author the component's markup in this page's template — the whole point is that the component's markup lives in ONE template (its own) and appears on many pages. The target's other fields stay addressable alongside it ({{ document.contact_form.headline }}), so you can still wrap it in your own section/heading.renderis set on the VALUE (the reference picker's "Render with its own template", or your document-value write) — a reference without it resolves to data only and has no.html. - Icon field → inline markup:
{{{ document.hero.icon }}}(triple braces). - Datetime field → renders ALREADY LOCALIZED in the page's locale (
{{ document.published }}→11. 6. 2026on a cs_CZ page; which parts show — date, time, both — follows the field's date/time flags). The stored machine form stays at{{ document.published.iso }}: use<time datetime="{{ document.published.iso }}">{{ document.published }}</time>for markup-visible dates, and.isofor comparisons/sorting. This works the same for datetime fields of resolved reference items ({{ this.published }}inside{{#each}}). NEVER hand-format a date, add a second display-string field for one, or rewrite dates client-side in JS. NO HARDCODED TEXT: a page/component template must contain NO literal human-readable text. EVERY visitor-facing string — headings, eyebrows/kickers, button and badge labels, empty-state copy, and thealt/placeholder/aria-label/titleattributes — is either a document-field binding or a{{t "group.key"}}store translation (see HELPERS below). The template is shared by every locale (and, for a shared schema, by every document), so a literal string can never localize and leaks the same words onto every page. When the copy has no home yet, CREATE one instead of inlining the literal — and pick the home by whether the words VARY: copy that differs from document to document → a field of the schema (set its value in each document); a string that is the same everywhere it appears → a{{t}}translations key. On a SHARED schema (one template rendering many documents — articles, campuses, services) every label of the template's own chrome — a sub-section heading (Máte dotaz?,Galerie,Lokalita), a button label (Kontaktujte nás), load-more / empty-state / tab / prev-next / breadcrumb labels — is the SAME on every document, so it is a translations key (one group per context:campus,article,ui), NEVER a field every document must fill and every editor must retype; a per-document OVERRIDE the user wants is an optional field with the key as its fallback ({{#if ask_title}}{{ask_title}}{{else}}{{t "campus.ask_title"}}{{/if}}). On a PER-DOCUMENT schema (one page) that page's own copy is a field. Never inline a literal "for now". Not text for this purpose: punctuation/decorative glyphs (·, —, ×), numbers coming from data, and machine values (CSS classes,data-*keys, URLs). (Transactional invoice/e-mail templates are the one exception — they are per-locale documents whose inline text IS the localized copy.) IMPORTANT: file/image fields are ALREADY resolved to a URL string AT THE FIELD KEY in the render context — write<img src="{{ document.hero.cover }}">, NEVER{{ document.hero.cover.path }}(there is no.path/.url). SIZE VARIANTS of an image field ARE addressable:{{ document.cover.thumb }}is thethumbrendition's URL (an unknown/missing variant name safely falls back to the original — old uploads never break),{{ document.cover.srcset }}is a ready width-descriptor srcset (<img src="{{cover}}" srcset="{{cover.srcset}}" sizes="100vw">for responsive heroes; it lists the variants AND the original when the value carrieswidth— uploads since 2026-10-27 do; empty otherwise),{{ document.cover.width }}/{{ document.cover.height }}are the original's pixel size when known (write them aswidth/heightattributes to avoid layout shift), and{{ document.cover.original }}is always the original. Variants exist only when the image was uploaded FOR its field: every generateImage / importImageFromUrl call whose result lands in a field passesfieldKey+schemaId(and the field declaressizes) — a result with awarninghas no variants, so redo it. Use the smallest sufficient variant in listing cards (<img src="{{ this.cover.thumb }}">) instead of the full-size original. Text fields are plain strings unless declared editor:"html" (then their value is the html envelope — see BRACES ↔ VALUE SHAPE). Use the ACTUAL field keys from the current schema below; do NOT hardcode copy that belongs in a field. HELPERS — ONLY the following are registered. Using ANY other helper (e.g.inc,concat,formatDate,json,capitalize,math,times) throws at render time and the WHOLE page falls back to raw, un-interpolated source. If a helper you want is not listed, compute it in the data via setDocumentValues instead — never invent one (dates never need this: datetime fields localize themselves, see above). - Built-ins:
{{#if x}}…{{else}}…{{/if}},{{#unless x}},{{#each list}}…{{/each}}(inside each:{{this}},{{@index}},{{@first}},{{@last}},{{@key}}),{{#with obj}},{{lookup obj key}}. - Comparisons (return a boolean — use as subexpressions inside #if):
(eq a b),(ne a b),(gt a b),(lt a b),(gte a b),(lte a b)— e.g.{{#if (eq @index 0)}}active{{/if}}. - Boolean logic:
(and a b …),(or a b …),(not a)— e.g.{{#if (and title subtitle)}}. - Arithmetic:
(add a b),(subtract a b),(multiply a b)— e.g.alt="Image {{add @index 1}}". - Block equality shortcut:
{{#ifEq a b}}…{{else}}…{{/ifEq}}. - Collection route:
{{route "{collection}" slug}}/{{route "{collection}" slug category}}— the public path of a document reached through a reference (see LINKING above); the optional third argument is the document'sseo.category(its category key) and adds the/{category}segment when that category is declared on the routed collection. The collection is a LITERAL string; a path expression that resolves to nothing renders empty. - Store translations:
{{t "group.key"}}— resolves a shared per-store string from the localizations store (documents under thetranslationsdata collection:{{t "cta.buy"}}reads keybuyof group doccta, in the page's locale). Use it for SHORT REUSABLE UI strings that repeat across templates — button labels (Buy,Add to cart,Send,Read more), badges (Sale,New), empty-state and form-feedback copy — instead of hardcoding them per template. Optional{name}placeholders interpolate from hash args:{{t "cart.count" count=items.length}}. A missing key renders the key itself (cta.buy) — so only reference keys that exist (readable via thetranslationscollection), or create them first when you can write collections. Several positional arguments join with a dot —{{t "tag" this}}→tag.sale— the form for a key held in DATA (a product'stags/categories:{{#each tags}}<span class="tag">{{t "tag" this}}</span>{{/each}},{{t "category" category}}), since there is no concat helper. Page-specific copy (headlines, body text) stays in document fields, NOT in translations.