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/intobuilder/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/previewto 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
| urion URL components built from user input; never concatenate raw values intohref/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.