A design is the identity-free look of a site — its brand colors, fonts, and design tokens — packaged as a small portable .design.json file. Any WebProCMS install can export its current design, download the design of any theme variant, and import a design file to restyle itself in one step. Pages, header, footer, navigation, content, and business identity (logo, favicon, white-label) are never touched.
Where it lives
Dashboard → Design → Theme, in the Variants section:
- Export current design — downloads the live design as
{site-name}.design.json. - Import design — upload a
.design.json, preview its name/colors/fonts, then apply. - Every variant card shows color swatches (primary / secondary / tone), the heading + body font names, and a download icon that exports that variant's design as
{variant-name}.design.json.
Applying an imported design uses the same pipeline as switching a theme variant: settings are written, the cached inline brand CSS is busted, and the Tailwind color allowlist regenerates (triggering a CSS rebuild only when the allowlist actually changed). The change is instant and content-safe.
What travels in a design
Exactly the setting keys in BrandingStyleService::DESIGN_KEYS:
| Group | Keys |
|---|---|
| Colors | branding.colors, branding.tone_scale, branding.color_group_toggles, branding.color_display_names |
| Typography | branding.typography (fonts + heading/body element defaults), branding.body_font, branding.heading_font, branding.heading_accent_font |
| Design tokens | branding.radius, branding.shadow, branding.section_spacing (+ _banner, _hero), branding.section_side_padding, branding.section_item_gap, branding.container_width |
| Page surface | branding.page_bg_light/dark, branding.page_text_light/dark |
| Appearance | branding.dark_mode (light / dark / system) |
Deliberately excluded (identity, never travels): branding.logo_url, branding.dark_logo_url, branding.favicon_url, branding.white_label, branding.admin_logo_mode, and per-admin editor preferences (branding.smart_picker_prefs). This exclusion is what makes a design safe to drop onto any client site.
The file format
{
"schema_version": 1,
"name": "Ocean Slate",
"exported_from": "webprocms",
"branding": {
"branding.colors": { "primary": "oklch(0.55 0.15 220)", "secondary": "#0d9488", "tone": "oklch(0.4 0.01 260)" },
"branding.body_font": "inter",
"branding.heading_font": "lora",
"branding.radius": "large"
}
}
Unset keys are simply omitted — on import they fall through to the receiving install's current values. The DB stays the source of truth; the file is an export/import envelope, not a storage backend.
Security model — the whitelist is the boundary
Two independent layers keep a design file from writing anything outside the design:
- Import validation (
BrandDesign::validate()) rejects loudly, with a user-readable message, on: an unknownschema_version, any key outsideDESIGN_KEYS(including identity keys), unknown font slugs (checked againstBrandingStyleService::FONTS), malformed colors, and out-of-range token values. Color function notation is restricted to a safe character set so a crafted value can never smuggle CSS (;,{,}) into the emitted inline style block. Nothing is written until the admin confirms the preview. - Apply whitelist (
ThemeInstaller::applyDesignBranding()) silently ignores any key not inDESIGN_KEYS. Every branding write — variant switches, theme-switch design restores, and design imports — flows through this one method.
Relationship to theme variants
A design maps 1:1 to a theme variant's branding block. Variant snapshots (themes:capture, "Save current design as variant", and the theme switcher's A→B→A design restore) capture all DESIGN_KEYS — colors, fonts, and tokens — via ThemeCapturer::captureVariantData(). Variants additionally carry header/footer + shared-row role slugs; a design file carries branding only, which is why it is safe cross-site while a variant is theme-scoped.
Planned (not yet built)
- Remote design gallery — a browse/download feed served by the webprocms.com mothership over the existing
CMS_LICENSE_KEYchannel, alongside the update feed. - Saved named designs — persisting imported designs as named variants in a gitignored custom tree (
resources/themes-custom/). MVP import applies directly to the live settings instead.