Theme & Layout Development
Create custom visual themes and page layouts for Contento sites.
How Themes Work
Contento themes are CSS variable collections stored as JSON. When a theme is activated, its variables are injected as CSS custom properties on the :root element, overriding the default design tokens. This means themes control colors, typography, spacing, and more — without touching HTML templates.
The theme system uses a layered approach:
- Base design tokens — Defined in
app.css, provide default values for all CSS variables - Theme overrides — The active theme's
cssVariablesJSON overrides specific tokens - Dark mode — The
.darkclass on<html>swaps to dark variants automatically
Theme Data Model
Each theme is stored in the themes database table with these fields:
| Field | Type | Description |
|---|---|---|
id | UUID | Auto-generated primary key |
name | varchar(400) | Display name (e.g. "Wabi Sabi") |
slug | varchar(200) | URL-safe identifier, unique |
description | text | Theme description |
version | varchar(100) | Semantic version |
author | varchar(200) | Theme author name |
css_variables | jsonb | JSON object of CSS variable overrides |
base_layout_id | UUID (FK) | Optional link to a default layout |
thumbnail_url | varchar(500) | Theme preview image URL |
is_active | boolean | Only one theme can be active at a time |
settings | jsonb | Additional theme configuration |
CSS Variables Reference
The design system uses semantic CSS custom properties. Your theme's cssVariables JSON should use these keys:
Core Colors
| Variable | Default (Light) | Description |
|---|---|---|
--color-ink | #1A1A1A | Primary text color |
--color-paper | #FAFAF8 | Page background |
--color-snow | #FFFFFF | Card/surface background |
--color-stone | #6B6B6B | Secondary/muted text |
--color-mist | #E5E5E3 | Borders, dividers |
--color-cloud | #F5F5F3 | Subtle backgrounds, hover states |
Accent Colors
| Variable | Default | Description |
|---|---|---|
--color-accent | #3D5A80 | Primary accent (links, buttons, focus) |
--color-accent-hover | #2C4A6E | Accent hover state |
--color-accent-light | #E8EEF4 | Light accent for backgrounds |
Typography
| Variable | Default | Description |
|---|---|---|
--font-body | Noto Sans JP, sans-serif | Body text font family |
--font-heading | Noto Serif JP, serif | Heading font family |
--font-mono | IBM Plex Mono, monospace | Code font family |
Spacing & Layout
| Variable | Default | Description |
|---|---|---|
--radius | 6px | Default border radius |
--shadow | 0 1px 3px rgba(0,0,0,0.06) | Default box shadow |
--max-width | 72rem | Maximum page content width |
Creating a Theme
Via the Admin UI
- Navigate to Admin → Themes
- Click "Create Theme"
- Fill in the name, slug, description, and author
- Enter your CSS variables as a JSON object
- Save the theme
- Click "Activate" to make it live
Via the API
curl -X POST https://your-site.com/api/v1/themes \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "name": "Midnight", "slug": "midnight", "description": "A dark, elegant theme with amber accents", "version": "1.0.0", "author": "Your Name", "cssVariables": "{\"--color-ink\":\"#E5E5E3\",\"--color-paper\":\"#0F0F0F\",\"--color-snow\":\"#1A1A1A\",\"--color-stone\":\"#8A8A8A\",\"--color-mist\":\"#2A2A2A\",\"--color-cloud\":\"#1F1F1F\",\"--color-accent\":\"#F59E0B\",\"--color-accent-hover\":\"#D97706\",\"--color-accent-light\":\"#292211\",\"--font-body\":\"Inter, sans-serif\",\"--font-heading\":\"Playfair Display, serif\"}" }' # Activate the theme curl -X POST https://your-site.com/api/v1/themes/{id}/activate \ -H "Authorization: Bearer <token>"
Via Database Seeding
For themes that ship with the CMS, add them to DatabaseSeeder.cs:
var theme = new Theme { Name = "Midnight", Slug = "midnight", Description = "Dark elegance with amber accents", Version = "1.0.0", Author = "Contento", IsActive = false, CssVariables = JsonSerializer.Serialize(new Dictionary<string, string> { ["--color-ink"] = "#E5E5E3", ["--color-paper"] = "#0F0F0F", ["--color-accent"] = "#F59E0B", ["--font-body"] = "Inter, sans-serif", ["--font-heading"] = "Playfair Display, serif", // ... more variables }) };
Built-in Themes
Contento ships with 8 built-in themes covering a range of styles:
| Theme | Style | Body Font | Accent | Background |
|---|---|---|---|---|
| Wabi Sabi | Japanese minimalism | Noto Sans JP | #3D5A80 | #FAFAF8 |
| Mono | Monochrome developer | Noto Sans JP | #1A1A1A | #FFFFFF |
| Sakura | Soft cherry blossom | Noto Sans JP | #C084A0 | #FFF5F7 |
| Ink | Modern editorial | Playfair Display | #E63946 | #FFFFFF |
| Grove | Classic literary | Merriweather | #2D6A4F | #FBF8F1 |
| Neon | Playful creative | DM Sans | #7C3AED | #FAF5FF |
| Folio | Minimal reading | Crimson Pro | #0E7490 | #FEFDFB |
| Dusk | Dark elegance | Lora | #F59E0B | #1A1A2E |
Dark Mode
Contento supports automatic dark mode through the CSS .dark class. When dark mode is toggled (via the sun/moon button in the header), the <html> element gets a .dark class and the app.css swaps to dark variants:
/* Light mode (default) */ :root { --color-ink: #1A1A1A; --color-paper: #FAFAF8; --color-snow: #FFFFFF; } /* Dark mode override */ .dark { --color-ink: #E5E5E3; --color-paper: #121212; --color-snow: #1E1E1E; --color-stone: #9A9A9A; --color-mist: #2E2E2E; --color-cloud: #1A1A1A; }
There is also a @media (prefers-color-scheme: dark) fallback that activates when the user's OS is in dark mode and no explicit class has been set:
@media (prefers-color-scheme: dark) { html:not(.light) { --color-ink: #E5E5E3; --color-paper: #121212; /* ... same as .dark */ } }
Layouts
Layouts define the structural arrangement of content regions on a page. Each layout is stored in the layouts table:
| Field | Type | Description |
|---|---|---|
id | UUID | Auto-generated primary key |
site_id | UUID (FK) | The site this layout belongs to |
name | varchar(400) | Display name |
slug | varchar(200) | URL-safe identifier, unique per site |
description | text | Layout description |
structure | jsonb | JSON defining regions and grid layout |
head_content | text | Custom HTML for <head> |
custom_css | text | Custom CSS for this layout |
custom_js | text | Custom JavaScript for this layout |
is_default | boolean | Whether this is the default layout |
The Structure JSON
The structure field defines how content regions are arranged. Here are the built-in layout structures:
Standard (Default)
{
"regions": ["header", "main", "sidebar", "footer"],
"grid": "8fr 4fr",
"maxWidth": "1200px"
}
Full Width
{
"regions": ["header", "body", "footer"],
"grid": "1fr",
"maxWidth": "860px"
}
Magazine
{
"regions": ["hero", "grid", "sidebar", "footer"],
"grid": "8fr 4fr",
"heroColumns": "12",
"maxWidth": "1400px"
}
Minimal
{
"regions": ["body"],
"grid": "1fr",
"maxWidth": "680px",
"centered": true
}
Creating Layouts
Via the Admin UI
- Navigate to Admin → Layouts
- Click "Create Layout"
- Enter the name, slug, and description
- Define the structure JSON
- Optionally add custom CSS, JS, or head content
- Save the layout
Via the API
curl -X POST https://your-site.com/api/v1/layouts \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "name": "Landing Page", "slug": "landing-page", "description": "Wide layout with hero and feature grid sections", "isDefault": false, "structure": "{\"regions\":[\"hero\",\"features\",\"cta\",\"footer\"],\"grid\":\"1fr\",\"maxWidth\":\"1400px\"}", "customCss": ".hero { min-height: 60vh; }", "customJs": null, "headContent": null }' # Set as default layout curl -X POST https://your-site.com/api/v1/layouts/{id}/set-default \ -H "Authorization: Bearer <token>"
Themes & Layouts Together
Themes and layouts are independent but complementary:
- Themes control how things look (colors, fonts, shadows)
- Layouts control where things go (regions, grid, max width)
A theme can optionally specify a base_layout_id to suggest a default layout pairing. However, users can always override the layout for individual posts.
Headless Mode
When using Contento in headless mode, the Content Delivery API returns both theme and layout data. Your frontend can use this to apply the correct styling:
const { data } = await fetch('/api/v1/content/site').then(r => r.json()); // Apply theme CSS variables const vars = JSON.parse(data.theme.cssVariables); for (const [key, value] of Object.entries(vars)) { document.documentElement.style.setProperty(key, value); } // Use layout structure for page rendering const layout = JSON.parse(data.defaultLayout.structure); console.log(layout.regions); // ["header", "main", "sidebar", "footer"] console.log(layout.maxWidth); // "1200px"
API Reference
Theme Endpoints
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET | /api/v1/content/themes | No | List all themes (public) |
GET | /api/v1/themes | Yes | List all themes (management) |
GET | /api/v1/themes/{id} | Yes | Get theme by ID |
POST | /api/v1/themes | Yes | Create a new theme |
PUT | /api/v1/themes/{id} | Yes | Update a theme |
DELETE | /api/v1/themes/{id} | Yes | Delete a theme |
POST | /api/v1/themes/{id}/activate | Yes | Activate a theme |
Layout Endpoints
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET | /api/v1/content/layouts | No | List all layouts (public) |
GET | /api/v1/layouts | Yes | List all layouts (management) |
GET | /api/v1/layouts/{id} | Yes | Get layout with components |
POST | /api/v1/layouts | Yes | Create a new layout |
PUT | /api/v1/layouts/{id} | Yes | Update a layout |
DELETE | /api/v1/layouts/{id} | Yes | Delete a layout (not the default) |
POST | /api/v1/layouts/{id}/set-default | Yes | Set as default layout |
Tips & Best Practices
- Test on both light and dark mode — Ensure your accent colors have sufficient contrast (4.5:1 minimum for WCAG AA) against both your theme's background and the dark mode background
- Use web-safe font stacks — Always include fallback fonts:
"Playfair Display, Georgia, serif" - Keep CSS variables minimal — Only override the variables you want to change. Unspecified variables inherit from the base design system
- Use the admin preview — The Themes admin page shows a live color swatch grid so you can preview your theme before activating
- Version your themes — Use semantic versioning so administrators know when a theme has been updated
- Test with real content — Create a few posts with images, code blocks, and headings to verify your theme looks good across all content types
- Consider loading performance — If using Google Fonts, keep the total number of font weights low (2-3 per family) to minimize page load time