Theming
Build Handlebars-based themes for Cursy Sites: templates, components, content collections, and theme settings. Same familiar template approach as Publii—with a more powerful component system and a clean separation of code, data, and styling.
Converting an existing HTML/CSS theme? See Converting HTML/CSS themes to Cursy Sites in the Documentation.
Overview
Cursy Sites themes are Handlebars-driven: you author .hbs templates and optional CSS; the app handles layout discovery, export, and—if you use them—components and content collections. Code, data, and content are kept separate so theme authors can focus on structure and style.
- Manifest – Theme identity, page templates, content collections, menu pages, and component definitions (data only).
- Templates –
templates/*.hbs: layout, page templates, partials, and component partials. - CSS –
css/*.css: editor preview, base theme, and style variants. Config-driven values use CSS variables injected by the theme shim. - Theme shim (JS) – Small browser script: register from manifest, optional admin panel, and vars + static CSS for the editor.
- Export (Node) –
export.js: one call tocreateHandlebarsThemeExportHooksplus optional hooks for CSS and data.
Theme structure
my-theme/
├── manifest.json # Identity, pageTemplates, contentCollections, menuPages, themeComponents, styles, settings
├── my-theme.js # Editor shim: registerHandlebarsTheme (loads manifest + CSS, registers descriptor & admin)
├── export.js # Node: createHandlebarsThemeExportHooks + optional getVarsCss, getBaseCss, getStyleCss, etc.
├── css/
│ ├── editor-preview.css # Canvas preview styles (uses vars)
│ ├── theme.css # Base theme for export
│ ├── theme-static.css # Minimal CSS for config.staticTheme (admin save)
│ └── style-minimal.css # Optional style variant
└── templates/
├── layout.hbs # Full HTML shell, {{{content}}}
├── frontpage.hbs # Home page
├── inner.hbs # Inner page
├── partials/
│ ├── header.hbs
│ └── footer.hbs
└── components/ # One .hbs per theme component
├── book-card.hbs
└── book-review.hbs
Manifest (data)
Everything the editor needs to show your theme (templates, content types, components) lives in manifest.json. No component or collection definitions in code—just data.
Core
id,name,version,description,main(theme shim script),type:"theme"
Page templates
pageTemplates: array of { id, label, description?, regions?, kind? }. Drives the Layout dropdown and export. Template files are auto-discovered from templates/*.hbs (except layout).
Content collections
contentCollections: array of { id, label, singularLabel, icon, fields }. Each collection gets a Content section in the app (e.g. Books, Authors). Fields support string, number, richtext, relation, etc.
Menu pages
menuPages: { id, label, href } for theme-specific menu entries (e.g. Books, Blog).
Theme components
themeComponents: array of { id, name, icon, category, properties, previewProp? }. Optional previewMaxLength for truncating the preview value. The editor uses these to build the component palette and canvas previews via the shared API.
Styles & settings
styles: e.g. [{ "id": "default", "label": "Default" }, { "id": "minimal", "label": "Minimal" }]. settings: schema for theme options (primaryColor, etc.) used by the admin panel and export.
Handlebars templates
Layout and page templates
templates/layout.hbs is the HTML shell; {{{content}}} is where the current page template output is injected. Page templates (e.g. frontpage.hbs, inner.hbs) render regions (header, main, sidebar, footer) and receive full context.
Data in templates
site– Full site (name, pages, blogPosts, content, extensions)page– Current page (title, slug, components, templateId, isHomePage)regions– headerHtml, afterHeaderHtml, mainHtml, sidebarHtml, footerHtmlconfig– Theme config (e.g. primaryColor, style)themeId– Theme id for scoped classespageTitle,pageDescription,pageKeywords,styleClass,effectiveTemplate
Use {{variable}} for escaped output, {{{html}}} for raw HTML (e.g. {{{regions.mainHtml}}}).
Built-in helpers
{{#if (eq a b)}}– equality{{#if (not value)}}– falsy check{{formatDate date}}– locale date;format="iso"for YYYY-MM-DD{{escape string}}– safe HTML escape
Component partials
Place a partial per component in templates/components/ (e.g. book-card.hbs). The export pipeline passes source (and other context) so the partial can render from content or props. Optional resolveComponentData in export.js wires content collections to components.
Components
Themes can define draggable components (e.g. book card, author block). Users pick them from the palette and drop them into regions; the editor shows a slim preview; export renders the real markup from Handlebars.
- Manifest –
themeComponents: id, name, icon, properties, andpreviewProp(which prop to show in the canvas) orpreviewMaxLengthfor truncation. - Editor – The shared API builds preview HTML from that data; no custom preview markup in your theme.
- Export –
templates/components/<id>.hbsis rendered with context. UseresolveComponentDatain export.js to pull from content collections (e.g. book by id) and passsourceinto the partial.
CSS separation
Theme CSS lives in files, not in JS. Only design tokens (e.g. primary color) are injected from config.
- Editor – The shim fetches
css/editor-preview.cssandcss/theme-static.css, and prepends a:rootvars block from config. One smallvarsBlock(config)in JS; the rest is static CSS. - Export –
getVarsCss(siteData, config)builds the vars block;getBaseCss()andgetStyleCss(siteData)can read fromcss/theme.cssandcss/style-minimal.css(or equivalent) so complex styles stay in stylesheets.
Export (Node)
In export.js you call createHandlebarsThemeExportHooks(themeId, { extensionDir, manifest, ... }) and pass optional overrides:
getVarsCss(siteData, config)– Design tokens CSSgetBaseCss()– Base theme CSS (or read from file)getStyleCss(siteData)– Variant CSS (e.g. minimal)preparePageData(context, data)– Add page-specific data (e.g. featured books, blog highlights)resolveComponentData(component, ctx, data)– Resolvesourcefor component partials from content
Templates are discovered from templates/; the list can be driven by manifest.pageTemplates. Simple themes need only the one-liner; advanced themes add the hooks above.
// Minimal
registerExportHooks('my-theme', createHandlebarsThemeExportHooks('my-theme', { extensionDir: __dirname }));
// With optional CSS and data hooks
const hooks = createHandlebarsThemeExportHooks('my-theme', {
extensionDir: __dirname,
manifest,
getVarsCss,
getBaseCss,
getStyleCss,
preparePageData,
resolveComponentData
});
registerExportHooks('my-theme', hooks);
Editor shim (browser)
The theme’s main script (e.g. bookstore.js) runs in the editor. It fetches manifest.json and the CSS files, then calls registerHandlebarsTheme(themeId, options) with:
descriptor– Built from manifest (contentCollections, pageTemplates, menuPages, components from themeComponents)defaultConfig– Default theme settingsgetCss(config)– Vars + editor preview CSS stringadmin– title, icon, bodyClass, fields (select, color, checkbox), getStaticThemeCssversion,price– From manifest
No manual registration of components or admin HTML—the shared API builds previews and the admin form from descriptor and fields. Keeps the shim small and consistent across themes.
Simple vs full theme
Simple theme (e.g. theme-handlebars): no content collections or theme components. Manifest has pageTemplates only; the theme script registers the descriptor from manifest; export is a one-liner. Ideal for layout-focused themes.
Full theme (e.g. Bookstore): content collections, theme components, admin panel, and optional export hooks for CSS and component data. Same Handlebars and manifest approach—you add collections and themeComponents in the manifest and, if needed, preparePageData / resolveComponentData in export.js.
Either way, code stays minimal and data stays in the manifest; styling stays in CSS files.
Next steps
Explore the Bookstore and theme-handlebars extensions in the repo for full examples. Converting an HTML/CSS theme? Follow the step-by-step guide in Documentation. Check Documentation for the Extension API and Marketplace for publishing themes.