Skip to content

Overview

A theme is the presentation layer of a Plumix site. It holds the templates that turn a resolved request into HTML, along with the design values, head tags and stylesheets those templates read. You declare it once with defineTheme and pass it to plumix.config.ts.

A theme is static. defineTheme takes a descriptor, validates it and hands the same object back. There is no setup hook and no registration context, so a theme cannot run code at boot the way a plugin does. Everything it contributes is spelled out in the object literal.

A site has one theme, and it arrives through the theme slot in plumix.config.ts. No plugin can register one.

templates is the only required slot. It holds an array of rules built by the template builders, or a bare component as shorthand for a theme with nothing but a catch-all. The rest are optional:

  • document is the <head> descriptor: title, titleTemplate, meta, link, script, plus attributes for <html> and <body>. A template can merge its own fragment over it per render.
  • tokens are named design values grouped by CSS property, and they are a registry rather than a stylesheet. Plumix emits no CSS for them. Picking a token in the editor writes var(--plumix-<group>-<slug>, <value>) into the block’s style, where <group> is the group name in kebab-case and <value> is the token’s registered literal, used as the inline fallback. A token registered without a value gets the bare reference and no fallback. Your own stylesheet declares the custom property. The known groups are color, spacing, fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, borderWidth, borderRadius, boxShadow, textShadow, backgroundImage and maxWidth, and a theme may add a group for any other CSS property.
  • breakpoints sets the tablet and mobile max-widths in pixels, defaulting to 991 and 640. The server style emitter and the editor canvas read the same two numbers, so what an author previews is what ships.
  • blocks and shortcodes are presentation components the theme owns. They merge into the per-app registries at boot at the highest precedence, so a theme shortcode replaces a plugin or core shortcode of the same name. A theme block only ever settles against a plugin block, because the core/ namespace is reserved and defineTheme throws on a theme block that claims it.
  • redirects are public-route rules the theme owns, for URL moves that belong to the design rather than to the content model. They lose a tie against config.redirects and against plugin-registered rules.
  • css lists stylesheet paths. The strings never enter the config module graph. The Vite plugin generates a client entry that imports each one, so Vite emits hashed bundles.
  • Template dep kinds sit at the top level too, one array of slugs per kind. A theme declaring settings: ["site"] has that data loaded and passed into every template’s render.

Slugs are yours to choose. One convention is worth knowing: @plumix/plugin-og paints its bundled social card from color.background, color.foreground and color.muted-foreground, so a theme spelling those three that way gets a social card in its own palette the moment the plugin is installed. It is all three or none — name two and the card keeps its own — and the default card’s palette covers the option that points it at whatever your theme calls them instead.

  1. Write the theme. Create theme/index.tsx:

    import type { EntryData } from "plumix";
    import { entry, fallback } from "plumix";
    import { BlockRenderer } from "plumix/blocks/renderer";
    import { defineTemplate, defineTheme } from "plumix/theme";
    const listing = defineTemplate({
    render: () => (
    <main>
    <h1>Recipes</h1>
    </main>
    ),
    });
    const recipe = defineTemplate<EntryData>({
    render: ({ data }) => (
    <article>
    <h1>{data.entry.title}</h1>
    {data.entry.contentBlocks ? (
    <BlockRenderer content={data.entry.contentBlocks} />
    ) : null}
    </article>
    ),
    });
    export const recipesTheme = defineTheme({
    templates: [fallback(listing), entry(recipe)],
    document: {
    titleTemplate: (title) => (title ? `${title} | Recipes` : "Recipes"),
    },
    });
  2. Register it. In plumix.config.ts, import recipesTheme from ./theme and pass it as the theme slot.

  3. Start the dev server.

    Terminal window
    pnpm dev

    Sicilian Caponata now renders through the entry template at /recipes/sicilian-caponata. Every other public URL, the archive at /recipes included, falls through to fallback.

    The /recipes/* routes exist because a plugin registered the recipe entry type. A theme targets types, it does not declare them, and Entry Types is where recipe comes from.

defineTemplate wraps a render function and brands the result, so the framework can tell a real template from an object that happens to have a render key. The render function takes one argument, and most templates destructure it.

data is the discriminated union of everything a request can resolve to, keyed by kind. Which member arrives depends on which rule matched, and the builder you bind with types it. ctx is the request context, carrying db, request, user, locale, basePath, hooks and the rest. Declared template deps arrive as further keys on the same argument, one per kind, each a record keyed by what the template declared — a slug for most kinds, a location id for menus.

A template may also carry document, either a literal manifest or a function called per request with the same argument render sees. Both forms resolve after the walk has picked a winner, and merge over the theme’s manifest for that request alone. That is how one page contributes an og:image without every page paying for it.

The theme slot is optional. A config that omits it gets welcomeTheme, a built-in substituted at config resolution, so nothing downstream can tell a theme-less site from one with a theme of its own.

It is the smallest theme there is: one fallback rule, one defineTemplate, no tokens and no stylesheet. The rendered screen inlines its CSS rather than linking one, so it fetches nothing, and it reads ctx.locale.code to pick its strings. Catalogs ship for English, German, Ukrainian, Arabic and Simplified Chinese, but only the English one carries strings today. The other four are empty .po files, and lingui compile backfills an empty entry with its English source, so every locale renders English until a translator fills one in. It carries a link to /_plumix/admin, built through ctx.basePath so it stays right on a site served from a subdirectory.

Its document manifest sets robots to noindex. A welcome screen reaching production is a misconfiguration, and search engines should not index one.

An entry’s body is a block tree rather than a string of HTML. Render it with <BlockRenderer content={...} /> from plumix/blocks/renderer. The tree comes off data.entry.contentBlocks, which is the stored content narrowed to the block-tree shape, and null when the stored JSON does not match that shape. The raw data.entry.content stays loose so a non-block serializer keeps working. Every node in that tree resolves to one of the specs in Core Blocks, unless a plugin or the theme registered its own.

The same subpath carries <Link> and <Image>. <Link> takes a resolved entry or term and uses the url already on it, and applies the site’s base path to a plain href. <Image> runs the site’s image handling. Reach for both instead of a bare <a> or <img>.

Every rule in the templates array comes from a builder, and Templates enumerates all sixteen of them with an example each. Template Hierarchy is how one of those rules gets picked for a given request, and Template Data is the union your render function destructures.

Custom Rule Kinds is for the other direction: a plugin adding a slot of its own beside templates, resolved by the same walk. The entry types and taxonomies your templates target are declared in a plugin, not in the theme, and Content Modelling covers how. Blocks covers the blocks a theme’s pages are built from. The theme slot sits beside every other slot in Configuration.

Read Templates next, then Template Hierarchy, which together cover what you declare and how a request finds it.

Four further pages in this section are not written yet, and what they cover already ships. Document Manifest goes through DocumentManifest and the theme:document boot-time filter that lets a plugin contribute head tags. Tokens and Breakpoints covers the token groups above and the var(--plumix-*) references a theme declares them for. Component Primitives documents Link, Image, PlumixProvider, the hooks that read its value such as useUser, useTokens and useBasePath, and useAuth, which fetches the signed-in visitor on the client. Template Dependencies covers declaring a dep kind on a theme or a template, and the override and extend forms.

When the theme renders, Routing explains which URLs reach it and Deployment takes the result to Cloudflare Workers.