Docs Developers Status ForumSpark Hub
Build · Custom Templates

Custom Templates

Custom Templates lets a board reshape how the stock forum surfaces render: the post author rail, topic rows, forum rows, profile cards, every post on the topic view, and in Advanced mode entire pages like the board index, the topic list and the topic view. Templates are written in the same {{ }} dialect widgets already use, edited in the Admin Control Panel, and they are free on every v2 plan. Customizations are stored separately from the stock designs, so a platform update never overwrites them.

Board admins find this under Admin > Appearance > Custom Templates. Every surface opens prefilled with the stock design as a starting point, every save keeps a restorable version, and a broken template never reaches members: the board falls back to the stock design and shows a notice in the ACP.

Start here: your first customization

You do not need a coding background. A template is just the HTML of one part of your board with placeholders where the real information goes, and the safest way to learn it is to change one small thing.

  1. Open Admin > Appearance > Custom Templates on your board.
  2. Pick a surface. The Post rail is a good first one: it is the member box that sits beside every post.
  3. The editor opens with the current design already filled in. Find the word Posts and change it to Messages.
  4. Press Save, then open any topic on your board. The rail now says Messages.
  5. Changed your mind? Press Revert to stock and the built-in design takes over again. Your saved version stays restorable, so nothing is ever lost.

That is the whole loop: open, tweak, save, look. Everything else on this page is the same idea with more options.

The words on this page

WordWhat it means
SurfaceA part of your board you can reshape: the post rail, a topic row, a whole page.
TemplateThe blueprint for a surface. HTML plus placeholders.
VariableA placeholder like @{{ user.username }}. When the page renders, it becomes the real thing: the member's name, a post count, an avatar.
PartialA smaller template reused inside pages, like the topic row inside the topic list.
StockThe built-in design. It is what renders until you customize, and what everything falls back to.
OutdatedA platform update improved the stock design of something you customized. Yours keeps rendering untouched; the ACP just offers you a look at what changed.

The template language

Templates are HTML plus variables and a small set of blocks. Variables use dot paths and unknown variables render as an empty string, never an error.

