Themes as bootable apps

In Squilla a theme is not a skin. It is a bootable app that ships its layouts, blocks, and a Tengo seed script that spins up a whole content tree on activation.

I switched themes on a fresh Squilla install and watched it spin up a 5-page marketing site in 4 seconds. That is not magic, that is just code. A button click, a few log lines, and suddenly there is a home page, an about page, a contact page with a working form, a blog index, and a footer menu that actually points at things. I did not seed anything. The theme did.

This is what I mean when I say themes in Squilla are bootable apps. In most CMSes a theme is a skin. Stylesheets and templates. You install it, you stare at an empty admin, and then you spend an afternoon recreating the demo content from screenshots. Squilla themes ship the demo content with them, as code, and they run it on activation.

What a theme actually contains

A Squilla theme is a directory with a few opinionated parts:

  • theme.json declares everything the theme provides: layouts, partials, blocks, taxonomies it expects, settings groups it owns, and the assets it ships.
  • layouts/ holds the page-level Go templates. One per slug, like default.html, post.html, landing.html.
  • partials/ holds reusable fragments. Headers, footers, hero strips.
  • blocks/ holds content blocks as view.html plus block.json pairs. Editors compose pages out of these.
  • templates/<slug>.json declares page recipes. An editor can click "create from template" and get the exact same block tree the theme seeded.
  • scripts/theme.tengo is the seed script. This is the bootable part.
  • assets/ holds images, fonts, and anything else the theme wants to serve. Files are addressed by key, not by raw path.

The first six items are familiar to anyone who has built themes before. The seed script is the interesting one.

theme.tengo runs at activation

When an operator activates a theme, the kernel runs scripts/theme.tengo exactly once per activation event. The script gets the full core/* module surface: nodes, menus, settings, taxonomies, terms, media, layouts. It can do anything the admin can do, except it does it programmatically and it does it fast.

The rule I follow is that every seed step must be idempotent. The script always checks before it creates. That way activating, deactivating, and reactivating the theme is safe. The site never ends up with three copies of the home page.

Here is a trimmed-down piece of a real theme.tengo that seeds a home page:

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

existing := nodes.query({slug: "home", status: "any"})
if len(existing.nodes) > 0 {
    log.info("home page already exists, skipping")
} else {
    nodes.create({
        node_type: "page",
        language_code: "en",
        title: "Welcome home",
        slug: "home",
        status: "published",
        layout_slug: "landing",
        blocks_data: [
            {type: "hero", fields: {
                heading: "A CMS that ships with its content",
                image:   "theme-asset:hero-main",
            }},
            {type: "feature-grid", fields: {
                items: [
                    {title: "Bootable themes", body: "Activate and go."},
                    {title: "Tengo hooks",    body: "Glue without a build step."},
                    {title: "Extensions",     body: "Bring your own stack."},
                ],
            }},
        ],
    })
    log.info("seeded home page")
}

Notice the image value. I did not write a path to a PNG. I wrote theme-asset:hero-main. That is a key that the media-provider extension resolves at render time. If the operator is running the bundled local media provider, the file comes from the theme's assets/ directory. If they have swapped in an S3 provider, the same key resolves to a CDN URL. The theme does not care. It just references the asset by name.

Templates are recipes editors can replay

The seed script is a one-shot. It runs at activation. But what if an editor deletes the home page a month later and wants it back? That is where templates/<slug>.json earns its keep. It declares the same block tree the seed script used, in a format the editor UI can read. The editor clicks "new page from template," picks "home," and gets the page back with one click. No support ticket.

I think of seed scripts and templates as two views of the same data. The seed script is the activation-time view. The template is the editor-time view. They should produce the same page.

Why this matters

Themes become reproducible. That sounds boring until you try to demo something. With most CMSes a theme demo is a hosted site, because the local install would not look anything like the marketing screenshots. With Squilla you can ship a theme called "agency landing page" or "docs site" or "trip diary" and it bootstraps the whole content tree on activation. The demo is the install.

It also means I can rebuild a site from scratch by activating its theme on an empty database. No SQL dumps, no content export files, no "first install these 14 plugins." The theme owns its full content shape. If the theme is in git, the site is in git.

And it means I can hand a theme to someone, watch them activate it, and know that what they see is what I built. No demo-content drift. No empty admin. No screenshots to recreate.

Themes that bring their content are themes you can actually demo. Everything else is a stylesheet pretending to be a product.

← all posts