N1 Nex1on

Tag-based views: how to redo a page without PHP

Any view of the site (home page, sign-in, registration, search, account, page) can be replaced with a tag file. It is plain HTML markup with {name} tags, conditions … and repeats [each:list]…[/each] (the common tag language, the same as in “News → Post templates”…

Any view of the site (home page, sign-in, registration, search, account, page) can be replaced with a tag file. It is plain HTML markup into which you insert the tags {name}, the conditions [if:name]…[/if] and the repeats [each:list]…[/each] (the common tag language, the same as in “News → Post templates”).

How to turn it on

In your theme, put a file at the path of the view, ending in .tpl.html instead of .php:

What you replaceFile in the theme
main layout (header, regions, content, footer)themes/<name>/views/layouts/main.tpl.html
site pagethemes/<name>/views/pages/show.tpl.html
sign-inthemes/<name>/views/auth/login.tpl.html
registration, password recovery, reset, second factor, email confirmation, rulesauth/register.tpl.html, auth/forgot.tpl.html, auth/reset.tpl.html, auth/two-factor.tpl.html, auth/verify.tpl.html, auth/rules.tpl.html
searchsearch.tpl.html
account, security, sessions, privacyauth/profile.tpl.html, auth/profile-security.tpl.html, auth/profile-sessions.tpl.html, auth/profile-privacy.tpl.html
public profileauth/public-profile.tpl.html

If there is no tag file or it is broken (for example, a block is not closed), the regular PHP view works and the page does not break. A tag file in the theme's common folder takes precedence over a module's view, just like a PHP file.

Main layout: an example in 15 lines

<header class="my-head">
  <a href="{home}">{site_name}</a>
  [each:menu]<a href="{item.url}">{item.title}</a> [/each]
  {languages} {search}
  [if:user.logged]<a href="{user.url}">{user.name}</a>[else]<a href="{user.login_url}">Sign in</a>[/if]
</header>
<main>{content}</main>
<aside>{region:sidebar}</aside>
<footer>© {year} {site_name}</footer>

Put this in themes/<name>/views/layouts/main.tpl.html and the page gets your header with the menu, language switcher, search and sign-in, the content, a sidebar and a footer. <head>, the engine scripts and the “back to top” button stay with the engine; styles and fonts are connected through theme.json and assets — you do not need to write them in the tag file.

Common tags for any page

TagWhat it outputs
{site_name}site name
{locale}page language
{year}current year
{home}home page address
{canonical}the main address of this page
{theme}path to the theme folder
{user.logged}condition: the visitor is signed in ([if:user.logged]…[/if])
{user.name}, {user.url}name of the signed-in user and the address of their profile
{user.login_url}address of the sign-in page
{user.avatar}avatar address; empty if there is no avatar (for markup, [if:user.avatar]…[/if])
{user.role}name of the signed-in user's role
{user.pm_unread}number of unread private messages; empty if the “Messages” module is off
{user.favorites_count}number of bookmarks
{user.pm_url}, {user.favorites_url}addresses of private messages and bookmarks
{user.admin_url}, [if:user.can_admin]…[/if]panel address and the condition “the user is allowed into the panel” (empty if not)
{user.register_url}, {user.forgot_url}addresses of registration and password recovery
{logout_button}ready-made “Sign out” button with a protection key (sign-out here is a form; a link cannot be used because of the key)
{menu_toggle}the “Menu” button and the section menu (what a button opens on a phone)
{user_menu}member menu: profile, messages, panel, sign-out
{login}ready-made sign-in and registration block
{content}page content in the engine's container (does not overflow the screen width); use it for the account, the forum and forms
{content_raw}content without the wrapper and without the width limit, only for your own full-page layouts
{languages}language switcher
{search}search box
{poll}current poll
{region:header}, {region:before_content}, {region:sidebar}, {region:after_content}, {region:footer}, {region:head_actions}module blocks placed in this spot of the theme
[each:menu]…[/each]site menu items; inside: {item.title}, {item.url}, {item.active}

Tags from view data, conditions, repeats

Next come the data the view received from the engine: {name}, {a.b}, [each:list] with {item}, {item.key}, {index}, the conditions [if:name], [if:name=value], [ifnot:name], and the option limit=N. Secrets (form protection keys, passwords, tokens, sessions) and objects are not exposed through tags.

Core views on tags

ViewWhat is available (besides the common tags)
auth/login{form} is the ready-made sign-in form; the words {t.eyebrow}, {t.title}, {t.lead}, {t.forgot}, {t.no_account}, {t.submit} and others; {forgot_url}, {register_url}; the condition [if:can_register]
auth/register{form}, the words {t.…} (prefix auth.register), {forgot_url}
auth/forgot, auth/reset, auth/two-factor, auth/verify, auth/ruleseach has {form} and the words {t.…} (prefixes auth.forgot, auth.reset, auth.2fa, auth.verify, auth.rules)
search{form} (search box), {query}, [each:results] ({item.title}, {item.url}, {item.excerpt}…), [each:popular], {found.*}, [if:tooShort], {minimum}, the words {t.…} (search.*)
auth/profile, auth/profile-security, auth/profile-sessions, auth/profile-privacy{form} (and {form:2}, {form:3}, the other forms of the view in order), {identity} (account header), {profile_nav} (account menu), the words {t.…} (profile.*)
auth/public-profile{identity} and the profile data
pages/show{title}, {page.title}, {page.excerpt}, {page.body} (page text, engine markup), {cover}, [each:trail], [each:children], [each:siblings]

A form in parts

In views with a form (sign-in, registration, recovery, password reset, second factor, email confirmation, rules, search, account) you can output the whole form with the {form} tag, which is the safe way, or assemble it from parts to place the fields in the order you want:

TagWhat it outputs
{form_open}the opening of the form with its address and hidden fields; the protection key is there
{field:name}the whole field: label, hint, error, as the engine builds it; the name is the name of the input
{field:captcha}the “not a robot” check
{submit}submit button
{form_close}the closing of the form
{form_open}
  {field:email}
  {field:password}
  {field:captcha}
  {submit}
{form_close}

A field that is not in your template does not appear in the form: the arrangement is the theme's choice. Do not leave out the required fields or the “not a robot” check, or the form will not pass validation.

The word {t.tail} gives an interface label “<view prefix>.<tail>” in the visitor's language, so the texts in the template are not duplicated for different languages.

Discussion (comments) on tags

The theme's file views/comments/thread.tpl.html replaces the discussion view under a post, a page and other materials. If the file is missing or fails validation, the regular view works.

TagWhat it outputs
[each:comments] … [/each]comments as a flat list in display order: a reply comes right after its comment, and the depth is given by {item.level} (0 is the root)
{item.id} {item.name} {item.avatar} {item.profile} {item.date}number (for the anchor), author name, avatar address, profile address (empty if the profile is closed), date
{item.text}the member's text: engine markup, sanitized (no scripts or foreign tags); empty for a comment hidden by the person through “People I hide”
{item.likes} {item.minus} {item.rating} {item.verified} {item.op}“Helpful” (a number) or the “+ / −” rating (votes for minus votes against; {item.minus} means the rating is negative), review stars, “verified buyer”, “post author”
{item.place} {item.group} {item.online} {item.signature}author card fields, if the owner turned them on (location, group, “online”, signature)
{item.image1} {item.image2} {item.image3}comment images (thumbnails)
{item.actions}the engine's ready-made buttons: “Helpful”, “Reply”, “Quote”, “Edit”, “Delete”, “Hide”, “Report”; the display rules are the same as in the regular view
{compose}the whole comment form (fields, robot trap, protection key) and the reply script
{form_open} {field:body} {field:author_name} {field:author_email} {submit} {form_close}the same form in parts: the markup is yours, the protection key and hidden fields stay with the engine
{follow}the “Follow comments” button (a signed-in user with a confirmed email follows at once; a guest gives an email and confirms it by letter); empty if the owner turned subscription off
{comments_script}the script for “Reply”/“Quote”/stars, separately (it is already inside {compose} and {form_close})
{pager} {report} {notice}pagination, “Report this post”, a message instead of the form (discussion closed, limit reached, sign-in needed)
{comments.total} [if:closed] [if:can_write]number of comments; the discussion is closed; writing is allowed

Example:

<section id="comments"><h2>{comments.word}: {comments.total}</h2>
[each:comments]
<article id="comment-{item.id}" class="c c-l{item.level}"><b>{item.name}</b> <time>{item.date}</time>
<div>{item.text}</div><footer>{item.actions}</footer></article>
[/each]
{pager}
[if:can_write]{compose}[else]{notice}[/if]
</section>

Filtered lists, archive, tags, categories and site numbers (“News” module)

These tags work in any tag file: in the main frame, on a page, in a feed. The filter is written in the list name itself as “key:value” pairs separated by colons; several values are joined with “_”. Protection keys, email addresses and anything private never get into lists; posts are selected by the same query as the feed (permissions, password, language, validity period).

ListWhat it givesFilter in the name
[each:news_list category=films,3 limit=5 sort=views] … [/each] (or the familiar [each:news_list:cat:3_5:limit:5:sort:views])posts
[each:comments_list limit=5 category=3] (or [each:comments_list:limit:5:cat:3])latest comments on posts (for a signed-in user, {item.name} shows their own name)limit, category, not_category, author (numbers or logins), days, post (post numbers)
[each:news_archive:limit:12]archive monthslimit
[each:news_tags:limit:30]tag cloudlimit
[each:news_categories]categories as a tree (parent, then nested)category (which categories; number, address, list), not_category (except), subcat: yes with nested, no only the named, only only the nested, depth: how many levels, limit

In the news_list item: {item.id} {item.title} {item.url} {item.excerpt} {item.date} {item.cover} {item.cover_large} {item.category} {item.category_url} {item.by} (the author, if the site shows authors) {item.views} {item.rating} {item.rating_count} {item.comments} {item.pinned} {item.locked} (a password-protected post: only the title is visible) {item.first} {index}. In comments_list: {item.name} {item.text} (up to 160 characters, no markup) {item.url} {item.date} {item.title} (post title) {item.avatar}. In news_archive: {item.title} {item.url} {item.count}. In news_tags: {item.title} {item.url} {item.count} {item.weight} (1–5, for the font size). In news_categories: {item.title} {item.url} {item.count} {item.active} (the current category) {item.level} (0 is the top level) {item.parent} {item.id} [if:item.has_children].

Site numbers: {stat.news} {stat.news_day} {stat.news_week} {stat.news_month}, and the same for comments and users ({stat.comments_week}, {stat.users_month}).

Example of “Categories with post counts” and “Popular this month”:

<nav><ul>
[each:news_categories]<li class="l{item.level}"><a href="{item.url}">{item.title}</a> <small>{item.count}</small></li>[/each]
</ul></nav>
<h3>Popular</h3>
[each:news_list:days:30:sort:views:limit:5]<p><a href="{item.url}">{item.title}</a> · {item.views}</p>[/each]
<p>Posts: {stat.news}, this week: {stat.news_week}</p>

“Downloads” on tags

A template for a category. News, “Downloads”, the forum (section) and the shop (category) tell the engine the category of the page (a category address in Latin letters, digits and hyphens, up to 40 characters). Then the file <view>.<category-slug>.tpl.html (for example, views/news/show.tech.tpl.html or views/forum/section.api.tpl.html) is taken before the general <view>.tpl.html; for the other categories the general one stays. The module also passes the page category's description (without markup) into the common page data; the core provides the {category.description} tag. Text trimming in post tags: limit=N characters, words=N words, wordsafe=1 to cut at a word boundary.

The theme's files views/downloads/index.tpl.html (catalog and category) and views/downloads/show.tpl.html (release page) replace the module's PHP views. Downloading goes through the engine's regular addresses: the counter, access restrictions and passwords work as before.

ViewTagWhat it outputs
catalog[each:cards]releases: {item.title} {item.url} {item.description} {item.version} {item.date} {item.cover} {item.category} {item.category_url} {item.hits} {item.first}
catalog[each:rubrics]categories: {item.title} {item.url} {item.count} {item.active}
catalog{category.title} {category.url} {category_description} {pager} {t.empty}the page's category (from the frame), its description, pagination, the word “empty”
release{item.title} {item.description} {item.version} {item.hits} {item.changelog} {item.requirements} {date} {cover} {cover_large}the release itself
release{category.title} {category.url} {files.count} [if:allow_download]category, number of files, “there is something to download”
release[each:files]files: {item.name} {item.url} (download address) {item.platform} {item.arch} {item.size} {item.ext} {item.hits} {item.primary} {item.external} (host of an external link)
release[each:sums]checksums: {item.name} {item.checksum}, to verify what was downloaded

Example of a release page:

<h1>{item.title}</h1><p>v{item.version} · {date} · downloaded {item.hits}</p>
[if:allow_download]
[each:files]<p><a href="{item.url}">{item.name}</a> {item.platform} {item.arch} {item.size} ↓{item.hits}</p>[/each]
[/if]
<h2>What's new</h2><p>{item.changelog}</p>
<h2>Requirements</h2><p>{item.requirements}</p>
[each:sums]<code>{item.name}: {item.checksum}</code>[/each]

Banners on tags

In any tag file a banner is placed by number or as a whole place. The display rules are the same as for regular banners: period, schedule, device, pages, roles, countries, “first page only”.

TagWhat it outputs
{banner:5}banner number 5 (the number is in the panel's banner list)
{banner_place:sidebar}all banners of a place as a whole; places: topbar, header, before_content, sidebar, after_content, footer, between_feed (between feed posts), between_comments (between comments)

The places between_feed and between_comments are drawn by the news and comments views themselves: a banner appears after every third feed post and after every fifth comment. In the panel, a banner has roles (guest is a guest), countries (RU, UA means only those; -BY means everywhere except) and “first page of the section only”. A page that has such a banner does not go into the shared cache, because the response depends on the viewer.

Insert catalog: any module element with one tag

Each module declares everything that can be moved to any page and inserted anywhere in the theme's tag file: ready-made blocks, lists for your own markup, numbers, forms and buttons. It is a single catalog, and from it are built the “Output on the site” tab in the module's panel (a table of elements, parameters, an example and the “Copy” button, view files), the template editor's hints and this table.

How to insert:

  • a block, form, button or number: {block:module.name param=value}, for example {block:news.count what=news period=week}; up to four parameters;
  • a list for your own markup: [each:list:module_name:param:value:…] … {item.field} … [/each], for example [each:list:news_latest:cat:3:sort:views:limit:5]; several values are joined with “_”, in the list name “module_name” (a dot in a tag name does not work).

A rule for the module: an element of the kind block, button or form is responsible for escaping its own output (through e() and the engine's views, like any view); a number and text (value) are only escaped text without markup; a list returns rows, and the tags escape them. The catalog validates parameters but does not sanitize foreign markup. Parameters are validated against the element's description: a number must be within bounds, a choice only from the list, identifiers only digits; anything extra is dropped. A failure, a missing permission, or exceeding the time or size limit gives an empty insert, and the page stays intact. The old tags (news_list…, {banner:5}, [each:comments]) keep working.

How a module declares an element (the filter theme.blocks, the entry “module.name” → module, kind (block, list, value, form, button), title, description, params, items, example, works_in, permission, render) is described in core/Themes/BlockCatalog.php. The ready-made layout blocks of modules (what a module puts into the theme's places) get into the catalog by themselves, and their kind in the table is marked “block”. The table below is built by the script php dev/katalog-spravka.php from the catalog and is not written by hand.

ModuleElementKindWhat it outputsInsertParameters
bannersbanners.after_contentblockBanners below the content{block:banners.after_content}—
bannersbanners.bannerblockOne banner{block:banners.banner}id
bannersbanners.before_contentblockBanners above the content{block:banners.before_content}—
bannersbanners.footerblockBanners in the footer{block:banners.footer}—
bannersbanners.headerblockBanners in the header{block:banners.header}—
bannersbanners.placeblockBanners of a place{block:banners.place place=sidebar}place (topbar/header/before_content/sidebar/after_content/footer/between_feed/between_comments)
bannersbanners.sidebarblockBanners in the sidebar{block:banners.sidebar}—
bannersbanners.topbarblockBanners in the strip above the header{block:banners.topbar}—
commentscomments.threadblockDiscussion under a post{block:comments.thread}—
contentcontent.factsblockDetails{block:content.facts}—
contentcontent.vakansii_homeblockVacancies{block:content.vakansii_home}—
downloadsdownloads.homeblockFiles on the home page{block:downloads.home}—
downloadsdownloads.latestblockLatest files{block:downloads.latest}—
downloadsdownloads.releaseslistLatest releases[each:list:downloads_releases:limit:5]{item.title}[/each]limit
eventsevents.homeblockComing up{block:events.home}—
favoritesfavorites.starblockBookmark{block:favorites.star}—
formsforms.formformForm{block:forms.form}id
forumforum.homeblockDiscussions on the home page{block:forum.home}—
forumforum.latestblockLatest discussions{block:forum.latest}—
forumforum.topicslistLatest topics[each:list:forum_topics:limit:5]{item.title}[/each]limit
gallerygallery.homeblockFrom the gallery{block:gallery.home}—
highlighthighlight.scriptblockCode highlighting{block:highlight.script}—
newsnews.archivelistArchive by month[each:list:news_archive:limit:12]{item.title}[/each]limit
newsnews.archive_boxblock“Archive” block{block:news.archive_box}—
newsnews.categorieslistCategories as a tree[each:list:news_categories]{item.title}[/each]—
newsnews.commentslistLatest comments[each:list:news_comments:limit:5]{item.name}[/each]limit, cat
newsnews.countnumber/textNumber{block:news.count what=news period=all}what (news/comments), period (all/day/week/month)
newsnews.feed_boxblock“Feed” block{block:news.feed_box}—
newsnews.latestlistEntries with a filter[each:list:news_latest:limit:5:start:0:sort:new]{item.id}[/each]limit, start, cat, user, days, tag, main, sort (new/old/title/views/rating/rand)
newsnews.latest_boxblock“Latest news” block{block:news.latest_box}—
newsnews.rssbuttonRSS button{block:news.rss}—
newsnews.suggestbutton“Suggest an entry” button{block:news.suggest}—
newsnews.tagslistTag cloud[each:list:news_tags:limit:30]{item.title}[/each]limit
pagespages.listblockPages{block:pages.list}—
pollspolls.currentblockPoll{block:polls.current}—
pollspolls.oneblockOne poll{block:polls.one}id
registryregistry.latestblockLatest engine version{block:registry.latest}—
reviewsreviews.homeblockClient reviews{block:reviews.home}—
shareshare.afterblockShare buttons{block:share.after}—
shopshop.cartbuttonCart{block:shop.cart}—
shopshop.cart_iconblockCart icon{block:shop.cart_icon}—
shopshop.categories_tilesblockCategory tiles{block:shop.categories_tiles}—
shopshop.latestblockNew arrivals{block:shop.latest}—
shopshop.productslistNew products[each:list:shop_products:limit:4]{item.title}[/each]limit
shopshop.showcaseblockShop showcase{block:shop.showcase}—
subscribesubscribe.compactblockSubscribe to the news{block:subscribe.compact}—
subscribesubscribe.formblockNewsletter{block:subscribe.form}—
tagstags.cloudblockTags{block:tags.cloud}—
tagstags.subjectblockTags{block:tags.subject}—

Security

  • Only the engine draws forms: in a tag file the <form> tag is stripped, so the protection key and the fields are always in place. The form is output by {form}.
  • Template markup is sanitized against a whitelist: no script, style, iframe, event handlers or data-* attributes; addresses may only be http(s)://, /path, #.
  • Values are escaped in place (differently in text, in an attribute and in an address).
  • The limits are the same as for the tag language: template size 20000 characters, 400 tags, nesting 6, repeats 200.
  • Emails are not needed in tag files: their texts are edited in “Mail → Site emails”.

Extra fields of news: where they work (after the move to “Extra fields”)

Fields are created in the “Extra fields” section (host “News”, can be limited to categories). An empty field is not shown anywhere: neither the value nor the label.

WhereWhat to writeCondition
Full post{field:name}, {fieldlabel:name}, [if:field:name], {fieldurl:name}the field is visible to this reader
Feed card (index.tpl.html, [each:cards]){item.field:name}, {item.fieldlabel:name}, {item.fieldurl:name}, [if:item.field:name]the “in the list” checkbox; a closed post (password-protected) has no fields
Lists and blocks ([each:news_list …], [each:list:news_latest], {block:news.latest_box})the same tags with item.the same
Filtering posts[each:news_list field:year>2000 field:genre=drama], custom=name (filled in), custom=name is=valuea field with the “filter” checkbox (otherwise the list is empty, not the whole feed)
Value as a link (“Director: Nolan” → all posts with this value)automatic on the post page; in lists: <a href="{item.fieldurl:name}">{item.field:name}</a>; the address looks like /news/category?field=name&value=valuethe “filter” and “value as a link” checkboxes; kinds: string, choice list, number, fraction
Search in the panelthe search box over the post list finds field valuesthe “search” checkbox
RSS“Label: value” is added to the post descriptionthe “output in RSS” checkbox; public fields only
Site-wide replace“Search and replace” sees the values of news fieldsthe permission to replace

An address with field=…&value=… shows a “Label: value” badge above the feed and a “Clear filter” button. Closed fields (staff only, by role) are not exposed through tags or in RSS.

“Suggest an entry” on tags (“News” module)

Theme files: views/news/suggest.tpl.html (the form for a signed-in user) and views/news/suggest-login.tpl.html (for a guest: an explanation and sign-in buttons). If there is no file, the page works as before.

The form is written by the engine (protection key, robot trap, validation): as a whole with {form}, or in parts with {form_open} … {form_close}:

TagWhat it is
{field:title}, {field:excerpt}, {field:category}, {field:tags}, {field:body}the main fields; a field that is not in the template will not be in the form (the title and text are required, or the form will not pass validation)
{field:f_name}your own extra field: only one that the owner marked in “Extra fields” with the checkboxes “in the site form” and “filled in by the visitor”; one bound to a category appears when that category is chosen; images, files and videos are not offered to the visitor
{submit}submit button
{t.title} {t.lead} {t.note} {t.send} {t.mine} {t.too_many}the page's words in the visitor's language
{left}, [if:can_send]how many suggestions are left for the day; whether more can be sent
{mine_url}address of “My suggestions”
[each:categories]categories to choose from: {item.id} {item.title}

The page for a guest: {login_url}, {register_url}, [if:can_register], the words {t.guest_title} {t.guest_text} {t.guest_login} {t.guest_register}.

Updated 16.10.2026

On this page
Share: Telegram WhatsApp
You can save things without signing in: they stay in this browser and you can move them to your account after signing in. Sign in
Discussion

Comments

No discussion yet

No one has commented yet. Be the first.

Report this post