Every template tag and how to use it
Every template tag SeedWebs supports, including embedded videos and social posts: attributes, defaults, examples, and where each tag works.
Last updated 2026-10-26
A template tag is text in double curly braces — {{site_name}} — that SeedWebs replaces with real data as it builds the page. Tags work in a theme (Header, Footer, Elements, Templates) and inside the content of a Page.
This document lists every tag used to write content and page templates. A tag not listed here is unknown to SeedWebs, and is either printed literally on the page or stripped out.
The one exception is the elements on the Nav tab, which have their own internal set of tags ({{items}}, {{columns}}, {{children}}, {{item.class}}, {{item.href}}, {{column.*}} and others) that are only filled in while the menu is being built. Those do not work anywhere else and are not documented here. The safe approach is to edit the classes in the code that shipped with the theme, rather than adding tags of your own.
Where you edit tags
Go to Settings → Themes (/admin/settings/themes) and open a theme. The tabs are:
| Tab | For |
|---|---|
| General | Header HTML, Footer HTML, Global CSS, Global JS, Body class |
| Nav | The menu’s elements (nav-item, nav-submenu, nav-mobile-item and so on) |
| Elements | Reusable pieces, such as post-card |
| Templates | Page templates, such as post-list, post-detail, search-results |
| Emails | Email templates (this tab appears only on sites with Newsletters enabled) |
Editing a theme is limited to super_admin and admin (see Users and permissions). The theme structure and the template lookup order are covered in Themes and templates.
In the theme editor, the right sidebar has Options → Theme reference, which opens a condensed tag summary to keep beside you while typing.
The rules for writing a tag
- The form is
{{tag_name}}or{{tag_name attr="value"}}. Write the tag name in lowercase exactly as documented here. - Attribute values always go in double quotes
", with two exceptions:limitandoffsetin{{#each}}must be bare numbers with no quotes (limit="12"is not read and the default is used instead), andcharsandwordson{{item.excerpt}}work either way. - Inside
{{#each}}, attribute values must contain no spaces. Use the slug —category="how-to", not a name with a space in it. - An unknown tag written as a bare name inside Page content (
{{foo}}) is stripped; the same tag in a theme is printed literally on the page. - After saving a theme, the public site is cached. Expect about a minute. To hurry it, press Clear cache / Rebuild site under Settings → General (
/admin/settings/general).
Site-level tags
For use in a theme’s Header and Footer.
| Tag | Result |
|---|---|
{{site_name}} | The site name from Settings → General (per language) |
{{tag_line}} | The site’s tag line |
{{year}} | The current year, such as 2026 |
{{locale}} | That page’s language code — en, th |
{{locale_home}} | A link to that language’s home page (/ or /th/) |
{{site_logo}} | An <img> tag for the logo. Empty when no logo is set |
{{site_logo class="h-10" alt="My site"}} | The logo with your own class and alt (without alt, the site name is used) |
{{site_logo_url}} | Just the URL of the logo file |
{{language_switcher}} | A dropdown language switcher (the current language is the button; the others open on hover) |
{{language_switcher class="..." activeClass="..."}} | A plain row of language links. activeClass defaults to active |
{{language_switcher}} renders only when the site has more than one language enabled. With one language it is empty.
{{site_name}}, {{tag_line}} and {{year}} also work in Page content.
Site-level conditionals
In the Header and Footer you can write conditions on these values: site_logo, site_name, tag_line, favicon and locale_<code>.
{{#if site_logo}}
{{site_logo class="h-10"}}
{{else}}
<span class="font-bold">{{site_name}}</span>
{{/if}}
{{#if locale_th}}ภาษาไทย{{else}}English{{/if}}
The menu, {{nav}}
Pulls links from the menu set up under Settings → Menus (/admin/settings/menus). With no menu built yet, SeedWebs uses your published top-level Pages plus a link per enabled content type.
| Attribute | Result |
|---|---|
class | Classes for the wrapping <ul>, merged with the default flex items-center gap-1 |
itemClass | Classes for each <a> |
activeClass | Classes merged over itemClass when a link matches the current page |
location="mobile" | Renders a hamburger drawer instead, with collapsible submenus |
menu="footer" | The menu with that slug instead of the header menu (the Footer tab in Settings → Menus prints the tag with its slug) — the header menu while that menu is missing, Draft or has no items |
start, end | Only the top-level items from number start to number end, counting from 1; submenus go with their item |
{{nav class="flex items-center gap-2" itemClass="px-3 py-2 text-sm text-gray-600 hover:text-gray-900" activeClass="font-semibold text-gray-900"}}
{{nav location="mobile" itemClass="block px-4 py-3 text-base border-b border-gray-100"}}
location takes only mobile. Any other value — footer included — is not a menu selector: it shows the header menu, exactly as it always has, so a theme that already writes {{nav location="footer"}} keeps showing what it showed. To place another menu, name it: {{nav menu="footer"}}. {{nav mobile="true" menu="footer"}} is that menu as a drawer.
start and end split one menu, for a logo in the middle of the header:
{{nav end="3"}} {{site_logo class="h-12"}} {{nav start="4"}}
Leaving out start means from the first item, leaving out end means to the last; a range past the end shows an empty list. Both work with menu="…" too. More in Menus and navigation, including how to show an item in one language only.
Class merging uses tailwind-merge, so you only specify what differs; conflicting utilities are replaced for you.
Content tags, {{item.*}}
For use in a theme’s Elements and Templates — both in cards inside {{#each}} and in detail-page templates.
Every column of the row is available directly: {{item.title}}, {{item.slug}}, {{item.body}}, {{item.id}}, {{item.locale}}, {{item.view_count}}, {{item.seo_description}}.
{{item.body}} and any field name ending _html are inserted as HTML. Every other field is escaped for safety.
Fields SeedWebs derives for you
| Tag | Result |
|---|---|
{{item.url}} | The item’s public link. Available inside {{#each}} for posts, events, products, tours, books and staffs |
{{item.excerpt}} | A summary from the body, cut at about 200 characters. Content containing <!--more--> is cut there instead |
{{item.excerpt chars=120}} | Length in characters — better for Thai |
{{item.excerpt words=30}} | Length in words — better for English |
{{item.date}} | Created date, in the language’s date format |
{{item.date_short}} | The created date with an abbreviated month |
{{item.time}} | Created time, in the language’s time format |
{{item.date format="D MMM" field="start_at"}} | Own pattern; field: another date. Or locale (th, en), style (short), order (mdy) |
{{item.updated_at_formatted}} | Every _at column has _formatted, _formatted_short and _time (empty when no time) |
{{item.read_time}} | Estimated reading time, as a number of minutes |
{{item.read_time_label}} | A ready-made string, such as 5 min read |
{{item.views_short}} | View count, abbreviated — 1.2K |
{{item.views_formatted}} | View count in full with thousands separators — 1,234 |
{{item.price_formatted}} | A formatted price, e.g. ฿1,234, in the currency set under Store settings ▸ General (with sale_price_formatted and amount_formatted companions) |
{{item.in_stock}} | Products only. Empty when the product is marked out of stock, so {{#if item.in_stock}} shows a Buy button and {{#unless item.in_stock}} shows a Sold Out badge |
{{item.stock_label}} | Products only. Empty for a normal product, otherwise out_of_stock or on_backorder — handy as a CSS class |
{{item.display_name}} | A display name, built from first_name and last_name when it is empty |
{{item.initial}} | The first letter of the display name, for a letter avatar |
Tours add {{item.adult_price_formatted}} and {{item.child_price_formatted}} (in that tour’s currency), plus {{item.includes_html}} and {{item.excludes_html}}, which are already converted into <ul> lists.
Images and their automatic variants
Any field holding an image URL (.webp, .jpg, .jpeg, .png, .avif) gets two extra tags automatically.
| Tag | Result |
|---|---|
{{item.featured_image}} | The main image URL |
{{item.featured_image_mobile}} | The image’s -sm file, or the main URL again when it has none |
{{item.featured_image_srcset}} | URL 1200w, URL-sm 768w for the srcset attribute — just URL 1200w when the image has no -sm file |
<img src="{{item.featured_image}}" srcset="{{item.featured_image_srcset}}" alt="{{item.title}}" loading="lazy" />
Custom fields
Fields you defined yourself are called with {{item.field.<key>}}, and get the same image variants.
{{#if item.field.brochure_image}}
<img src="{{item.field.brochure_image}}" srcset="{{item.field.brochure_image_srcset}}" alt="" />
{{/if}}
{{item.field.xxx}} is a separate namespace from the ordinary fields. With a custom field called url, its value is at {{item.field.url}}, while {{item.url}} is still the item’s link.
The whole declared list at once — {{#each item.fields}}
On a staff page, every field declared on the person’s staff category — and on
its parent categories — is also available as one list, already labelled and in
the order the fields were declared. Useful when a directory has ten or twenty
fields and writing {{item.field.x}} for each one, per category, is not a
template.
{{#if item.fields}}
<dl>
{{#each item.fields}}
<div><dt>{{item.label}}</dt><dd>{{item.value}}</dd></div>
{{/each}}
</dl>
{{/if}}
Inside the loop, {{item.label}} is the label from the definition,
{{item.value}} the person’s value, {{item.key}} the machine name and
{{item.group}} either main or sidebar. {{item.fields_main_label}} and
{{item.fields_sidebar_label}} are the two group headings.
Fields with no value are left out of the list, so a heading never appears above
an empty value. Fields switched off for the public site (Public unchecked) never
appear at all. Repeaters are not in the list — write those out with
{{item.field.<key>}} and {{#each item.field.<key>}}.
Inheritance is what makes this work on a category tree: declare Department once
on Lecturer and every person filed under Bangkok or Graduate gets it. A
child category may redeclare a field to relabel it; it keeps the parent’s
position in the order.
The same loop works inside a card in {{#each staffs}}, so a directory listing
can show a person’s department under their name.
A field may also be declared against one of the person’s built-in columns —
position, email, phone, display_name, first_name, last_name — and it
appears in the list with its label like any other. Notes stays private, and the
biography belongs in {{item.body}}.
The internal note is not available on the public site at all — not through a
field definition, and not as {{item.notes}} in a staff template or card. It is
the one staff column kept for the admin side only.
Staff pages also carry their category chain, root first, including the person’s own category — enough for a breadcrumb the tree implies:
<nav>
<a href="{{locale_prefix}}/staffs">Team</a>
{{#each item.category_ancestors}}
/ <a href="{{locale_prefix}}/staff-categories/{{item.slug}}">{{item.name}}</a>
{{/each}}
</nav>
{{item.category.slug}} and {{item.category.name}} are the person’s own
category on its own.
The default theme’s Staff Detail template already does this, so a site with a
staff category tree gets the crumbs without any editing. The catch-all
Default category is left out of the chain — it means “not filed anywhere”, so
a site that never made staff categories keeps the plain Home / Team / <name>.
A person can be filed under several categories (Staff ▸ edit ▸ Also filed under), so they appear on every one of those category pages. The breadcrumb follows their primary category only — the one in the Category dropdown — because a breadcrumb has to name one path.
Author, categories and tags (posts)
In the post-detail template and in cards inside {{#each posts}}, the related data is available.
{{#if item.author}}
<a href="{{locale_prefix}}/authors/{{item.author.slug}}">
<img src="{{item.author.avatar_url}}" alt="" />
{{item.author.display_name}}
</a>
{{/if}}
{{#each item.categories}}<span>{{item.name}}</span>{{/each}}
{{#each item.tags}}<span>#{{item.name}}</span>{{/each}}
{{#each item.coauthors}} works in a post’s detail template, and inside it {{item.display_name}}, {{item.slug}} and {{item.avatar_url}} behave as they do for the primary author.
On an experimental site, {{#each item.tags}} lists a post’s tags in the order they were set on that post; elsewhere, in the order the tags were created.
Nested category addresses. With Settings ▸ General ▸ URLs ▸ Post categories set to example.com/category/parent/child, link a post’s categories with {{item.url}} rather than building /category/{{item.slug}} by hand — {{item.url}} is the category’s full address in the visitor’s language, and a hand-built one costs every click a redirect. The post also carries the chain of its first category, root first, for a breadcrumb:
{{#each item.categories}}<a href="{{item.url}}">{{item.name}}</a>{{/each}}
<nav>
{{#each item.category_ancestors}}
/ <a href="{{item.url}}">{{item.name}}</a>
{{/each}}
</nav>
With the setting on its default (example.com/category/child), neither a post’s item.category_ancestors nor its categories’ {{item.url}} is filled in.
Categories of the other types. A category of a product, program, book, tour, staff member or portfolio always has {{item.url}} — its category page, the full chain when that type’s categories are nested (Settings ▸ General ▸ URLs) — and so do the item.category_ancestors crumbs on staff and program pages, and {{item.category.url}} on a staff page. Link with it rather than /product-categories/{{item.slug}}, which costs a redirect once nested.
Categories and tags per language (experimental sites). With Settings ▸ General ▸ Advanced URLs ▸ Categories and tags set to per language, a post’s tags get an {{item.url}} too — the tag’s address in the visitor’s language (/en/tag/video-streaming-2 for the tag whose Thai address is /tag/ding-yuxi), or its own language’s address when it does not exist in the visitor’s. Link tags with it rather than /tag/{{item.slug}}, which is only the Thai address:
{{#each item.tags}}<a href="{{item.url}}">#{{item.name}}</a>{{/each}}
Conditionals, {{#if}} and {{#unless}}
{{#if item.featured_image}}<img src="{{item.featured_image}}" alt="" />{{/if}}
{{#if item.excerpt}}<p>{{item.excerpt}}</p>{{else}}<p>No description yet</p>{{/if}}
{{#unless item.avatar_url}}<div class="placeholder">{{item.initial}}</div>{{/unless}}
The falsy values are empty, missing, false and 0. {{#unless}} does not support {{else}}.
Do not nest {{#if}} inside another {{#if}} — the renderer treats the first {{/if}} as the closing tag. For nested conditions, split the inner part into its own element and call it with {{> ...}}.
Saving checks your conditions. A {{#if}} or {{#unless}} that the template you are saving cannot evaluate is refused with a message naming the line and the tag — a misspelt name ({{#if categroy_body}}, with a suggestion), a name that only works elsewhere ({{#if site_logo}} outside the header and footer), {{#unless}} in the header, footer or menu elements, any condition in an email template, {{else if}}, or a block that is never closed. Without the check, such a block prints on the page as text with its content always showing. A problem that was already in the saved version never stops you saving — it is listed as a warning instead, so you can fix it when convenient.
Looping over content, {{#each}}
{{#each posts limit=12}}
{{> post-card}}
{{/each}}
The types that can be queried are posts, events, products, tours, books and staffs. Any other name yields nothing (experimental sites also have product_brands and pages, below).
Of those six, only posts works on every site straight away. products needs Pro or Business (see Plans and billing), and events, books, staffs and tours need the team to turn on the experimental flag for your site (see What SeedWebs does not have). Where the type is not switched on under Settings → General → Features, the loop shows nothing, wherever you write it, and the type’s listing and detail pages answer 404. A products loop follows the Store switch.
| Attribute | Default | Works with |
|---|---|---|
limit=12 | 6 | Every type |
offset=3 | 0 | Every type (skip the first few, for a separate featured item) |
order="views" | Per type | Accepts latest, oldest, title, title desc, views, price, price desc. price works on products only — on a type without a price the loop keeps its normal order |
category="news" | No filter | posts (including sub-categories), products, tours, books |
tag="featured" | No filter | posts |
author="somchai" | No filter | posts |
since="3m" | No filter | posts, events, products, tours |
category="tech,business" | — | posts, experimental sites: posts in any of the categories (sub-categories included) — also in an article sidebar on a product, brand or tag listing page. Elsewhere the comma is part of one category name |
category="current" | — | posts in a post template, experimental sites: posts in any of the shown post’s own categories (sub-categories included) — a “related posts” row; add exclude="current" to leave the post itself out. For a post filed in no category, and in a Page’s content or another type’s template, there is no category filter. Archive templates (post list, category, tag, author and date pages), and other sites, read current as a category name. products in a product template, experimental sites: the shown product’s main category (sub-categories included) — a “related products” row; with exclude="current" the product itself is left out. A product in no category gets no category filter |
exclude_category="pr" | No filter | posts, experimental sites: leaves out posts filed in those categories (comma-separated; the categories themselves, not their sub-categories) — also in an article sidebar on a product, brand or tag listing page |
exclude="current" | Off | posts in a post template, or products in a product template, experimental sites: leaves out the post or product being shown — a “related” row without the item itself |
page_param="query-7f09590f-page" | Off | posts in a Page’s HTML content, experimental sites: the loop shows the page that address parameter asks for (?query-7f09590f-page=2), and {{pagination}} in the same content prints its page links — see Pagination below |
paginate="true" | Off | Every type — marks this loop as the one {{pagination}} counts pages for |
since takes a number plus a unit, where the unit is d (days), m (months) or y (years) — 7d, 1m, 3m, 6m, 1y. Weeks are not supported.
Default ordering with no order: posts, products and tours newest first; events by the most recent start date; books by sort_order then newest; staffs by sort_order.
A loop returns only items whose status is published (staffs use active), and only in the language of the page being rendered.
Patterns people use most:
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{{#each posts limit=6 category="how-to" order="views" since="3m"}}
{{> post-card}}
{{/each}}
</div>
{{#each posts limit=1}}<article class="hero">{{item.title}}</article>{{/each}}
{{#each posts limit=6 offset=1}}{{> post-card}}{{/each}}
Brands, {{#each product_brands}} (experimental sites)
{{#each product_brands order="count" hide_empty="true"}}
<a href="{{item.url}}">{{#if item.image}}<img src="{{item.image}}" alt="{{item.name}}" />{{/if}}{{item.name}} ({{item.count}})</a>
{{else}}<p>No brands yet</p>{{/each}}
Lists the store’s brands, except Default: {{item.name}} in the page’s language, {{item.url}} the brand’s page, {{item.image}} and {{item.image_mobile}} the image set on the brand, {{item.count}} the number of products its page shows. Sorted by name; order="count" puts the brands with the most products first, hide_empty="true" leaves out brands with none, and limit (default and maximum 500) shortens the list. The {{else}} part shows when there is nothing to list — also with the Store off. A product card or product page has its brand as {{item.brand.name}}, {{item.brand.url}} and {{item.brand.image}}. On other sites the loop shows nothing.
Sub-pages, {{#each pages}} (experimental sites)
{{#each pages parent="vocab"}} lists the published sub-pages of /vocab (about/team for a deeper one) in the page’s language, with a post card’s tags ({{item.title}}, {{item.url}}, {{item.excerpt}}, {{item.featured_image}}, {{item.date}}, {{item.field.…}}), by page order then title. order also takes title, title desc, latest, oldest; limit 100, max 200. {{else}} and other sites work as for brands.
Loops that only work on certain pages
| Tag | Template | Notes |
|---|---|---|
{{#each search}} | search-results | Search results. Inside it: {{item.url}}, {{item.type}}, {{item.type_label}}, {{item.title}}, {{item.excerpt}}, {{item.featured_image}}. {{item.type}} is the fixed key (posts, products), good for {{#if}} checks. {{item.type_label}} is the name you gave that type under Settings → General → Features, in the page’s language; without one it prints the same key as {{item.type}}. On an experimental site a product result also has {{item.price}}, {{item.sale_price}} and {{item.field.…}}, as on a product card, and /api/public/search?q=… returns the same results as JSON for a search-as-you-type box |
{{#each authors}} | author-list | Authors, 50 per page, sorted by display name. Attributes have no effect on this loop |
Including another piece, {{> slug}}
{{> post-card}} inserts the element or template with that slug, passing the same item data into it. A slug that does not exist yields nothing.
One level only. A {{> ...}} inside an element that was itself included is not substituted.
Inserting the header and footer, {{header}} and {{footer}}
In a Page template, {{header}} and {{footer}} place the theme’s header and footer exactly where you want them. The stock page template already uses both. When a template contains them, SeedWebs does not wrap the page in a header and footer again, so nothing appears twice. In the landing template both tags are always empty, because that kind of page is meant to have neither.
Pagination, {{pagination}}
{{pagination}}
{{pagination class="px-3 py-2 text-sm text-gray-500" activeClass="px-3 py-2 text-sm font-semibold bg-primary text-white"}}
Works in every listing template (post-list, event-list, product-list, book-list, staff-list, tour-list, author-list), in the category and tag templates (category, tag, product-category, product-brand, tour-category, book-category), and in search-results. With only one page of results it is empty.
class must come before activeClass. Swap them and the tag is not recognised, and is printed literally on the page.
The page numbers come from the listing’s OWN loop — {{#each products}} on a product page, {{#each posts}} on a category page — so other loops in the same template, like an article sidebar, do not affect them — and such a sidebar shows the same newest items on page 2 and every later page as on page 1. When a template loops the same content twice with different limit=, the smaller page size wins, because that is the one a visitor clicks through. To choose for yourself, put paginate="true" on the loop the numbers should follow.
In a Page’s content (experimental sites). A Page in HTML mode can page a post grid by an address parameter of its own:
{{#each posts category="technology" limit=15 offset=5 page_param="query-7f09590f-page"}}
{{> post-card}}
{{/each}}
{{pagination}}
/technology?query-7f09590f-page=2 shows the next 15 posts, and {{pagination}} (above or below the loop) links each page as the Page’s own address with ?query-7f09590f-page=N — page 1 is the address alone, which is also what the page names as its canonical. offset still skips the first posts on every page. A page past the last shows an empty grid. When the content has more than one such loop, {{pagination param="query-7f09590f-page"}} says which one the links are for; here class and activeClass may come in any order. On any other site, or in Article mode, the attribute does nothing and a plain {{pagination}} in Page content prints nothing.
How many items in all, {{total}}
<p>Showing {{total}} products</p>
Prints the number of items the listing holds in all, not the number on the page — so a
category of 3,755 products prints 3755 on every page of it, which is what a “Showing 17–32 of
3,755 results” line needs.
It reads the same loop {{pagination}} counts, so the two always agree: the listing’s own
{{#each}}, or whichever loop you marked paginate="true". Other loops in the template, such as
an article sidebar, do not affect it.
Works in every listing template, in the category, tag, brand and author templates, and in
search-results — where {{result_count}}, which is search-only, prints the same number.
On a template with no listing loop, it prints 0.
Sorting a listing from the address, ?orderby=
A visitor can sort any listing page with two parameters on the URL:
/product-categories/sound-system?orderby=price&order=asc
/product-categories/sound-system?orderby=price&order=desc
/products?orderby=title&order=asc
orderby takes latest, oldest, title, price or views, and order takes asc or desc. ?orderby=price_desc in one parameter works too. The sort happens in the database before the page is cut into pages, so page 2 continues the same sorted list, and every page link keeps the sort. A cheapest-first dropdown is therefore just a set of links — the theme does not need to load the whole category to sort it.
price sorts on the price the card shows (the sale price when there is one). Products with no price come last either way. Ask for a sort a content type has no field for and the listing keeps its normal order.
Ready-made widgets
| Tag | Attributes | Defaults and notes |
|---|---|---|
{{search_form}} | class, placeholder, inputClass, buttonClass | A search form posting to that language’s /search, with a magnifier button in the text colour. Placeholder Search… by default |
{{search_overlay}} | buttonClass, iconColor, placeholder, theme, hint, hotkey | A magnifier button that opens a full-screen search field. theme takes light (default) or dark; hotkey="true" enables ⌘K / Ctrl+K. Injected once even if written in several places |
{{add_to_cart slug="…"}} | slug, variant, class, price, quote_href | That product’s buy box anywhere: a page, any template, the header or the footer. Same store rules as above; an unknown slug shows nothing. |
{{products}} | limit (8), columns (4, up to 6), collection, orderby, order, ids | A grid of product cards anywhere. collection is a product category slug; ids lists product slugs, comma-separated, shown in that order. Shows nothing while the Store is off |
{{product slug="…"}} | slug | One product card |
{{add_to_cart}} | variant, class, product_id, title, price, url | On a product template (no attributes needed): the buy box — price, variation picker, quantity, Add button. In a product card, variant="button" adds in one click (a variable product links to its page). Nothing until the store is on: Store settings → Checkout enabled plus one configured payment gateway |
{{mini_cart}} | class, iconColor, href, drawer | Header cart icon with a count badge (filled in the browser, never cached). Links to /cart; drawer="true" opens the drawer instead |
{{cart_drawer}} | position, theme | The slide-out cart: lines, quantity steppers, subtotal, View cart / Checkout. position right (default) or left; theme light or dark. Injected once; added before </body> automatically when the store is on and the theme has no tag |
{{floating_cart}} | position | A round cart button (bottom-left by default, position="bottom-right" to move it) that hides at zero and opens the drawer. Store settings → Floating cart: auto shows it only when the header has no {{mini_cart}}, on always, off never |
{{cart_page}} | — | The whole cart page: lines from the visitor’s browser, quantities, coupon field and totals fetched from the shop. In the cart template (what /cart renders); put anything around it |
{{checkout_page}} | — | The whole checkout: contact, delivery address, delivery method (from your shipping zones), payment method (from your configured gateways) and the Place order button. In the checkout template (what /checkout renders) |
{{subscribe_form}} | class, interests | An email signup form. interests="posts,events" ties subscribers to those interests |
{{social_share}} | networks, class, buttonClass, iconColor | networks takes copy, facebook, x, comma-separated; all three by default. iconColor="primary" uses your site’s primary colour. The copy button confirms for 2 seconds |
{{gallery}} | cols, gap, lightbox, class, group | The gallery attached to a Page or Post. Defaults to 3 columns, gap 3, lightbox on; turn it off with lightbox="false". This tag reads quoted values only — write cols="4", not cols=4 |
{{slider slug="hero"}} | slug (required) plus any config key | Slides from Sliders (/admin/sliders). Override the saved settings inline: autoplay="false", loop, arrows, dots, effect, speed, perView, gap, autoplaySpeed. Nothing while Sliders is off under Settings → General → Features; a slider of posts, products, events, books or staff is empty while that type is off |
{{ad slot="home-ads-1"}} | slot (required), class, imgClass, wrapClass, device, breakpoint, loading, limit | The live ad of that slot from Ads (/admin/ads): its link around a picture that switches to the phone image at 767px (breakpoint="1023" to move it). device="desktop" or "mobile" prints one image. wrapClass wraps it in a <div> only when an ad shows, so an empty slot leaves no gap. Only on sites with the Ads content type turned on |
{{form slug="contact"}} | slug or name, class, submit | A form from Forms (/admin/forms); button Submit by default. An unknown or inactive slug yields nothing, as does every form while Forms is off under Settings → General → Features |
{{chat_widget}} | position, theme, welcome | The AI chat button. Appears only with AI chat enabled and a key saved under Settings → General → AI. position takes bottom-right (default) or bottom-left |
{{cookie_consent}} | None | The cookie bar. Appears only when enabled under Settings → Cookies (/admin/settings/cookies) |
{{youtube}} | url or id (required), mode | A YouTube video: url a watch, youtu.be, shorts, embed or live address, or id the 11-character code. mode="embed" (default) loads the player (youtube-nocookie.com) on click; mode="link" is a card opening YouTube in a new tab. The Article editor’s YouTube button writes this tag. See Videos and social posts below |
{{instagram}}, {{facebook}}, {{tiktok}} | url (required; {{tiktok}} also takes id), mode, size, thumb, label | One post or video you pick. mode="link" (default) is a card that opens the post in a new tab; mode="embed" is a cover that loads the network’s own player when clicked. size="fill" fills the column. See Videos and social posts below |
{{tiktok_profile}} | user (required), label | A TikTok account’s latest videos, loaded when clicked, with a link to the profile underneath. See Videos and social posts below |
{{map}} | url (required), label | A Google map, loaded when clicked. See Maps below |
{{social_feed}} | source, limit, columns, gap, aspect, link, follow, follow_label, part | Social media beta sites only: your newest imported posts as a grid of pictures. See Your imported posts as a grid below |
{{pdf_button}} | url (required), label, mode | A button to a PDF. mode="download" (default) downloads it; mode="view" opens it in a new tab. The default button text is Thai on every page (ดาวน์โหลด PDF, ดูเอกสาร PDF), so set label on an English page |
With the gallery switch on in a Page or Post editor and no {{gallery}} in the content, SeedWebs appends one at the end; type the tag yourself mid-content to place it.
{{subscribe_form}} in the Header or Footer submits without reloading and, without interests, shows interest checkboxes from your configuration; in Page content it is an ordinary form that reloads the page.
It shows and accepts signups only while Newsletters is on under Settings → General → Features — the switch that opens Subscribers and Newsletters, which are plan-limited and need the experimental flag (Plans and billing). Off, the tag prints nothing, so no address is collected that you could not see.
{{chat_widget}} and {{cookie_consent}} are injected automatically when enabled and the theme has no such tag; write them only to set the position or colour theme, or when the chat widget’s own switch for automatic injection is off.
Bot protection for forms and subscribe boxes
A bot can submit a form without opening your page, hundreds of times from different addresses. Bot protection stops most of that: switch it on in the Bot protection card at the top of Forms → All Forms (/admin/forms) or Newsletters → Subscribers (one setting). Only Super Admin and Admin see it; it is off until you turn it on. Donation forms are always protected, whatever it says.
With it on:
- Visitors see nothing. Starting a form quietly fetches a one-time pass. A submission without a valid pass (a bot that never opened the page, or a reused pass) is answered as if it went through, but nothing is saved, emailed or sent to webhooks; a subscribe box still returns with
subscribed=1. - Minimum seconds (1–30, default 3) between starting a form and sending it; a quicker visitor is held for the rest of it, not refused.
- The same answers sent to the same form twice within 24 hours are saved once, so a double-click no longer makes two entries.
- It covers
{{form}},{{subscribe_form}}and forms you wrote yourself in the theme, as long as they send to/api/public/forms/…or/api/public/subscribe. - A script or another site sending to those addresses directly must fetch a pass from
/api/public/form-token?k=subscribe(or?k=and the form’s slug), wait the minimum seconds, and send itstokenas a field named_bt; one pass, one submission.
Two optional limits count all visitors together, for a bot that keeps changing address (empty = no limit). They work with the switch off too, and once one is reached real visitors are turned away too until the next hour — set one only if needed; an event registration can have a genuine rush:
- Limit across all visitors (per hour) in a form’s settings, under Uploads & limits, next to the per-IP limit. Past it, the form answers that it has received too many submissions.
- Sign-ups per hour, all visitors in the Bot protection card, for subscribe boxes.
Sliders that show posts
A slider whose Source is Posts (Sliders → edit the slider) builds one slide per post. Its two settings below came in October 2026; a slider saved before then has neither on and shows what it always did until you change it.
Which posts. Two lists of your categories:
- Only these categories — posts filed in any category you tick, including its sub-categories. Tick none to show all posts.
- Leave out these categories — drops posts filed in a ticked category (that category only, not its sub-categories).
Limit and Order still apply, and the slider always shows posts in the language of the page it is on. If the slider has a value typed into the old “Category slug” box, it appears above the list as an old value: it never filtered the slider, and it does not now. Remove it, or tick the categories you meant.
Fill slides like posts loop cards. With this ticked (it is, on a new slider and with the Posts carousel template), the Item template is written like a card in {{#each posts}}, with the same tags:
<a href="{{item.url}}">
<img src="{{item.featured_image}}" srcset="{{item.featured_image_srcset}}" alt="{{item.title}}">
{{#if item.categories}}<span>{{item.categories.0.name}}</span>{{/if}}
<h2>{{item.title}}</h2>
<p>{{item.excerpt}}</p>
</a>
{{item.url}} is the post’s address, {{item.featured_image}} and {{item.featured_image_srcset}} its cover image, {{item.excerpt}} the excerpt you wrote (or one taken from the content), {{item.date}} the post date. {{item.categories.0.name}} is the post’s main category; {{#each item.categories}}{{item.name}}{{/each}} lists them all. With the box unticked, a slide gets the post as it is stored: {{item.featured_image}} is a number rather than an image address, and there is no excerpt or date. Sliders whose source is Products, Events, Books or Staff work that way too.
The tour booking form
{{book_tour_form}} works only in a tour detail template (tour-detail-page or tour-detail), and requires tour_id — without it you get nothing. The stock template writes it like this:
{{book_tour_form tour_id="{{item.id}}" tour_slug="{{item.slug}}" tour_title="{{item.title}}" currency="{{item.currency}}" adult_price="{{item.adult_price}}" child_price="{{item.child_price}}"}}
With no payment gateway configured, the widget shows a notice instead of the form. Tours need the experimental flag (see What SeedWebs does not have).
Press releases from an outside feed, {{external_feed}}
On an experimental site with a feed set up (Settings ▸ General ▸ Advanced URLs ▸ External feeds), {{external_feed feed="dataxet_pr" lang="th" limit="50"}} in a page body lists that feed’s newest press releases (more in Pages and posts). Attributes: feed (required, the feed’s name), lang (th or en; the page’s language by default), limit (1–50, default 10), columns (1–3, default 1), and part / empty / error to use your own elements instead of the built-in markup (part="pr-list-th" also looks for pr-list-th-empty and pr-list-th-error). Your element gets the list as {{#each item.items}}…{{/each}}, each release with {{item.headline}}, {{item.url}}, {{item.excerpt}}, {{item.date}} (also date_th, date_en), {{item.display_time}} and {{item.doc_id}}. On any other site, and for a feed that is switched off, the tag is shown as typed.
Videos and social posts
{{youtube}}, {{instagram}}, {{facebook}} and {{tiktok}} each show one video or post you pick, and {{tiktok_profile}} shows a TikTok account’s latest videos. They work on every site, with no account connected. The Social blocks under Add Block in the HTML Builder are ready-made grids of them, plus a single YouTube video block (see Social media).
{{instagram url="https://www.instagram.com/reel/AbC123/" mode="embed" size="fill"}}
{{facebook url="https://www.facebook.com/yourpage/posts/123" label="Our Songkran sale"}}
{{tiktok url="https://www.tiktok.com/@yourshop/video/7291234567890123456" mode="embed"}}
{{tiktok_profile user="yourshop"}}
{{youtube url="https://www.youtube.com/watch?v=dQw4w9WgXcQ"}}
urlis the post’s address on instagram.com (/p/,/reel/or/tv/), facebook.com or fb.watch, or tiktok.com (with/video/). Any other site, an Instagram or TikTok address that is not a post (a profile, a search), or a shortvt.tiktok.comlink shows nothing — open that one in a browser and copy where it lands.{{tiktok}}also takes the video’s number asid. For{{facebook}}withmode="embed", use the address-bar address (with/posts/or/videos/), not a Share → Copy link one (with/share/); a link card takes either.- For
{{instagram}},{{facebook}}and{{tiktok}},mode="link"(default) is a card that opens the post in a new tab and loads nothing from the network;mode="embed"is a cover with the network’s player behind it, loading nothing until clicked (the cover says opening it loads that network’s content, which may set cookies, and the network then learns which page it was opened from). Ctrl-click or ⌘-click opens a new tab instead. - Players have a minimum width: 326px for Instagram, 350px for a Facebook post and 288px for
{{tiktok_profile}}. On a narrower screen, clicking the cover opens the post on the network in a new tab instead. In a grid, give Instagram covers columns at least 340px wide, and usemode="link"for Facebook. size="fill"makes the card or cover as wide as its column, for grids. Without it, a card is at most 420px wide, and a cover is at most 480px (Instagram), 500px (Facebook) or 340px (TikTok) wide and centred.thumbis the cover picture from your own media library (your photo, or a frame of your own video) — never a screenshot of someone else’s post. An image on Instagram, Facebook, TikTok or YouTube is ignored (it would contact the network before a click). Without it the cover is a light-grey box with the text.labelis the card’s text: by default “Watch on Instagram” for a video (reel, TikTok or Facebook video), else “View on Instagram” — Thai on a Thai page, English otherwise. Give each post its own (what it shows), or every card and cover reads the same, to screen readers too.
{{youtube}} takes only url or id, and mode. Its default is mode="embed", where the player waits for the click. In both modes the video’s picture loads from YouTube’s image server with the page, before any click, so mode="link" does not avoid contacting YouTube.
{{tiktok_profile user="yourshop"}} shows up to 10 of an account’s latest videos (TikTok picks them) in TikTok’s box, loaded on click; give it the page’s full width. user is the username (with or without @) or https://www.tiktok.com/@yourshop; an invalid name shows nothing. A private account, one under 18 or with no age set shows TikTok’s private card, an unknown name “Profile not available”; the View @yourshop on TikTok link under it always works. label changes the cover text.
Write each tag on its own line. You can type or paste a tag in the Article editor, in a Markdown post or in the HTML Builder. Inside an {{#each}} loop in Page content, a tag cannot take a card’s field: url="{{item.field.ig}}" shows nothing.
Type plain " quotes: curly ones (“ ”, common on phone keyboards) work only when every quote in the tag is curly — in a mixed tag only the straight pairs are read. In a Markdown post, keep * and _ out of a label, or the card shows HTML such as <em>.
Pasting an address in the Article editor
A YouTube, Instagram, Facebook or TikTok address pasted alone on an empty line becomes its tag; Ctrl+Z (⌘Z) once keeps the link. In a sentence, list or table, or for an address no tag shows (vt.tiktok.com, a Facebook Page, Vimeo), it stays a link. WordPress embed blocks with such an address play as these tags too.
Maps, {{map}}
{{map url="https://www.google.com/maps/embed?pb=!1m18!1m12…" label="Our shop on Sukhumvit"}}
url is the src of Google Maps’ Share → Embed a map code; in the Article editor, paste the whole code on an empty line. Other map addresses show nothing. The map fills its column and loads from Google only when clicked; for one without the click, paste Google’s code into an HTML Builder page.
Your imported posts as a grid, {{social_feed}}
On a site in the social media beta (see Social media), {{social_feed}} shows the newest published articles imported from Instagram, Facebook and TikTok as a grid of their pictures, newest first. A new import appears by itself; an article you unpublish, trash or delete leaves the grid. Attributes: source (instagram, facebook, tiktok, or all, the default), limit (1–24, default 9), columns (1–6, default 3; fewer on a narrow screen, so no picture is under 80px), gap (0–24 pixels, default 4), aspect (square by default, portrait 4:5, tall 9:16), link (source, the default: the post on the network, in a new tab; article: the article on your site), follow (your profile’s https:// address, for a button under the grid) and follow_label (its text; by default “Follow us on Instagram”, in the page’s language). Posts you pin on Tools → Social → Social feed come first (up to 6), posts you hide never show, and an account switched off there leaves every grid. An account set to Show on the site only adds its newest posts, which always open on the network (there is no article). A post with no picture shows its text on a grey tile, and a video gets a small play mark. The pictures are copies SeedWebs keeps (an imported article’s are in your media library), so nothing loads from the network before a click. part="my-feed" uses your own element instead: it gets {{#each item.items}}…{{/each}}, each post with {{item.url}}, {{item.image}}, {{item.title}}, {{item.excerpt}}, {{item.provider_label}}, {{item.is_video}}, {{item.date}} and {{item.pinned}} (1 for a pinned post), plus {{item.follow_url}} and {{item.follow_label}}; in the built-in grid a pinned tile carries data-pinned="1" for your CSS. It works in your theme’s Header and Footer too, so one grid can sit at the foot of every page. With nothing to show, the tag leaves only an HTML comment. On a site outside the beta, or whose plan does not include social import, it shows nothing.
Page-specific tags
| Page / template | Tags available |
|---|---|
search-results | {{search_query}}, {{result_count}}, {{#if search_query}}...{{else}}...{{/if}}, {{#each search}}, {{search_form}}, {{pagination}} |
category | {{category_name}}, {{category_slug}}, {{category_description}}, {{category_body}}, {{category_image}}, {{category_icon}}, {{current_q}}, {{current_subcat}}, {{subcategory_list}}, {{category_search_input}}, {{#if category_...}} and {{#unless category_...}}; with nested category addresses on, also {{category_url}}, {{category_parent_name}} and {{category_parent_url}} |
tag | {{tag_name}}, {{tag_slug}} |
date-archive | {{archive_title}} (“May 2024”, “พฤษภาคม 2024” — Western year), {{#each posts}}, {{total}}, {{pagination}} — only on an experimental site with date archives switched on |
product-category, tour-category, staff-category, portfolio-category, program-category, book-category | {{category_name}}, {{category_slug}}, {{category_description}}, {{category_body}}, {{category_image}}, {{category_icon}}, {{#if category_...}} and {{#unless category_...}} |
product-brand | {{brand_name}}, {{brand_slug}} |
not-found | {{site_name}} only |
tour-thankyou | {{booking_number}}, {{locale_prefix}} |
{{category_body}} is the category’s own page — the copy that belongs above the list, edited in the category form under Archive Page Content. It is inserted as HTML, so headings, lists and images all work, and an {{#each}} block inside it is expanded too, which is how a category page shows its copy and then its own rows. {{category_description}} is the short intro beside it and is inserted as plain text — it also becomes the page’s meta description. If your old site had a real page at /programs/bachelors/ followed by the list of bachelor programmes, that page is the category: put it here rather than creating a programme that is not a programme.
Where a sub-category has no {{category_description}}, {{category_body}}, {{category_image}} or {{category_icon}} of its own, its parent’s value is used — so a faculty can write one shared intro on the parent. {{category_icon}} inserts SVG as code — use fill="currentColor" in the SVG so it picks up the theme’s colour.
{{subcategory_list}} takes outerClass, labelClass, wrapClass and selectClass, and renders only when the category has sub-categories. {{category_search_input}} takes placeholder and class, and waits 450 milliseconds after typing before filtering.
Both work together with loading the list without a page refresh, which requires id="sw-posts-grid" around the card grid and id="sw-pagination" around the pagination in the category template.
Where each tag works
The most common confusion is that the same tag does not work everywhere. This table follows the code.
| Tag | Header / Footer | Elements / Templates | Page content | Post, Product, Event content |
|---|---|---|---|---|
{{site_name}}, {{tag_line}}, {{year}} | Yes | No | Yes | No |
{{nav}}, {{site_logo}}, {{language_switcher}}, {{locale_home}} | Yes | No | No | No |
{{search_overlay}}, {{chat_widget}}, {{cookie_consent}} | Yes | No | No | No |
{{mini_cart}}, {{cart_drawer}}, {{floating_cart}} | Yes | Yes | Yes | No |
{{add_to_cart}} | No | Product templates and cards | Inside a {{#each products}} loop | No |
{{products}}, {{product}}, {{product_collections}}, {{product_page}}, {{add_to_cart slug="…"}} | Yes | Yes | Yes | No |
{{cart_page}}, {{checkout_page}} | No | cart / checkout templates | Yes | No |
{{search_form}} | Yes | Every template | Yes | No |
{{item.*}}, {{#if item.*}}, {{> partial}} | No | Yes | No | No |
{{#each posts}} and the other types | No | Yes | Yes | No |
{{slider}} | Page and home page only | Yes | Yes | No |
{{form}} | No | Yes | Yes | No |
{{ad}} | Yes | Yes | Yes | Yes |
{{subscribe_form}} | Yes | No | Yes | No |
{{social_share}} | Yes | Yes | Yes | Yes |
{{gallery}} | No | Yes | Yes | Yes |
{{youtube}}, {{instagram}}, {{facebook}}, {{tiktok}}, {{tiktok_profile}}, {{map}}, {{pdf_button}} | No | Page and detail templates, not listing, search or 404 templates | Yes | Yes |
{{social_feed}} (beta sites) | Yes | Page and detail templates, not listing or 404 templates | Yes | No |
{{pagination}} | No | Listing templates only | Experimental sites, with a page_param loop (HTML mode) | No |
Page content additionally has {{title}}, {{slug}}, {{featured_image}}, {{featured_image_mobile}}, {{seo_title}}, {{seo_description}} and {{item.field.<key>}} (all working in both Article and HTML Builder mode) — but {{#if}} does not work in Page content outside an {{#each}} loop. Inside a loop the body is a card: {{#if item.*}}…{{else}}…{{/if}} works, and {{item.field.<key>}} is each card’s own field, not the page’s.
{{locale_prefix}} works in Elements, Templates and Post content. In Page content it works inside an {{#each}} loop and is empty outside one.
The video, social, map and PDF tags have a few exceptions. On a home page written in Article mode they, {{social_feed}} and the product tags ({{products}}, {{product}}, {{product_collections}}, {{product_page}}, {{add_to_cart slug="…"}}) are the only tags that work: every other tag prints as typed there. In an {{#each}} card of a listing page, search results or a theme’s page template they print as typed. On an Article-mode page nested under a built-in address such as /search/…, a cover opens the post or map in a new tab instead of loading it on the page. In slides you write yourself in a slider, and in banners (Settings → Banners), they print as typed. {{pdf_button}} shows nothing in a post written in Markdown: write that post in the Article editor and use the PDF button on its toolbar.
Limits to know before you start
- Post, Product, Event, Book, Doc and Tour content accepts only
{{gallery}},{{social_share}},{{locale_prefix}},{{pdf_button}}and the video, social and map tags ({{youtube}},{{instagram}},{{facebook}},{{tiktok}},{{tiktok_profile}},{{map}}), plus{{ad}}on a site with Ads turned on. Tags like{{#each}},{{slider}}and{{form}}typed into that content do nothing. Move them into that content type’s template, or use a Page instead. {{item.url}}has a value only inside an{{#each}}loop. In a detail template, build the link yourself:{{locale_prefix}}/posts/{{item.slug}}.- In a detail template,
{{item.*}}has not been substituted yet at the moment{{#each}}runs, so{{#each posts category="{{item.slug}}"}}is not possible. The exception is{{book_tour_form}}, which is processed later and therefore does accept{{item.id}}. For a post’s own categories, an experimental site can writecategory="current"(see the attribute table). - On an author-detail page,
{{#each posts}}already filters to that author’s posts. {{locale_prefix}}is empty in the primary language and/thin another. Use it on every link in a theme so readers do not fall out of the language they are reading.- If your theme has no template for a page, SeedWebs falls back to the default theme’s. See Themes and templates.
Tags in email templates
Each of these Emails-tab templates fills in only the tags listed.
| Template | Tags filled in |
|---|---|
email-welcome (sent on a new subscription) | {{site_name}}, {{email}}, {{unsubscribe_url}} |
email-newsletter (sending a newsletter) | {{subject}}, {{body}}, {{site_name}}, {{email}}, {{unsubscribe_url}} |
email-order-notification (a new store order, to you) | {{site_name}}, {{order_number}}, {{order_date}}, {{customer_name}}, {{customer_email}}, {{customer_phone}}, {{shipping_method}}, {{shipping_address}}, {{order_items}}, {{order_subtotal}}, {{order_discount}}, {{order_shipping}}, {{order_tax}}, {{order_total}}, {{payment_method}}, {{payment_status}}, {{order_notes}}, {{order_admin_url}} |
A theme missing one of the first two sends the Default theme’s copy; an email that lost {{unsubscribe_url}} gets an unsubscribe line added. Add email-order-notification yourself (Add email, active theme) to replace the built-in new-order email. Other stock tags, like {{confirm_url}} and {{resubscribe_url}}, are never filled in and appear literally.
Start from what is already there
The quickest way in is to open an element or template that shipped with the platform and see which tags it uses. post-card packs almost every technique in this document into one file.
<a href="{{item.url}}" class="group block rounded-lg border">
{{#if item.featured_image}}
<img src="{{item.featured_image}}" srcset="{{item.featured_image_srcset}}" alt="{{item.title}}" loading="lazy" />
{{/if}}
<div class="p-4">
{{#each item.categories}}<span class="text-xs uppercase">{{item.name}}</span>{{/each}}
<h3 class="mt-2 font-semibold">{{item.title}}</h3>
{{#if item.excerpt}}<p class="mt-1 text-sm text-gray-500">{{item.excerpt}}</p>{{/if}}
<div class="mt-3 text-xs text-gray-400">
{{#if item.created_at_formatted}}<time>{{item.created_at_formatted}}</time>{{/if}}
{{#if item.read_time_label}}<span>· {{item.read_time_label}}</span>{{/if}}
</div>
</div>
</a>
The theme called default cannot be deleted, and neither can the parts that shipped with it — but Reset restores the factory version, so you can experiment without losing anything.