Replaceable themes, templates and portable bundles
For selection and browser editing steps see the theme guide. This reference covers template/binding semantics, bundles, APIs and source layout.
Viking theme schema version 1 uses native component JSON plus CSS. The server renders the same component tree that the visual editor edits; it does not store a second, independently editable HTML template. This format is specific to Viking, not a Hugo or WordPress theme compatibility layer.
Authoring and publication
Themes opens the theme workspace. Viking Light and Viking Ink are bundled, read-only starting points. Edit either and choose Save as copy to create an independent custom theme. Custom themes survive CMS restarts.
Built-in definitions live in cms/internal/theme/themes/<id>/, with JSON
metadata/component trees, separate CSS and optional asset files. Go embeds and
automatically discovers the directories; adding a default theme requires a new
valid folder and a CMS rebuild. See the
built-in theme source guide for the
file layout, manifest and validation commands. The loader assembles the same
theme model used by the visual editor and portable bundles. Shared preview
content is maintained in cms/internal/theme/samples/*.md, with titles, paths
and display order in samples/index.json and a bundled sample.png fixture.
Select a page layout or shared header/footer in Template. GrapesJS provides point and click editing, drag and drop blocks and style controls. Theme binding blocks represent page content, page title, site title, site description, navigation, copyright, shared header and shared footer. A binding's displayed placeholder is editor chrome; the renderer supplies actual content. Each page layout must contain exactly one page-content binding. Shared parts cannot contain page content or form recursive references. Add layout starts from the default layout. Layout name changes its display label in the theme and page layout menus. The layout map key remains its stable ID, so names can be changed while the layout is in use. The shared stylesheet controls typography, colours and other global styles. Template-local styles are stored separately, including selectors for page content that is represented by a placeholder in the editor. Saving a template keeps those selectors and excludes the editor's own CSS reset.
The Copyright block is in Theme bindings in the template editor and in
Site in the visual page editor. Site title is available alongside it.
Site and theme binding blocks can be clicked to insert beside/inside the selected
element (or at page root), as well as dragged into position.
Default theme footers and new Footer blocks contain [site-title] [copyright] as
inline bindings, which render as My Website © YEAR. All rights reserved. The
year is the current UTC year at preview/publication time; published releases
retain their rendered year until republished. The page editor displays current
values without freezing them into page source. Site title updates refresh those
binding views, and publication resolves them even without an active site theme.
Existing custom theme footers retain their design; drag the new binding into a
saved custom footer to add it. Bracketed examples elsewhere remain ordinary text.
Delete theme removes an unused custom theme after confirmation. It discards that theme's saved templates, bundled assets and unsaved workspace edits. The button is disabled for bundled themes and the active site theme; the API enforces the same protections. Select another site theme before deleting the active one. Cancelled or failed deletion preserves workspace changes. Published releases and snapshots already in progress keep their independent theme data and asset bytes.
Use theme for site saves themeId and defaultLayout in settings. Every
page uses that theme and its selected site layout unless the page has an
explicit layout override. This applies to both Markdown and visual HTML pages.
Theme CSS follows page CSS so global page defaults do not override the selected
theme. Content-specific styles remain available. Select Original page styling
in Themes to remove the site theme.
The page editors display the rendered theme around the editable content. Shared headers, footers, navigation and styles are outside the page source and undo history. Applying a theme or saving an already selected theme refreshes both editors immediately; page layout and title changes refresh the decoration too.
Selecting a theme does not change the public site. Publish snapshots page source, settings, the selected theme and asset bytes together, then atomically activates the rendered release. The source revision includes the selected complete theme. A later theme edit cannot alter a publication already in progress. Incompatible layout selections are rejected before settings are saved; active theme edits must retain layouts currently in use. Rollback continues to reactivate an immutable release with its previous theme assets.
Preview
Preview content switches between a standard sample site and saved pages from the current site. The sample includes headings, long titles, Unicode, paragraphs, lists, task-list checkboxes, quotations, links, tables, code, a generated image and an empty page. It is isolated from project drafts and never imported into them. Site previews use saved content; ordinary page Preview includes the current unsaved page content and title. All previews use the publication renderer and support desktop/tablet/mobile widths. A shared part is previewed within the default page layout.
The editor canvas uses labelled binding placeholders. The rendered preview is the authoritative view of a binding's actual content. Text editing is committed before switching templates, saving or previewing. Theme operations freeze their controls while requests are pending. Save/discard/cancel gates protect theme changes when switching themes, closing, applying, importing or exporting.
Bundle format
Download theme bundle exports the selected saved theme as
<id>-<version>.viking-theme.zip. Import theme bundle installs a new theme;
an existing identifier produces a conflict rather than replacing it. Bundled
themes cannot be overwritten. Theme IDs are local installation identifiers;
creating a copy assigns a new ID.
theme.json manifest, component trees, shared/local CSS, asset MIME types
assets/<filename> optional raster images and WOFF/WOFF2 fonts
preview/welcome.md standard public fixture
preview/typography.md standard public fixture
preview/empty.md standard public fixture
preview/sample.png generated public fixture image
The manifest contains schemaVersion, id, name, semantic version, optional
author, license, description, shared css, a required layouts.default,
optional additional layouts and parts, and optional assets. A template is
{"name":"Article page","components":[...],"css":"..."}. The optional name
is a display label; unnamed layouts show their map key. Settings defaultLayout
and page layout values always contain the stable map key. Names survive
saving, copying and bundle export/import without changing those references.
Component trees use GrapesJS definitions
(type, tagName, content, components, attributes, classes, style).
Bindings use attributes["data-viking-slot"] with a standard binding name or a
shared part name. The renderer handles these structural definitions; arbitrary
GrapesJS plugins, scripts and custom executable component behaviours are not
part of the theme format.
Theme-owned images and fonts are added through the workspace asset input.
Images are inserted into the current template. The theme canvas image picker
also uploads directly into the theme bundle and lists only that theme's images.
Both controls enforce the same filename, duplicate-name and per-asset size
rules. CSS asset references use
viking-asset:<filename>, including within @font-face declarations. Assets
publish under /theme-assets/<sha256>/<filename>; that namespace is reserved.
Previews inline the same bytes so images/fonts work in the sandbox without CMS
cookies. Site-specific /api/assets/ references are rejected: bundle theme
assets instead. External resources are not downloaded into a bundle.
ZIP import reads only supported manifest, asset and fixture entries into memory; it never extracts paths onto disk. It rejects traversal, duplicate entries, symlinks, undeclared assets, unsupported schemas, missing content bindings, recursive parts and invalid asset MIME/signature combinations. Limits are 6 MiB per source document/compressed bundle/expanded ZIP, 2 MiB per binary asset, 128 archive entries, 16 layouts, 16 shared parts and 40 component nesting levels. Published HTML/CSS still pass through the existing sanitizer. SVG and executable assets are not supported. Native fonts and raster image formats are supported.
API and storage
Authenticated endpoints (mutations require the usual CSRF session and token):
| Endpoint | Purpose |
|---|---|
GET /api/themes |
List installed and bundled themes |
GET /api/themes/{id} |
Read full editable theme source |
PUT /api/themes/{id} |
Save a validated custom theme |
DELETE /api/themes/{id} |
Delete an unused custom theme; bundled themes return 403 and the active theme returns 409 |
GET /api/themes/samples |
Read standard fixture metadata/source |
POST /api/themes/preview |
Render an unsaved theme with a sample or saved site page |
GET /api/themes/{id}/bundle |
Download portable ZIP |
POST /api/themes/import |
Import raw ZIP bytes without replacing an existing ID |
Preview accepts {theme, layout, sampleId} or {theme, layout, pageId} and
returns {html, css}. Theme selection uses the existing partial
PUT /api/settings with {themeId, defaultLayout}. Page authoring accepts an
optional layout. An empty themeId selects original page styling.
Custom themes are atomic JSON files at <privateRoot>/templates/<id>.json.
Their asset data is base64 within this private source document; ZIP export
moves binary data to separate asset files. Theme asset GETs on the CMS are
authenticated; public releases contain independent copies. Theme navigation is
generated from each page's visibility, position and parent metadata. Nested menus
and tablet/mobile disclosures are shared by all theme navigation bindings; see
Site navigation for authoring controls and Markdown directives.
Standalone example themes
Field Notes and Cobalt Studio provide alternative
native designs without adding to the embedded built-ins. Each source directory
has a build.sh assembling its component JSON and separate CSS into a portable
ZIP. Both supply default/wide layouts and shared site/navigation/copyright bindings.
Build locally with Python 3 or use the Docker example workflow,
then import/apply/publish separately. CI stores these ZIPs alongside plugin examples
in the GitLab generic registry; that distribution is independent of the disabled
Forge feature below.
Viking Forge placeholder
The workspace shows Publish Theme Bundle to Viking Forge, disabled, with an explanation. There is no click handler, network request or Forge publish API. No registry credentials or Forge account are needed for local theme work.
A Forge design should first explore the author's and site's owner's journeys: creating a bundle, previewing it against known content, browsing a theme detail page, downloading/installing, and understanding updates to a locally customised theme. Useful early screens would be a searchable gallery, a detail page with screenshots and live sample previews, and an author upload/release form.
Before enabling publishing, decide how author identity and namespace ownership, licences, compatible schema versions, immutable versioned downloads, integrity checks, release review, update notifications and local customisations work. Ratings, payments and a broader marketplace can follow evidence from that initial gallery. These are design questions; this implementation makes no assumptions about a Forge service or commercial model.
Optional featured-image slot
data-viking-slot="featured-image" on a container renders the current page's
featured image and alt text. The entire slot is omitted when there is no image.
Use a figure or div, rather than an img, because slots are container bindings.
The portable theme stores no site asset ID; the selected page provides the
library URL at render time. See page metadata.
