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).
  • Templatestemplates/*.hbs: layout, page templates, partials, and component partials.
  • CSScss/*.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 to createHandlebarsThemeExportHooks plus 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, footerHtml
  • config – Theme config (e.g. primaryColor, style)
  • themeId – Theme id for scoped classes
  • pageTitle, 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.

  • ManifestthemeComponents: id, name, icon, properties, and previewProp (which prop to show in the canvas) or previewMaxLength for truncation.
  • Editor – The shared API builds preview HTML from that data; no custom preview markup in your theme.
  • Exporttemplates/components/<id>.hbs is rendered with context. Use resolveComponentData in export.js to pull from content collections (e.g. book by id) and pass source into 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.css and css/theme-static.css, and prepends a :root vars block from config. One small varsBlock(config) in JS; the rest is static CSS.
  • ExportgetVarsCss(siteData, config) builds the vars block; getBaseCss() and getStyleCss(siteData) can read from css/theme.css and css/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 CSS
  • getBaseCss() – 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) – Resolve source for 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 settings
  • getCss(config) – Vars + editor preview CSS string
  • admin – title, icon, bodyClass, fields (select, color, checkbox), getStaticThemeCss
  • version, 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.