Every template tag and how to use it
The template tags SeedWebs actually supports, with their attributes, defaults, examples, and a table of which tags work in which part of a theme.
Last updated 2026-08-11
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 |
{{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"}}
SeedWebs stores one menu per site, bound to the header location. Any value other than mobile — including location="footer" — renders the same links as the header in the desktop style. For different footer links, write the <a> tags in the Footer code yourself.
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}} | The created date with a full month name, in the content’s language |
{{item.date_short}} | The created date with an abbreviated month |
{{item.date locale="th" style="short" order="dmy"}} | Force the language (th, en), the month form (long by default, short) and the order (dmy by default, mdy) |
{{item.created_at_formatted}} | The same as {{item.date}}, spelled out |
{{item.created_at_formatted_short}} | The same as {{item.date_short}} |
{{item.updated_at_formatted}} | Every column ending _at has a _formatted and _formatted_short companion |
{{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 price as ฿1,234 (with sale_price_formatted and amount_formatted companions) |
{{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) gets two extra tags automatically.
| Tag | Result |
|---|---|
{{item.featured_image}} | The main image URL |
{{item.featured_image_mobile}} | The same URL with -sm inserted before the extension |
{{item.featured_image_srcset}} | URL 1200w, URL-sm 768w, for the srcset attribute |
<img src="{{item.featured_image}}" srcset="{{item.featured_image_srcset}}" alt="{{item.title}}" loading="lazy" />
These are built by string manipulation. SeedWebs does not check that the -sm file exists, so if that image has no mobile version in Media, the link points at a file that is not there.
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.
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.
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 {{> ...}}.
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.
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 enabled, its listing and detail pages answer 404 — so {{item.url}} in a card leads to a Not Found page.
| 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 |
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}}
Loops that only work on certain pages
| Tag | Template | Notes |
|---|---|---|
{{#each search}} | search-results | Search results. Inside it: {{item.url}}, {{item.type}}, {{item.title}}, {{item.excerpt}}, {{item.featured_image}} |
{{#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. 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.
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. The button carries a magnifier that takes its colour from the surrounding text. Placeholder defaults to Search… |
{{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 |
{{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 |
{{form slug="contact"}} | slug or name, class, submit | A form from Forms (/admin/forms). The submit button says Submit by default. An unknown slug, or one whose status is inactive, yields nothing |
{{chat_widget}} | position, theme, welcome | The AI chat button. Appears only with AI Features enabled and a key saved under Settings → General. position takes bottom-right (default) or bottom-left |
{{cookie_consent}} | None | The cookie bar. Appears only when enabled under Settings → Cookies (/admin/settings/cookies) |
{{gallery}} has one special behaviour: with the gallery switch on in a Page or Post editor and no such tag in the content, SeedWebs appends {{gallery}} to the end of the content for you. To control where it goes, type the tag yourself mid-content.
{{subscribe_form}} renders in two forms depending on where it sits. In the Header or Footer you get a form that submits without reloading the page, and which fills in interest checkboxes from your configuration when interests is not specified. In Page content you get an ordinary form that reloads the page.
The form accepts signups on any site, but the Subscribers and Newsletters screens are plan-limited and need the experimental flag. Without it you will collect the addresses but be unable to view them or send a newsletter (see Plans and billing).
{{chat_widget}} and {{cookie_consent}} are injected automatically when enabled and the theme has no such tag, so you do not need to write them unless you want to set the position or colour theme yourself. (The chat widget has its own switch to disable the automatic injection; with that off, you must write the tag.)
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 are only available on sites where the team has turned on the experimental flag (see What SeedWebs does not have).
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_image}}, {{category_icon}}, {{current_q}}, {{current_subcat}}, {{subcategory_list}}, {{category_search_input}}, and {{#if category_...}} |
tag | {{tag_name}}, {{tag_slug}} |
product-category, tour-category, book-category | {{category_name}}, {{category_slug}} |
product-brand | {{brand_name}}, {{brand_slug}} |
not-found | {{site_name}} only |
tour-thankyou | {{booking_number}}, {{locale_prefix}} |
Where a sub-category has no {{category_description}}, {{category_image}} or {{category_icon}} of its own, its parent’s value is used. {{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 |
{{search_form}} | Yes | search-results only | No | 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 |
{{subscribe_form}} | Yes | No | Yes | No |
{{social_share}} | Yes | Yes | Yes | Yes |
{{gallery}} | No | Yes | Yes | Yes |
{{pagination}} | No | Listing templates only | No | 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.
{{locale_prefix}} works in Elements, Templates and Post content. In Page content it is always empty.
Limits to know before you start
- Post, Product, Event, Book, Doc and Tour content accepts only
{{gallery}},{{social_share}}and{{locale_prefix}}. 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}}. - 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
The Emails tab holds several templates, but only two are actually sent, and each fills in only these tags.
| 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}} |
Other tags found in the stock email templates, such as {{confirm_url}} and {{resubscribe_url}}, have nothing filling them in yet. Left in place they appear literally in the email.
Newsletters and the subscriber list are plan-limited and need the experimental flag (see Plans and billing).
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.