Built with Chinaski

Template Reference

A Chinaski theme is a flat directory of Template Toolkit 2 (.tt) templates. The build process renders your templates into static HTML for every page and post, once per configured language. This page covers the theme files, the variables they receive, and the helpers and rules that come with them.

Where themes live

  • The built-in default theme ships with the application and serves as the fallback.
  • Custom themes live in builder/themes/<your-theme-slug>/. When a theme with the active theme’s slug exists there, the builder uses it; otherwise it falls back to the built-in templates.
  • Which theme is active is chosen in Settings.

Required and optional template files

File Purpose Required?
page.tt Default template for every published page Yes
post.tt Template for every published blog post Yes
blog_index.tt Paginated blog listing and category pages Yes
404.tt Per-language 404 page Yes
_head.tt <head> partial: CSS, meta tags, font imports Convention
_header.tt Site header: logo, nav, tagline Convention
_footer.tt Site footer: copyright, secondary nav Convention
_cookie_notice.tt Optional GDPR banner No
_sidebar.tt Optional sidebar partial No
search.tt Optional search results page No
_password_gate.tt Client-side password-gate UI for protected pages Required if page.tt/post.tt support password_hash
<custom>.tt Any page that selects it via the template setting No

Files starting with _ are partials and are never rendered as standalone pages.

Template variables

Templates receive a flat set of variables: there are no page. or site. objects. Use site_name, title, lang, and so on directly.

page.tt and 404.tt

Variable Type Notes
title string Page title
body HTML string Pre-rendered: do not escape with | html
slug string URL slug
lang / language string 2-letter code, e.g. en
meta_title string? Overrides <title>
meta_description string? SEO description
og_title string? OpenGraph title
og_description string? OG description
og_image_url string? OG image
canonical_url string Auto-generated
featured_image_url string? Hero image
featured_image_alt string? Alt text
updated_at string SQLite datetime
site_name string From settings
site_tagline string From settings
footer_copyright string From settings
year integer Build year
url_base string URL prefix: may be empty; prepend to ALL internal links
cookie_notice_active 0/1 Whether to render _cookie_notice.tt
noindex 0/1 Emit <meta name="robots" content="noindex">
nav_items array {label, url, slot, sort_order}: slot is header or footer
hreflang_links array {hreflang, href} for translations
breadcrumbs array {name, url, is_current}
json_ld HTML string Pre-serialised JSON-LD: emit raw
ga4_id string? Google Analytics ID
gtm_id string? GTM container ID
twitter_site string? Twitter handle with @
password_hash string? If set, you MUST wrap the content in _password_gate.tt (see below)

post.tt: everything from page.tt, plus

Variable Type Notes
excerpt string Author-supplied excerpt; may be empty
category string? Single category name
tags string Comma-separated; split with tags.split(',')
author string Display name
is_sticky 0/1 Pinned to top of blog index
published_at string? SQLite datetime; first publish moment
date string YYYY-MM-DD slice of published_at
feed_url string Absolute RSS feed URL
og_type string Always 'article'

blog_index.tt

Variable Type Notes
posts array Sorted post hashes with keys p.title, p.slug, p.date, p.body, p.category
pagination hash? {current_page, total_pages, prev_url, next_url, abs_prev_url, abs_next_url}: undef when only one page
title string Localised “Blog” or category name
category_name string? Set when filtering by category

search.tt

Variable Type Notes
query string Search term
results array {title, url, excerpt}
result_count integer Total matches

Blocks

Authors place blocks by writing {{block:my-cta}} in a page or post body. At build time the builder finds the shortcode, looks up the published block with that name and language, renders it with its template, and substitutes the rendered HTML into the body.

Block templates are shared across themes and live outside your theme directory; your theme is responsible for the CSS that styles their output.

CSS classes your theme must style

Each block type emits a predictable wrapper class and inner classes. Your _head.tt must style these:

