Built with Chinaski

Build a Theme from Scratch

Chinaski themes are a directory of Template Toolkit files: a couple of HTML shells plus a head partial, and every block type renders via its own template. This guide walks through making your own end to end: scaffolding, writing templates, wiring blocks and menus, and testing.

Starting: three ways

  • Clone a registered starter theme on the Themes screen; the new slug gets a full copy to edit.
  • Generate with Claude: the Themes panel takes a name, an optional Design Brief (choose a previously uploaded reference design, or upload one inline) and optionally a one-paragraph Design Intent. Create Theme Scaffold clones the starter files, then shows a copy-ready prompt naming your theme, brief, and intent; hand it to your MCP-connected assistant, which will read the built-in theme guide and write the files for you.
  • Manually: copy builder/site_templates/ into builder/themes/<your-slug>/ and edit the copies to the same contract below.

Either route creates the slug and registers it in the themes table; the Themes screen shows which theme is currently active and only non-active themes can be deleted.

The files a theme carries

The default theme’s file map is in the Letterbox page: every template there (_head.tt, _header.tt, _footer.tt, page.tt, post.tt, blog_index.tt, plus 404.tt, documents.tt, search.tt, offline.tt) belongs to a theme. Your replacement theme re-creates any subset by name; templates at page.tt/post.tt/blog_index.tt are what the builder looks for.

Declare menu slots by writing a marker comment at the top of a template: [%# MENU_SLOT: header %]. The Navigation screen picks up every declared slot from your theme and offers it when you create a nav item.

The renderer contract

builder/build.pl reads the SQLite database and runs every published page and post through Template Toolkit, writing the resulting HTML under output/<lang>/:

  • renders pages, posts, the blog + category indexes, the RSS feed, sitemap, robots.txt and the 404
  • copies public/media/ in per language
  • cleans up output not written this run (deleted, unpublished, out-of-window items)
  • honors the promotion guard, write budget, and the overall build timeout

Template render failures don’t abort a build; the failing template is recorded and the build continues (visible via Build history / dashboard).

Template Toolkit in brief

Use | html for any user-controlled field and | uri for URL components. IF/UNLESS/FOREACH covers loops (loop.index, loop.count, loop.last). Includes share the built-in partials by stash ([% INCLUDE '_head.tt' %]).

Each site template receives documented variables: page.tt gets title, body, slug, language, meta fields, OG fields, featured image, and site-level settings like site_name, url_base; post.tt additionally receives excerpt, author, category, tags, custom fields; blog_index.tt gets the posts listing and pagination object. The built-in theme docs list the full variable tables per template and the details.

Blocks

builder/block_templates/*.tt (one file per block type) are theme-independent markup, so themes style blocks by class name rather than by markup. The default theme’s stylesheet (_head.tt) implements every wrapper/inner class documented in the built-in blocks class reference; your _head.tt must cover the same contract (or blocks render unstyled). Any inline {{block:name}} in a body arrives already rendered in your template’s body variable; coordinate CSS classes rather than rescanning markers.

Testing while an inactive theme is yours

  • Preview: the Themes screen’s preview link renders sample page, post, and blog views from the in-progress theme without touching the live site (with real nav data from the default language).
  • Real build: click Build (or run perl builder/build.pl), check the dashboard and Build history for full build output and failing template names.
  • Safe iterations: run the builder with CHINASKI_OUTPUT=/tmp/preview to write to another directory and not touch your live site.

Security rules to build by

  • escape user-controlled scalars with | html: exceptions: page/post bodies (intentionally raw) and JSON-LD passed to you pre-escaped
  • use | uri on URL components built from user input; never concatenate raw values into href/src
  • block field values come from admin forms, still untrusted, escape them
  • snippet content is verbatim; audit who may edit snippets (that permission is one step away from running arbitrary third-party JavaScript on your public site)
  • minimize inline <script> blocks: the static site’s CSP allows them today, but every additional inline script widens the XSS surface if a body injection ever lands

Shipping it

When the new theme looks right in preview, activate it on Themes; the old theme stays available; deleting a theme is allowed only for non-active ones. Changing the active theme invalidates the incremental build state, so the next build after the switch is a full one, and live output will consistently use the new templates.

Read this site in