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.
What a theme is
Section titled “What a theme is”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:
documentis 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.tokensare 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 writesvar(--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 avaluegets the bare reference and no fallback. Your own stylesheet declares the custom property. The known groups arecolor,spacing,fontFamily,fontSize,fontWeight,lineHeight,letterSpacing,borderWidth,borderRadius,boxShadow,textShadow,backgroundImageandmaxWidth, and a theme may add a group for any other CSS property.breakpointssets thetabletandmobilemax-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.blocksandshortcodesare 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 thecore/namespace is reserved anddefineThemethrows on a theme block that claims it.redirectsare public-route rules the theme owns, for URL moves that belong to the design rather than to the content model. They lose a tie againstconfig.redirectsand against plugin-registered rules.csslists 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.
Quickstart
Section titled “Quickstart”-
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"),},}); -
Register it. In
plumix.config.ts, importrecipesThemefrom./themeand pass it as thethemeslot. -
Start the dev server.
Terminal window pnpm devSicilian Caponata now renders through the
entrytemplate at/recipes/sicilian-caponata. Every other public URL, the archive at/recipesincluded, falls through tofallback.The
/recipes/*routes exist because a plugin registered therecipeentry type. A theme targets types, it does not declare them, and Entry Types is whererecipecomes from.
What a template receives
Section titled “What a template receives”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 welcome theme
Section titled “The welcome theme”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.
Rendering entry content
Section titled “Rendering entry content”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>.
Related
Section titled “Related”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.
Next steps
Section titled “Next steps”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.