For AI agents
Building lazysite themes and layouts programmatically - the full technical contract.
This is the reference an agent needs to build a lazysite theme or layout over the
WebDAV / control-API path. It assumes you can already write files to the docroot. The
server is authoritative - verify everything against whoami and the live docs;
treat this page as a map, not gospel.
Connect, then verify
Exchange your lzp_ pairing key for an lzs_ token, then use HTTP Basic auth
(username = partner id, password = token) for WebDAV and the control API. Check
your grant before assuming anything:
POST /cgi-bin/lazysite-manager-api.pl?action=whoami
Theming needs manage_themes + manage_layouts; setting config needs manage_config;
the nav needs manage_nav. Capabilities are read from the account per request - a
fresh grant applies with no new token. They are independent of any admin-group status.
The model (D013)
- Layout =
layout.tt+layout.jsonatlazysite/layouts/NAME/. Structural HTML only; brand-neutral. - Theme =
theme.json+assets/main.cssatlazysite/layouts/LAYOUT/themes/THEME/. Declareslayouts[]. - Everything under
lazysite/is internal; you write there withmanage_themes/manage_layouts, not as ordinary content.
theme.json schema
{
"name": "x", // matches the dir; sanitised to [A-Za-z0-9_-]
"version": "1.0.0", // semver
"description": "...",
"author": "...",
"layouts": ["x"], // REQUIRED, non-empty. Theme ignored if active layout not listed
"config": { "colours": { "accent": "#4F46E5" }, "fonts": { "body": "Inter" } }
}
config is group -> key -> value; values must be strings, and the characters
; { } < > are stripped from them. The processor emits each as
--theme-GROUP-KEY at :root via the [% theme_css %] variable; your main.css
consumes them with var(--theme-GROUP-KEY). Refs and config entries must match 1:1.
Assets and the mirror
A theme's assets/ subtree is served at /lazysite-assets/LAYOUT/THEME/ (this is
[% theme_assets %]). Reference fonts/images from main.css with relative paths.
The mirror is populated at install / activation, not by a raw WebDAV PUT - so link
that path in layout.tt and activate once to build it; it persists thereafter.
layout.tt - the TT variables
| Variable | Notes |
|---|---|
content |
Rendered page body HTML |
page_title, page_subtitle |
Front-matter title / subtitle |
page_modified, page_modified_iso |
File mtime |
request_uri |
Current path, e.g. /about |
nav |
Array from nav.conf; items have label, url, children |
site_name, site_url |
From lazysite.conf |
layout_name, theme_name |
Active names (theme_name unset if no compatible theme) |
theme |
Parsed theme.json hash: theme.config.GROUP.KEY |
theme_css |
<style>:root{ --theme-*-*: ...; }</style> (empty if no theme) |
theme_assets |
/lazysite-assets/LAYOUT/THEME (unset if no theme) |
authenticated, auth_user, auth_name, auth_groups |
Identity |
year |
4-digit current year |
Any other front-matter key is also exposed as a TT variable in the layout (e.g. a page
that sets current_page: can be read as [% current_page %]).
Template Toolkit and extended functions
Standard TT works: [% FOREACH x IN list %], [% IF a == b %], [% MACRO m(x) BLOCK %],
and string virtual methods - [% url.split('/').last %], [% s.replace('a','b') %],
[% list.size %]. lazysite adds page-scoped data via front matter:
tt_page_var:
posts: scan:/blog/*.md sort=date desc # array of page objects
ver: url:https://example.com/VERSION # fetched + cached with the page
scan:returns page objects exposingurl,title,subtitle,date,tags(array) - not arbitrary front-matter keys. Loop them and derive a slug with[% t.url.split('/').last %].url:fetches and caches a remote value (good for a version badge).raw: trueoutputs the body without the layout (fragments/partials);api: trueserves it as JSON;query_params:exposes the query string as[% query %].register:adds a page tositemap.xml/llms.txt/ feeds.- Fenced divs become styled boxes:
::: widebox/textbox/examplebox/marginbox.:::formbuilds a form bound to a delivery handler.::: include pathinlines a partial.
Navigation
The nav is not a WebDAV file (lazysite/nav.conf is 403). Edit it through the
control API, gated by manage_nav:
POST .../lazysite-manager-api.pl?action=nav-read
POST .../lazysite-manager-api.pl?action=nav-save { "items": [ ... ] }
An item with no url is a section heading; children make a sub-menu. A layout
renders it with [% FOREACH item IN nav %].
Gotchas (learned the hard way)
These are the failure modes that cost the most time. Internalise them.
- Per-page override is
layout:only - nottheme:. A page previewed via a per-pagelayout:gets notheme_css, so a self-contained look needs its own:rootfallbacks (var(--theme-colours-x, #literal)) in that theme's CSS. - Markdown mangles inline
<style>in content. Any CSS line that starts with#becomes an<h1>. Writediv#id { }so the line starts with a letter. Raw HTML blocks: keep them de-indented and contiguous (a blank line or_/*can inject<p>/<em>and break wrappers). Hide JS-only data blocks by attribute ([data-x]{display:none}), not by a container that markdown might split. - The active layout is write-locked. A PUT to it returns 403. Stage a new layout dir (or deactivate first), then activate.
- Cache. A content write busts that page; a theme/layout activation clears the cache
site-wide.
nav-savealone may not re-render cached pages - touch a page or activate. - Provisioning. WebDAV needs the app
webdav_enabledand an nginx/dav/route (unauthGET /dav/should be 401, not 404).config-read/config-setare limited for token clients. A transient 502 mid-batch can cascade - re-stage with status checks.
Minimal working theme
theme.json -> { "name":"x","version":"1.0.0","description":"x","author":"x",
"layouts":["x"], "config":{ "colours":{"accent":"#4F46E5"} } }
assets/main.css -> :root{ --accent: var(--theme-colours-accent,#4F46E5) }
a{ color: var(--accent) }
layout.tt -> ...[% theme_css %]<link href="/lazysite-assets/x/x/main.css">...
<nav>[% FOREACH i IN nav %]<a href="[% i.url %]">[% i.label %]</a>[% END %]</nav>
<main>[% content %]</main>
Stage the files under lazysite/layouts/x/ (+ themes/x/), activate
(action=layout-activate&path=x&theme=x), confirm the mirror returns 200, and verify
the live page. For more, see the on-site docs and lazysite.io.