Type Wrapper Inner classes
cta .cta-block .cta-inner .cta-heading .cta-subheading .cta-button
image .image-block .image-caption
quote .quote-block .quote-text .quote-attribution
hero .hero-block .hero-media .hero-inner .hero-heading .hero-subheading .hero-button
testimonial .testimonial-block .testimonial-label .testimonial-body .testimonial-avatar .testimonial-text .testimonial-attribution
stat .stat-block .stat-number .stat-label
promo .promo-block .promo-media .promo-content .promo-heading .promo-body .promo-button
embed .embed-block .embed-caption .embed-content
video .video-block .video-heading .video-wrapper .video-caption
form .form-block .form-heading .form-subheading .block-form .block-form__field
newsletter .newsletter-block .newsletter-inner .newsletter-heading .newsletter-subheading .newsletter-form .newsletter-form__row
accordion .accordion-block .accordion-heading .accordion .accordion__item .accordion__summary .accordion__body
table .table-block .table-heading .table-wrapper .block-table
columns .columns-block .columns-heading .columns .columns--2 .columns--3 .columns--4 .column .column__image .column__heading .column__body
gallery .gallery-block .gallery-heading .gallery .gallery__item

Minimum required styling for the utility classes:

  • .video-wrapper: position: relative; padding-bottom: 56.25%; with the iframe position: absolute; inset: 0; width: 100%; height: 100%; (16:9 responsive video)
  • .columns--2 / .columns--3 / .columns--4: display: grid with appropriate grid-template-columns
  • .table-wrapper: overflow-x: auto so tables scroll on mobile
  • .gallery: responsive grid with gap
  • .accordion__item / .accordion__summary: basic details/summary reset

Multi-language pages

  • Every page is rendered once per configured language.
  • Internal links must include the language prefix: use [% url_base %]/[% lang %]/contact/ or similar. The only exception is nav_items[].url, which already contains the full correct URL; use it as-is.
  • The <html> element must carry the language: <html lang="[% lang | html %]">.

Security rules

The following directives are forbidden in theme and block content and are rejected:

  • [% PERL %] / [% RAWPERL %]
  • [% INSERT '/absolute/path' %]
  • [% USE ... %] of modules like File/IO/Socket/Net/LWP/WWW/HTTP/System/Cmd/Process/Pipe/Expect

Password-protected pages

Password protection is a reader-facing gate: the page content is still present in the published HTML, and _password_gate.tt hides it behind a JavaScript check until the visitor enters the right password. page.tt and post.tt must implement it like this:

[% IF password_hash %]
[% WRAPPER '_password_gate.tt' %][% PROCESS page_content %][% END %]
[% ELSE %]
[% PROCESS page_content %]
[% END %]

(Wrap whatever block holds your main content: the block’s exact name doesn’t matter, only that it is defined and processed inside the wrapper.) If your template omits the password_hash branch entirely, an administrator can still set a password on a page using your theme, and the page will render publicly with no warning that the gate is missing.

Cheat sheet

  • [% variable %]: raw output
  • [% variable | html %]: HTML-escaped (use for ALL user-controlled text)
  • [% variable | uri %]: URI-encode (for URL path segments)
  • [% IF / ELSE / END %], [% UNLESS ... %]: conditionals
  • [% FOREACH item IN items %]: loops; loop.index / loop.count available
  • [% INCLUDE '_partial.tt' %]: include a partial, sharing the current variables; add x = 42 to pass or shadow values
  • [% PROCESS '_macros.tt' %]: like INCLUDE but without introducing a new scope
  • [%- ... -%]: trim surrounding whitespace

Key gotchas

  1. Flat stash: no nested objects. site.title, page.title, post.title are all wrong; use site_name and title.
  2. Nav item fields are label, url, slot, not name, href, active.
  3. Dates: never call date formatting on published_at; use the pre-computed date variable, or take the first 10 characters of published_at.
  4. Body is pre-rendered HTML: output [% body %] without escaping.
  5. Partials are prefixed with _.
  6. All internal links need url_base and lang.
Read this site in