TH SeedWebs

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-08

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:

TabFor
GeneralHeader HTML, Footer HTML, Global CSS, Global JS, Body class
NavThe menu’s elements (nav-item, nav-submenu, nav-mobile-item and so on)
ElementsReusable pieces, such as post-card
TemplatesPage templates, such as post-list, post-detail, search-results
EmailsEmail 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: limit and offset in {{#each}} must be bare numbers with no quotes (limit="12" is not read and the default is used instead), and chars and words on {{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.

TagResult
{{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.

AttributeResult
classClasses for the wrapping <ul>, merged with the default flex items-center gap-1
itemClassClasses for each <a>
activeClassClasses 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, endOnly 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

TagResult
{{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 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.

TagResult
{{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" />

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.

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 a site with the experimental flag, {{#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 ▸ Content 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 item.category_ancestors nor a category’s {{item.url}} is filled in.

Categories and tags per language (sites moved from WordPress with WPML). With Settings ▸ General ▸ Languages ▸ 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.

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 → Admin Menu, 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.

AttributeDefaultWorks with
limit=126Every type
offset=30Every type (skip the first few, for a separate featured item)
order="views"Per typeAccepts 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 filterposts (including sub-categories), products, tours, books
tag="featured"No filterposts
author="somchai"No filterposts
since="3m"No filterposts, events, products, tours
category="tech,business"—posts, on sites with the experimental flag: posts in any of the categories (sub-categories included). 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
exclude_category="pr"No filterposts, experimental sites: leaves out posts filed in those categories (comma-separated; the categories themselves, not their sub-categories)
exclude="current"Offposts in a post template, experimental sites: leaves out the post being shown — a “related posts” row without the post itself
page_param="query-7f09590f-page"Offposts 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"OffEvery 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

TagTemplateNotes
{{#each search}}search-resultsSearch 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 → Admin Menu, in the page’s language; without one it prints the same key as {{item.type}}
{{#each authors}}author-listAuthors, 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.

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.

In a Page’s content (sites with the experimental flag). A Page in HTML mode can page a post grid the way a WordPress page built with GenerateBlocks does — 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

TagAttributesDefaults and notes
{{search_form}}class, placeholder, inputClass, buttonClassA 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, hotkeyA 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}}variant, class, product_id, title, price, urlOn a product template: the buy box — price, variation picker, quantity and the Add button. In a product card write variant="button" for a one-click add (a variable product links to its page instead). Reads the product it is rendered with, so it needs no attributes there. Renders nothing until the store is on: Store settings → Checkout enabled, plus one configured payment gateway
{{mini_cart}}class, iconColor, href, drawerCart icon with a count badge, for the header. Links to /cart; drawer="true" opens the slide-out drawer instead. The badge is filled in the browser, so the count is never cached into the page
{{cart_drawer}}position, themeThe slide-out cart: lines, quantity steppers, subtotal, View cart / Checkout. position takes right (default) or left; theme takes light or dark. Injected once. When the store is on and the theme has no tag, it is added automatically before </body>
{{floating_cart}}positionA 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: line list from the visitor’s browser, quantities, coupon field and authoritative totals fetched from the shop. Lives in the cart template, which is what /cart renders; put anything you like 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. Lives in the checkout template, which is what /checkout renders
{{subscribe_form}}class, interestsAn email signup form. interests="posts,events" ties subscribers to those interests
{{social_share}}networks, class, buttonClass, iconColornetworks 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, groupThe 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 keySlides from Sliders (/admin/sliders). Override the saved settings inline: autoplay="false", loop, arrows, dots, effect, speed, perView, gap, autoplaySpeed. Nothing shows while Sliders is switched off under Settings → General → Admin Menu, and a slider that shows posts, products, events, books or staff has no slides while that type is switched off
{{ad slot="home-ads-1"}}slot (required), class, imgClass, wrapClass, device, breakpoint, loading, limitThe 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, submitA form from Forms (/admin/forms). The submit button says Submit by default. An unknown slug, or one whose status is inactive, yields nothing — and so does every form while Forms is switched off under Settings → General → Admin Menu
{{chat_widget}}position, theme, welcomeThe 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}}NoneThe cookie bar. Appears only when enabled under Settings → Cookies (/admin/settings/cookies)
{{youtube}}url or id (required), modeA YouTube video. url takes a watch, youtu.be, shorts, embed or live address; id is the 11-character video code. mode="embed" (default) shows the video’s picture and loads the player (from youtube-nocookie.com) only when clicked; mode="link" is a card with a small copy of the picture that opens YouTube in a new tab. In both modes the picture loads from YouTube’s image server with the page, before any click. The YouTube button in the Article editor’s toolbar writes this tag for you. See Videos and social posts below
{{instagram}}, {{facebook}}, {{tiktok}}url (required; {{tiktok}} also takes id), mode, size, thumb, labelOne 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), labelA TikTok account’s latest videos, loaded when clicked, with a link to the profile underneath. See Videos and social posts below
{{map}}url (required), labelA Google map, loaded when clicked. See Maps below
{{social_feed}}source, limit, columns, gap, aspect, link, follow, follow_label, partSocial 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, modeA 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

{{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 shows, and accepts signups, only while Newsletters is switched on under Settings → General → Admin Menu — the same switch that opens the Subscribers and Newsletters screens, which are plan-limited and need the experimental flag (see Plans and billing). With Newsletters 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, 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.)

Bot protection for forms and subscribe boxes

A bot can fill in a form without ever opening your page, and send the same thing 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 of Newsletters → Subscribers — it is the same setting in both places. Only Super Admin and Admin see the card, and it is off on every site until you turn it on.

With it on:

  • Visitors see nothing. When someone starts filling in a form, the page quietly fetches a one-time pass for that form. A submission without a valid pass — from a bot that never opened the page, or one reusing a pass — is answered as if it went through, but nothing is saved, emailed or sent to your form webhooks. A subscribe box still goes back to the page with subscribed=1.
  • Minimum seconds (1–30, default 3) is the least time between starting to fill in the form and sending it. A real visitor who is quicker is not refused: the submission is held for the rest of that time, then sent.
  • 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.
  • It works on your own pages only. A script or another site that sends to those addresses directly must first fetch a pass from /api/public/form-token?k=subscribe (or ?k= and the form’s slug), wait at least the minimum seconds, and send the token it gets back as a field named _bt. One pass is good for one submission.

Two optional limits count every visitor together, for a bot that keeps changing its address. Leave them empty for no limit. They work even with the switch off, and once one is reached real visitors are turned away too until the next hour, so set one only if you need it — 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. Two settings under the source were added in October 2026. A slider saved before then has neither turned on and keeps showing exactly what it showed, 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 are only available on sites where the team has turned on the experimental flag (see What SeedWebs does not have).

Press releases from an outside feed, {{external_feed}}

On a site where the SeedWebs team has turned on experimental features and set up a feed (Settings ▸ General ▸ External feeds), {{external_feed feed="dataxet_pr" lang="th" limit="50"}} in a page body lists the newest press releases from that feed — read from the provider each time the page is built, and kept for a few minutes (5 by default), so a new release shows without anyone editing the site. 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}}. Each release also has its own page at the feed’s address, such as /pr-th/<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"}}
  • url is the post’s address on instagram.com (a /p/, /reel/ or /tv/ address), facebook.com or fb.watch, or tiktok.com (an address with /video/ in it). An address on any other site shows nothing, and so does an Instagram or TikTok address that is not a post, such as a profile or a search. A short vt.tiktok.com link from the TikTok app shows nothing either: open it in a browser first and copy the address it lands on. {{tiktok}} also accepts the video’s number alone as id. For {{facebook}} with mode="embed", use the post’s own address from the browser’s address bar (with /posts/ or /videos/ in it) rather than a Share → Copy link address (with /share/ in it); a link card works with 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 own player behind it. Nothing from Instagram, Facebook or TikTok loads until the visitor clicks, and the cover says that opening it loads content from that network, which may set cookies. After the click, the network also learns which page the post was opened from. Ctrl-click or ⌘-click opens the post in 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 use mode="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.
  • thumb is the cover picture: the address of an image in your own media library, such as your own photo or a frame of your own video. Never use a screenshot of someone else’s post. An image address on Instagram, Facebook, TikTok or YouTube is ignored, because it would contact that network before the visitor clicks. Without thumb, the cover is a plain light-grey box with the text.
  • label is the card’s text. By default it reads “Watch on Instagram” for a video (a reel, a TikTok video, a Facebook video) and “View on Instagram” for any other post: in Thai on a Thai page, and in English on a page in any other language. Give each post its own label, such as what the post shows: otherwise every card and cover on the page 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 own box, loaded when the visitor clicks the cover. user is the username, with or without @, or the profile’s address https://www.tiktok.com/@yourshop; anything that is not a valid TikTok username shows nothing. Give it the full width of the page. A private account, an account under 18, or one with no age set shows TikTok’s private card instead, and a name TikTok does not know shows “Profile not available”. The View @yourshop on TikTok link under the box always works. label changes the text on the cover.

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 the quotes as plain ". Curly quotes (“ ”), which phone keyboards often type, work only when every quote in the tag is curly: in a tag that mixes the two, only the straight pairs are read. In a Markdown post, leave * and _ out of a label: Markdown turns them into bold or italics, and the card then shows HTML code 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, opens the post on the network in a new tab; article opens 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 to the same grid, newest first; those always open the post on the network, even with link="article", since they have no article on your site. 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 / templateTags 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 a site moved from WordPress 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.

TagHeader / FooterElements / TemplatesPage contentPost, Product, Event content
{{site_name}}, {{tag_line}}, {{year}}YesNoYesNo
{{nav}}, {{site_logo}}, {{language_switcher}}, {{locale_home}}YesNoNoNo
{{search_overlay}}, {{chat_widget}}, {{cookie_consent}}YesNoNoNo
{{mini_cart}}, {{cart_drawer}}, {{floating_cart}}YesYesYesNo
{{add_to_cart}}NoProduct templates and cardsInside a {{#each products}} loopNo
{{cart_page}}, {{checkout_page}}Nocart / checkout templatesYesNo
{{search_form}}YesEvery templateYesNo
{{item.*}}, {{#if item.*}}, {{> partial}}NoYesNoNo
{{#each posts}} and the other typesNoYesYesNo
{{slider}}Page and home page onlyYesYesNo
{{form}}NoYesYesNo
{{ad}}YesYesYesYes
{{subscribe_form}}YesNoYesNo
{{social_share}}YesYesYesYes
{{gallery}}NoYesYesYes
{{youtube}}, {{instagram}}, {{facebook}}, {{tiktok}}, {{tiktok_profile}}, {{map}}, {{pdf_button}}NoPage and detail templates, not listing, search or 404 templatesYesYes
{{social_feed}} (beta sites)YesPage and detail templates, not listing or 404 templatesYesNo
{{pagination}}NoListing templates onlyExperimental 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 and {{social_feed}} 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

  1. 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.
  2. {{item.url}} has a value only inside an {{#each}} loop. In a detail template, build the link yourself: {{locale_prefix}}/posts/{{item.slug}}.
  3. 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, a site with the experimental flag can write category="current" (see the attribute table).
  4. On an author-detail page, {{#each posts}} already filters to that author’s posts.
  5. {{locale_prefix}} is empty in the primary language and /th in another. Use it on every link in a theme so readers do not fall out of the language they are reading.
  6. 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.

TemplateTags 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.