AI briefing - layouts and themes
Guide for AI assistants helping users author or modify a lazysite layout or theme.
Who this is for
This briefs an AI assistant working on the visual layer of a
lazysite site - the layout template (layout.tt) and its
themes. For content, see
AI briefing - authoring. For
configuration, see
AI briefing - configuration.
Terminology (D013)
Layout: the Template Toolkit file (layout.tt) that wraps
every page. Provides <head>, header, navigation, footer.
Installed at lazysite/layouts/NAME/layout.tt with optional
lazysite/layouts/NAME/layout.json metadata.
Theme: colours, fonts, spacing, and assets that sit on top
of one or more layouts. Installed nested at
lazysite/layouts/LAYOUT/themes/THEME/. Declares compatibility
in theme.json's layouts[] array.
Manager UI: has its own internal template at
lazysite/manager/layout.tt. Outside the layout+theme system.
Do not modify unless explicitly asked.
On-disk example:
lazysite/
layouts/
default/
layout.tt
layout.json
themes/
odcc/
theme.json
main.css
assets/
manager/
layout.tt
assets/manager.css
lazysite-assets/
default/
odcc/
main.css
assets/
TT variables in layout.tt
Always available:
content- rendered HTML page bodypage_title,page_subtitle- front-matter valuessite_name- fromlazysite.confnav- array parsed fromnav.confrequest_uri- current URL path
D013 additions:
layout_name- resolved layout name (string)theme_name- resolved theme name when a compatible theme is active (string; unset otherwise)theme- hash, the parsedtheme.json. Access config values as[% theme.config.colours.primary %]etc. Empty hash when no theme is active.theme_assets- URL prefix/lazysite-assets/LAYOUT/THEME(nested for local themes), or/lazysite-assets/CACHE_KEYfor remote layouts (flat), or unset when no themetheme_css- pre-rendered<style>:root { ... }block of CSS custom properties. Empty string when no theme.
Auth variables:
authenticated- truthy if the request has a valid sessionauth_user,auth_name,auth_groups- user identitymanager- "enabled"/"disabled" from confmanager_path- manager UI URL path
theme.json schema (D013)
Required fields:
name- matches directory nameversion- semverdescription- free textauthor- free textlayouts- array of layout names this theme is compatible with. The manager rejects an upload without this. The processor ignores a theme if the active layout isn't in this array.config- object grouping design tokens. Common groups:colours,fonts,spacing,icons. Group names and keys are author-chosen; values must be strings.
Optional:
files- list of files shipped. Used for remote-layout auto- fetch; not consulted for local themes.
Example:
{
"name": "odcc",
"version": "1.0.0",
"description": "OpenDigitalCC brand theme",
"author": "OpenDigitalCC",
"layouts": ["default"],
"config": {
"colours": {
"primary": "#332b82",
"text": "#2a2a2a"
},
"fonts": {
"body": "Open Sans"
}
},
"files": ["theme.json", "main.css"]
}
Auto-generated CSS variables
The processor walks theme.config and emits a <style> block
with CSS custom properties at :root, exposed as
[% theme_css %]:
<style>
:root {
--theme-colours-primary: #332b82;
--theme-colours-text: #2a2a2a;
--theme-fonts-body: Open Sans;
}
</style>
Naming: --theme-GROUP-KEY.
Use in the theme's CSS:
body {
color: var(--theme-colours-text);
font-family: var(--theme-fonts-body);
}
This is the recommended pattern: layout.tt emits theme_css;
the theme's own main.css references the variables. A theme
fork that only tweaks colours edits theme.json and doesn't
need to duplicate CSS structure.
Minimum layout.tt
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[% page_title %][% IF site_name %] - [% site_name %][% END %]</title>
[% theme_css %]
[% IF theme_assets %]
<link rel="stylesheet" href="[% theme_assets %]/main.css">
[% END %]
</head>
<body>
[% IF nav.size %]
<nav>
[% FOREACH item IN nav %]
<a href="[% item.url %]">[% item.label %]</a>
[% END %]
</nav>
[% END %]
<main>
<h1>[% page_title %]</h1>
[% IF page_subtitle %]<p>[% page_subtitle %]</p>[% END %]
[% content %]
</main>
</body>
</html>
Activating layout + theme
A site has ONE active layout + theme, set in lazysite.conf:
layout: default
theme: odcc
Both values are sanitised to [A-Za-z0-9_-] at resolve time. Activate the
theme globally and keep pages layout-agnostic - do not put layout: in page
front matter as the way to apply a design. Every page then inherits the active
layout, so the whole site re-themes in one step. A per-page layout: is only
for previewing a staged candidate (below) or a deliberate one-off page - and
you remove preview overrides once you activate.
Agents set these themselves through the control API (layout-activate /
theme-activate), which also clears the cache - it is a self-serve action with
manage_layouts / manage_themes, not an operator hand-off.
Staging a layout over WebDAV
If you publish over WebDAV you do NOT edit the live look in place - you stage a new layout beside the active one, preview it, and activate it yourself over the control API.
- Capabilities come from your account, not your token. Editing layout
structure (including
layout.tt) needsmanage_layouts; theme files needmanage_themes(separate capabilities). The token does not encode capabilities - they are read from your account on every request - so an operator's grant takes effect immediately and you do NOT need a new token. If a layout write still403s right after a grant, you are almost certainly writing the active layout (next point), which is denied regardless of capability. (Ruled that out and a fresh grant still seems not to apply? Rotating your token is a reliable belt-and-braces.) - Stage a NEW layout dir - never the active one. A
PUTinto the active layout returns403: the live layout is immutable in place, by design (a deliberate guard, not a grant failure). A path under a new layout returns409until you create its collections, then it is writable - soMKCOLlazysite/layouts/<new>and…/themes/<theme>first, thenPUTthe files. - Preview by per-page override. Set
layout: <new>in a single page's front matter to render that page through the staged layout before any global switch - this is the preview mechanism. The theme's SOURCE css is web-served at/lazysite/layouts/<new>/themes/<theme>/main.css, so reference that for preview; the canonical mirror/lazysite-assets/<new>/<theme>/main.cssis404until activation. - Activate it yourself.
POSTaction=layout-activate&path=<new>thenaction=theme-activate&path=<theme>to the control API (needsmanage_layouts/manage_themes); each sets the pointer inlazysite.confAND clears the cache atomically - no operator step. Then remove the per-pagelayout:preview overrides: they are a preview tool, not the deploy mechanism, and left in place they quietly defeat the next site-wide theme switch. Once active, the canonical/lazysite-assets/mirror serves the theme CSS.
Theme assets and the activation mirror
main.css and other theme assets must live under the theme's assets/
directory: lazysite/layouts/<layout>/themes/<theme>/assets/main.css. On
activation the server builds a flattened mirror served at
/lazysite-assets/<layout>/<theme>/main.css, and layout.tt links that mirror.
- A
main.cssat the theme ROOT (not underassets/) is not mirrored, so the page links a404. Put assets underassets/. - Verify the mirror built after activation:
GET/lazysite-assets/<layout>/<theme>/main.cssand expect200. If it404s the mirror did not materialise - recover by writing the mirror files directly over WebDAV (a supported recovery), or re-activate. - Before activation, the theme SOURCE css is web-served at
/lazysite/layouts/<layout>/themes/<theme>/main.css- use that for preview; switch links to the/lazysite-assets/mirror once active.
theme.json must be strict JSON, ASCII, and quote-free in values - a
non-ASCII character (e.g. an em-dash in description) or embedded quotes in a
config value fails validation. The check runs at activation (and is
cached), so after fixing theme.json you must re-activate, not just
re-PUT it; a rejection now names the failing reason.
Author .html files in the content tree (include partials with no matching
.md/.url source) are content, not cache - the activation cache-clear
leaves them alone. Generated cache (<page>.html beside <page>.md) is what
gets cleared.
Theme incompatibility
If theme.json.layouts does NOT contain the active layout:
- The processor logs a WARN:
theme not declared for layout theme_cssis emptytheme_assetsis unsetthemeis an empty hash- The page still renders through layout.tt
What NOT to do
- Do not modify
lazysite/manager/layout.tt- that's the manager UI's internal chrome. - Do not place themes at the pre-D013 path
lazysite/themes/NAME/- the processor doesn't look there any more. - Do not write
view.tt- that file name is gone; it'slayout.ttnow. - Do not omit
layouts[]fromtheme.json- the manager rejects the upload and the processor can't activate it. - Do not emit CSS expressions in
theme.configvalues;;{}<>are stripped to prevent declaration escape.