Theme development
Build a block-first theme
A Justflows theme is a portable design system: CSS, tokens, block templates, reusable template parts, patterns, and optional demo layouts. Public pages use the core renderer—there are no executable PHP or EJS theme templates.
Theme anatomy
acme-theme/
├── justflows.json
├── justflows-theme.json
├── styles/global.css
├── templates/
│ ├── index.json
│ ├── front-page.json
│ └── single.json
├── parts/
│ ├── header.json
│ └── footer.json
├── patterns/
└── demo/The three files below are one worked example, Studio — a theme with a full color palette, a Customizer-contributed accent picker, per-block CSS-variable controls, and one landing pattern. Copy it whole and delete what you don’t use.
Styles
styles/global.css, components.css, and blocks.css — whichever exist — are concatenated into /theme.css, in that order.Templates & parts
templates/*.json and parts/header.json / footer.json are found by filename, not declared in the manifest — see the hierarchy below.Patterns
patterns/*.json are insertable designs. List them under patterns in justflows-theme.json for a stable id → file mapping, or omit the map and every patterns/*.json file is picked up by its filename.Demo
demo/home.json, blog.json, header.json, and footer.json seed a new site’s front page, blog index, and default chrome. Also found by filename, not declared.justflows-theme.json and justflows.json
{
"id": "acme.studio",
"name": "Studio",
"version": "1.0.0",
"description": "A block-first marketing theme with a landing pattern, a full design-token palette, and a Customizer-driven accent color.",
"author": "Acme",
"license": "GPL-2.0-or-later",
"engines": { "justflows": ">=0.2.0 <0.3.0" },
"cssVariables": {
"--color-primary": "#4f46e5",
"--color-primary-hover": "#4338ca",
"--color-bg": "#ffffff",
"--color-surface": "#f8fafc",
"--color-text": "#0f172a",
"--color-muted": "#64748b",
"--color-border": "#e2e8f0",
"--max-width": "720px",
"--max-width-wide": "1140px"
},
"styles": ["styles/global.css"],
"patterns": {
"landing": "./patterns/landing.json"
},
"supports": { "blockEditor": true, "customColors": true, "customFonts": true, "patterns": true },
"blockControls": {
"core.color-scheme": [
"--jf-color-scheme-active-bg",
"--jf-color-scheme-active-fg",
"--jf-color-scheme-hover-bg",
"--jf-color-scheme-hover-fg",
"--jf-color-scheme-hover-border"
]
},
"customize": {
"brand": {
"label": "Brand",
"controls": {
"--color-accent": { "label": "Accent color", "type": "color", "default": "#f59e0b" }
}
}
}
}{
"schemaVersion": 1,
"type": "theme",
"id": "acme.studio",
"name": "Studio",
"version": "1.0.0",
"publisher": "Acme",
"description": "A block-first marketing theme with a landing pattern, a full design-token palette, and a Customizer-driven accent color.",
"license": "GPL-2.0-or-later",
"engines": { "justflows": ">=0.2.0 <0.3.0" },
"permissions": [],
"registry": {
"commercialMarketplace": false,
"listed": true,
"free": true,
"comingSoon": false,
"category": "Marketing",
"tags": ["landing", "marketing", "dark-mode"],
"screenshots": []
}
}justflows-theme.json, field by field
- id / name / version
- Same dot-namespaced
idand SemVerversionas the archive manifest. - description / author
- Display text in the theme picker and Marketplace. Not read by the renderer.
- license / engines
- A GPL-compatible license (required) and the
justflowshost-version range this build supports. - cssVariables
- The theme’s default design tokens. The Customizer starts every site on these values before an owner overrides any of them.
- styles
- Informational — records which stylesheets this theme ships (used when the admin “Save as new theme” forks it).
/theme.cssitself always concatenates the fixed filenamesstyles/global.css,components.css,blocks.css, whichever are present — this array does not choose them. - patterns
- Maps a pattern id to its file, e.g.
"landing": "./patterns/landing.json". Omit the map entirely and everypatterns/*.jsonfile is registered by its filename instead — both work; the map exists so an id can differ from the filename. - supports
- Declarative capability flags shown in the theme picker today. The host does not yet gate any behavior on them — do not rely on flipping one to turn a feature on or off.
- blockControls
- Promotes chosen CSS custom properties on a block type to first-class inspector fields, keyed by block type —
{ "core.button": ["--my-var", ...] }. Authors then restyle a block instance with no CSS. - customize
- Adds Customizer sections. Each control key must itself be a CSS custom property;
typeis one ofcolor,range,select, orfont— nothing else validates.
Template hierarchy
The renderer builds an ordered list of candidate slugs for the request and uses the first templates/{slug}.json the theme actually ships, always ending in index. There is one chain per kind of request — they do not chain into each other:
Home, set to a static page → front-page → page-{slug} → page → singular → index
Home, set to the post feed → front-page → home → index
A "page"-type content row → page-{slug} → page → singular → index
Any other content row → single-{type}-{slug} → single-{type} → single → singular → index
Nothing matched the URL → 404 → indexEvery template is a block document. Context blocks such as core.post-title and core.post-content resolve against the current request, while the content row’s own blocks fill the page body.
Patterns
{
"schemaVersion": 1,
"id": "landing",
"title": "Landing page",
"description": "Hero, proof, and call to action",
"category": "marketing",
"version": "1.0.0",
"requiresBlockTypes": ["acme.forms.form"],
"blocks": [
{
"id": "hero",
"type": "core.heading",
"version": 1,
"props": { "level": 1, "text": "Ship faster with Studio" }
},
{
"id": "cta-form",
"type": "acme.forms.form",
"version": 1,
"props": { "formId": "contact" }
}
]
}Every non-core block type used in blocks must be listed in requiresBlockTypes — the pattern fails validation otherwise. The builder shows an installation notice instead of importing a broken design when a listed type isn’t installed. Patterns feed the categorized block-pattern library — theme-width previews, editable insertion, locale variants, and validated JSON import/export. Plugins contribute the same shape at runtime with ctx.patterns.register().
Headers
Headers are a library of named designs with one site default, edited in Theme builder → Header and served from /api/headers (the old /api/header-presets is gone). Themes and plugins contribute designs through the header.templates, header.resolve, and header.config hooks — a theme manifest itself declares nothing for headers.
Menus
Admin → Menus is a visual designer with layouts (horizontal, dropdown, multi-level-dropdown, mega, footer, drawer, …), a per-menu mobile breakpoint and collapse pattern, per-item styling, and server-side visibility rules. A theme ships the front-end for it — partials/nav-menu.ejs, /js/site-nav.js, and .jf-nav styles in global.css — and the shared data-jf-devices primitive is emitted into every theme’s /theme.css. Contribute one-click layout presets through the menu.design.presets filter (static MenuDesignSeed data, no build(); ids must be "{themeId}:{slug}"). The host re-parses whatever you return, so a preset can never widen what the designer allows.
Design tokens and cascade
Expose documented custom properties for colors, spacing, type, widths, radii, and shadows. This allows the Customizer and third-party plugins to inherit the active visual language.
- Theme CSS
- Customizer tokens and platform animation CSS
- Active plugin styles
- Site owner’s Additional CSS
Package and test
Validate every JSON document, confirm the fallback templates/index.json exists, test missing optional plugins, preview responsive layouts, and verify both light and dark modes before creating the archive.
COPYFILE_DISABLE=1 tar -czf ../../acme-studio.jfpkg \
justflows.json justflows-theme.json styles templates parts patterns demo