← writing

Themes as bootable apps

In most CMSes a theme is a skin. In Squilla a theme is a bootable app that ships with its own pages, menus, settings, and demo content, and stands itself up on activation. Here is how that actually works, and why it changes what a theme even means.

I switched themes on a fresh Squilla install last week and watched it spin up a 5-page marketing site in 4 seconds. Hero, features, pricing, about, contact. Menu wired. Footer populated. Settings filled in. Demo blog posts. All of it, just sitting there, ready to edit.

That is not what themes usually do. In most CMSes a theme is a skin. You drop it in, you get new stylesheets and templates, and then you stare at an empty homepage wondering where the content goes. The theme demo you saw on the marketplace was a screenshot. The thing you installed was a husk.

Squilla treats themes differently. A theme is a bootable app. It ships layouts and partials and blocks like you would expect, but it also ships a small Tengo script called scripts/theme.tengo that runs on activation and seeds everything the theme needs to actually demo itself. Pages, menus, taxonomies, settings, sample posts. The screenshot you saw on the marketplace is the screenshot you get on first boot.

What lives inside a theme

The anatomy is roughly this:

  • theme.json declares the theme, its layouts, partials, blocks, asset map, and which seed script to run on activation.
  • layouts/ holds full-page templates. One per route family. The post layout renders blog posts. The landing layout renders marketing pages.
  • partials/ holds reusable fragments. Header, footer, nav, hero, anything you want to compose into layouts.
  • blocks/ holds content blocks. Each block is a pair: block.json declares its fields, view.html renders it. Editors pick blocks from a library when they edit a page.
  • assets/ holds images, fonts, and CSS. Anything referenced in templates with theme-asset:<key> resolves to a URL the runtime serves.
  • scripts/theme.tengo is the seed script. This is the part that makes the theme bootable.
  • templates/<slug>.json declares page recipes. These are reusable page templates editors can re-create with one click from the admin UI.

The whole thing lives in a single folder under themes/. Drop it in, activate it, done.

The seed script is the magic bit

When you activate a theme, Squilla runs scripts/theme.tengo exactly the way you would expect any boot script to run. It has access to the full Tengo CoreAPI, so it can create nodes, register menus, set settings, upload images, define taxonomies, anything. The contract is that the script must be idempotent. Run it twice and the second run should be a no-op. That is what lets you re-activate a theme safely without duplicating content.

Here is a tiny slice of what one looks like:

nodes := import("core/nodes")
settings := import("core/settings")

existing := nodes.query({slug: "home", language_code: "en"})
if len(existing.nodes) == 0 {
  nodes.create({
    node_type: "page",
    language_code: "en",
    status: "published",
    slug: "home",
    title: "Welcome",
    layout_slug: "landing",
    blocks_data: [
      {type: "hero", fields: {
        heading: "Build with Squilla",
        subheading: "A CMS that boots itself.",
        image: "theme-asset:hero-bg"
      }},
      {type: "features", fields: {items: [/* ... */]}}
    ]
  })
}

settings.set("general.site_name", "Acme")
settings.set("general.tagline", "We make the thing.")

That is the entire pattern. Query for the thing, create it if missing, set settings. Repeat for every page, every menu item, every taxonomy term. A real seed script for a marketing theme runs maybe 150 lines and produces a complete site.

Why theme-asset:<key> matters

The seed script can reference images by key, not by URL. When the script writes image: "theme-asset:hero-bg" into a block, Squilla resolves that key against the theme manifest at render time. The theme controls where the bytes actually live. You can ship them inside assets/ for a self-contained theme, or point the key at an external URL if you want to keep the bundle small.

The reason this matters is portability. When you export a theme to share or sell, you do not have to chase down hardcoded URLs across thirty pages. Every image reference is symbolic. Re-activating the theme on a different domain just works.

Page recipes are the other half

The seed script handles initial activation. Page recipes handle everything after. A recipe lives in templates/<slug>.json and looks a lot like a blocks_data payload with placeholders. Editors see it in the admin UI as a button that says something like "New pricing page" or "New case study". Click it, the recipe instantiates, the editor fills in the real copy.

This is what closes the loop. The seed boots the demo. The recipes let editors keep extending the site without ever touching the theme code. The theme stays the source of truth for layout and structure. The content stays in the database where it belongs.

What this changes

The big shift is that you can actually demo a Squilla theme. Hand someone a zip, they activate it, they see the marketing site you designed. No "now import this XML file and configure these widgets". No screenshots that lie. The theme brings its content because the content is part of the theme.

It also changes how I think about building themes. I am not writing CSS in a vacuum hoping someone fills it with the right content later. I am writing a small app that ships with its own first impression. The pages are part of the design. The menu structure is part of the design. The settings are part of the design.

Themes that bring their content are themes you can actually demo. Everything else is a screenshot. 🚀

Want more like this?

Occasional, opinionated, no listicles.
all writing →