<div class="post-rail">
    <img src="{{ user.avatar_url }}" alt="{{ user.username }}">
    <b>{{ user.username }}</b>

    {{#if user.trust_level}}
        <span class="level">{{ user.trust_level }}</span>
    {{else}}
        <span class="level">New member</span>
    {{/if}}

    {{#each user.achievements}}
        <img src="{{ this.icon }}" title="{{ this.name }}">
    {{/each}}

    Posts: {{ user.num_posts | abbreviate }}
</div>
SyntaxWhat it does
{{ user.username }}Prints a variable, HTML-escaped. Dot paths walk nested data.
{{{ reply.content }}}Prints raw HTML. Use only for variables documented as HTML, like formatted post content.
{{#if var}} ... {{else}} ... {{/if}}Conditional. Empty strings, empty lists, zero and false are all falsy.
{{#unless var}} ... {{/unless}}Inverted conditional, with the same {{else}} support.
{{#each items}} ... {{/each}}Loops a list. Inside the block, this is the current item ({{ this.name }}) and {{ @index }} is the position.
{{ var | filter }}Filters transform output: number (1,234), abbreviate (1.2K), limit:40 (truncate), upper, lower, default:'fallback'.
{{> topic_row }}Includes another surface's template, customized or stock, whichever the board has. Inside {{#each}} it renders the current item, so a page template can loop its rows through the row template.

Hard limits keep rendering safe: templates max 64KB, output max 256KB, blocks nest at most 16 levels and each loop renders at most 100 items. A template that exceeds a limit or fails to parse falls back to the stock design.

Surfaces

Generated from the platform registry
SurfaceWhat it replacesAdvanced
post_rail The member column beside every post. The character-sheet classic. No
topic_row Every row on the forum index and topic lists. No
profile The identity card at the top of member profiles. No
forum_row Each forum entry on the board index. Yes
reply Every post on the topic view, replacing the card or rail design. Yes
category Category sections wrapping groups of forums. Yes
board_index The whole category and forum listing on the home page. Yes
topic_list The full list of topics inside a forum, pinned included. Yes
topic_view The whole reply stream on the topic page, composed from the reply template. Yes

Normal surfaces are always editable. Advanced surfaces, including the whole-page templates for the board index and the topic list, sit behind an explicit Advanced mode toggle: deeper customizations are more likely to need a review after a platform update, so they carry a warning and rely on the same Outdated review flow, automatic fallback and restorable versions.

Pages compose from partials through includes. The stock board index template loops categories through {{> category }}, which loops its forums through {{> forum_row }}; the stock topic list includes {{> topic_row }} per topic; the stock topic view includes {{> reply }} per post. Customize just a partial and every page picks it up, including customized pages that kept the include. A page template that drops the include and writes its own row markup takes full control of that page; the partial keeps working everywhere else.

Variables per surface

Generated from the platform registry

Each surface exposes a documented set of variables. Names starting with this. are fields of the current {{#each}} item on that surface, for example each topic inside topics on the topic list.

SurfaceVariables
Post rail user.username user.avatar_url user.group_name user.group_image_url user.custom_title user.rank user.num_posts user.num_topics user.reputation user.joined user.joined_long user.location user.is_online user.last_active user.profile_url user.trust_level user.credits user.is_staff user.custom_fields user.achievements
Topic row topic.title topic.url topic.unread_url topic.is_read topic.is_pinned topic.is_locked topic.is_hot topic.is_solved topic.is_open topic.replies_count topic.views_count topic.views_short topic.created_at topic.created_at_full topic.label topic.labels topic.participants topic.author.username topic.author.avatar_url topic.author.profile_url topic.last_post.author topic.last_post.avatar_url topic.last_post.date topic.last_post.url topic.forum.name topic.forum.url topic.checkbox
Forum row forum.name forum.description forum.url forum.topics_count forum.replies_count forum.viewers_count forum.is_read forum.marker_icon forum.marker_image_url forum.last_post.author forum.last_post.avatar_url forum.last_post.date forum.subforums
Reply reply.number reply.content reply.body reply.actions reply.meta reply.reactions reply.permalink reply.created_at reply.created_at_full reply.is_op reply.is_best reply.attachments user.username user.avatar_url user.group_name user.num_posts user.trust_level user.credits
Category category.name category.description category.forums
Board index board.name board.description board.url stats.topics stats.replies stats.members stats.online stats.latest_member stats.latest_member_url categories this.name this.url this.forums this.topics_count this.replies_count this.last_post.author this.last_post.date this.widgets_after widgets.above_board widgets.sidebar widgets.below_board widgets.footer
Topic list forum.name forum.description forum.url forum.topics_count pinned topics pagination.current_page pagination.last_page pagination.next_url pagination.prev_url this.title this.url this.is_read this.is_solved this.labels this.participants this.replies_count this.views_count this.views_short this.created_at this.author.username this.author.avatar_url this.last_post.author this.last_post.avatar_url this.last_post.date this.last_post.url this.checkbox widgets.above_board widgets.sidebar widgets.below_board widgets.footer
Topic view topic.title topic.url topic.replies_count topic.views_count topic.is_locked topic.is_solved replies pagination.current_page pagination.last_page pagination.next_url pagination.prev_url this.reply.number this.reply.content this.reply.body this.reply.actions this.reply.meta this.reply.reactions this.reply.permalink this.reply.created_at this.reply.is_op this.reply.is_best this.user.username this.user.avatar_url this.user.group_name this.user.num_posts widgets.above_board widgets.sidebar widgets.below_board widgets.footer

Outlet snippets

Besides replacing a surface, boards can inject small snippets at named outlets without touching the stock design, for example a trust badge under every avatar. Snippets are managed on the same ACP page, can be reordered and toggled individually, and share the variables of their surface.

Generated from the platform registry
OutletPosition
post-rail:below-avatar Post Rail, Below Avatar
post-rail:after-group Post Rail, After Group
post-rail:after-meta Post Rail, After Meta
topic-row:after-title Topic Row, After Title
topic-row:after-meta Topic Row, After Meta
topic-row:after-stats Topic Row, After Stats
profile-card:after-name Profile Card, After Name
profile-card:after-stats Profile Card, After Stats
profile-card:footer Profile Card, Footer

Copy-paste recipes

Each recipe is one small change. Open the surface, find the line from the starter design, and swap or add as shown. Save, refresh your board, done.

Show where a member is from

Surface: Post rail. Find this pair in the starter:

<dt>Reputation</dt>
<dd>{{ user.reputation }}</dd>

Add this right below it. The {{#if}} wrapper keeps the row hidden for members who left their location empty:

{{#if user.location}}
<dt>From</dt>
<dd>{{ user.location }}</dd>
{{/if}}

Bigger, bolder topic titles

Surface: Topic row. Find:

<a href="{{ topic.url }}">{{ topic.title }}</a>

Replace with:

<a href="{{ topic.url }}" style="font-size:1.05rem;font-weight:700">{{ topic.title }}</a>

A level badge under every avatar

No template editing at all: this one is an outlet snippet, a small addition that leaves the design untouched. On the Custom Templates page choose Add snippet on the Post Rail outlets, pick the post-rail:below-avatar outlet and paste:

{{#if user.trust_level}}
<div style="text-align:center;font-size:.72rem;font-weight:700">{{ user.trust_level }}</div>
{{/if}}

The safety model

  • Validated twice: the editor validates while you type and again on save. A template with syntax errors cannot be saved.
  • Silent fallback: if a stored template ever fails at render time, members see the stock design and the admin gets a dismissible ACP notice naming the surface and the error.
  • Sanitized on save: <script> and <style> tags are stripped and the HTML is sanitized. JavaScript belongs in script-injection extensions, CSS in Theme Studio.
  • Escaped by default: {{ var }} output is HTML-escaped; raw output requires the explicit triple-brace form.
  • Update-safe: when a platform update changes a stock design you customized, your template keeps rendering untouched. The ACP flags it as Outdated with a line-by-line view of what changed, and you merge what you want, revert to the new stock, or mark it up to date as is.
  • Reversible: every save snapshots a version that can be restored, each surface has its own on/off toggle, and Revert to stock never deletes saved versions.
The same dialect powers widget html_template packages, so everything on this page carries over to the Widgets guide and back.

The official cookbook

Six official add-on packs ship in every board's extension directory (ACP, Extensions, Browse) and install in one click, custom profile fields included. Every snippet inside them is reproduced here to copy, paste and remix in your own outlets.

Character Sheets

Provisions 3 profile fields: Character name, Class, Alignment.

Character sheet · outlet post-rail:after-meta

{{#if user.custom_fields}}<div style="margin-top: 0.55rem; padding-top: 0.45rem; border-top: 1px solid rgba(128,128,128,0.25); font-size: 0.74rem;">{{#each user.custom_fields}}<div style="padding: 0.1rem 0;"><small>{{ this.label }}</small><br><b>{{ this.value }}</b></div>{{/each}}</div>{{/if}}

Gamer Tags

Provisions 4 profile fields: Steam, Discord, PSN, Xbox.

Gamer tags · outlet post-rail:after-meta

{{#if user.custom_fields}}<div style="margin-top: 0.55rem; padding-top: 0.45rem; border-top: 1px solid rgba(128,128,128,0.25); font-size: 0.72rem;">{{#each user.custom_fields}}<div style="padding: 0.08rem 0;"><small>{{ this.label }}:</small> <b>{{ this.value }}</b></div>{{/each}}</div>{{/if}}

Post Rail Flair

Trust level badge · outlet post-rail:after-group

{{#if user.trust_level}}<div style="font-size: 0.7rem; margin-top: 0.2rem;">{{ user.trust_level }}</div>{{/if}}

Credits line · outlet post-rail:after-meta

{{#if user.credits}}<div style="font-size: 0.72rem; margin-top: 0.2rem;"><b>{{ user.credits }}</b> credits</div>{{/if}}

Profile Showcase

Standing line · outlet profile-card:after-stats

<div style="font-size: 0.78rem; margin-top: 0.35rem;">{{#if user.trust_level}}{{ user.trust_level }} &middot; {{/if}}{{#if user.is_online}}Online now{{else}}Last seen {{ user.last_active }}{{/if}}</div>

Member since footer · outlet profile-card:footer

<div style="font-size: 0.74rem; margin-top: 0.4rem;">Member since {{ user.joined_long }}{{#if user.location}} &middot; {{ user.location }}{{/if}}</div>

Thread Meta

Active line · outlet topic-row:after-stats

{{#if topic.last_post.date}}<span style="font-size: 0.72rem;">Active {{ topic.last_post.date }}</span>{{/if}}

Exact start date · outlet topic-row:after-meta

<span style="font-size: 0.7rem;">Started {{ topic.created_at_full }}</span>
Built something worth sharing? Bundle your own snippets and fields with Export as add-on on the Custom Templates page and publish it through the directory.