Configuration

Layouts, navigation, site variables, forms, auth, and extensions.

lazysite can be configured two ways. The manager is a menu-driven web interface - the easiest way to change most settings, with nothing to edit by hand. This page covers the other way: the configuration files themselves, which is what the manager writes to underneath. Systems and AI agents can also configure a site over the control API and the AI connector (MCP).

lazysite.conf

lazysite/lazysite.conf defines site-wide variables available in layout.tt and all page bodies. It is a plain text file with one key-value pair per line.

Minimal example

site_name: My Site
site_url: ${REQUEST_SCHEME}://${SERVER_NAME}

site_url uses Apache CGI environment variables set automatically on every request. Do not hardcode the domain - the same lazysite.conf works on staging and production.

Value types

# Literal string
site_name: My Site

# Environment variable (CGI - allowlisted vars only)
site_url: ${REQUEST_SCHEME}://${SERVER_NAME}

# Remote URL fetch (trimmed, cached with page TTL)
version: url:https://raw.githubusercontent.com/example/repo/main/VERSION

# Directory scan (array of page metadata)
blog_pages: scan:/blog/*.md sort=date desc

Recognised keys

site_name
Display name of the site.
site_url
Full URL of the site. Typically ${REQUEST_SCHEME}://${SERVER_NAME}.
layout
Active layout. The processor resolves it to lazysite/layouts/NAME/layout.tt. May also be a full remote URL.
theme
Active theme. Must declare the active layout in its theme.json's layouts[]. The processor resolves it to lazysite/layouts/LAYOUT/themes/THEME/theme.json.
layouts_repo
GitHub OWNER/REPO source for the manager's release browser. Default unset hides the browser.
nav_file
Navigation file path, docroot-relative. Default: lazysite/nav.conf.
search_default
Site-wide default for the search: front matter key. true (default) or false.
engine_docs_public
true lists the engine's /docs manual in this site's own search, sitemap, feeds and llms.txt, as if its pages were the site's. Default unset: the manual serves at /docs/ on every site and stays out of the site's listings, which are about the site's own content. For a site that is the product's documentation. See The /docs manual.
llms_txt
The AI-readable summary, /llms.txt: on by default. It is generated beside the sitemap and lists the site's public pages - never a protected page, a draft, a page copy or an api:/raw: page - with their titles and descriptions, and the organisation entry below. off turns it off: it is then neither generated nor served (a request for it is a 404). A llms.txt you wrote yourself in the content is the site's own and is served either way. See Structured data for search.
time_zone
The site's time zone, a name from the host's zoneinfo such as Europe/London. Schedules and dated rows (shows_from, shows_until) are read in it, summer time included. Default unset: UTC. A name the host does not know is read as UTC and lazysite check says so (the tzdata package provides the names; the Debian package recommends it).
site_type, site_address, site_phone, site_email, site_hours
The site's own details, for the organisation entry search engines read: the home page's JSON-LD and the summary's About section. site_type is a schema.org type (default Organization; LocalBusiness, Restaurant, Store ...), site_hours names a schedule table (schedule: true) whose opening hours it publishes. The entry is published once one of these is set, with site_name and site_url; a site that sets none publishes nothing new. Each is published as written - set only what the site means to make public.
design_guide
A path on this site, such as /guides/design, to the site's own guide for whoever designs for it. The manual's Layouts and themes page and the layouts briefing then open with a "This site's design guide" line linking it. Default unset: no line. Only a same-site path counts (letters, digits, _, -, . and /, starting with /); a full URL or anything else is ignored. A domain names its own guide with alias.<host>.design_guide: /path (see Domain aliases), which overrides this one for that host; a domain without the line uses this one, except a domain with a content root of its own, which shows no line.
manager
enabled or disabled. Controls the built-in manager at /manager.
manager_path
URL path for the manager. Default: /manager.
manager_groups (retired - SM138)
Retired. Manager access is the ui capability granted through a group on the Groups page; on upgrade any group this key named receives its capabilities explicitly and the line is removed.
update_channel
The minimum release maturity the site accepts on upgrade, on the edge < beta < stable < certified ladder: stable installs supported releases; certified only builds whose compliance records were walked, beta takes beta and stable builds, any other value (the default) accepts everything. Out-of-channel upgrades are skipped and audited. Set with lazysite channel edge|beta|stable|certified --docroot ... (or --domain <site> / --all), or from Manager → Site settings. See Update channel.
update_policy
auto or manual (default). Whether a fleet-wide lazysite upgrade --all run (deb-managed hosts) touches this site at all: manual sites are skipped and upgraded only deliberately. Set with lazysite policy auto|manual --docroot ... (audited as policy-set).
log_level
ERROR, WARN, INFO (default), or DEBUG.
log_format
text (default) or json.
extensions
The extensions this site has switched on, one - extensions/<file>.pl per line - the list the manager's Extensions page writes, and the way to pre-enable one without it. A site from before 0.16.4 kept this list under an older header; its upgrade rewrote it, and lazysite check names a list written back under the old one, which nothing reads.
auth_default
Site-wide default for the auth: front matter key. required, optional, or none (default).
auth_header_user, auth_header_name, auth_header_email, auth_header_groups
Override HTTP header names when using an external auth proxy. Defaults: X-Remote-User, X-Remote-Name, X-Remote-Email, X-Remote-Groups. Every reader honours them - the page gate, the per-file ACLs, the manager and its API - and only under auth_proxy_trusted: true, from a peer listed in auth_proxy_trusted_from; behind the built-in cookie login the identity is the login's, whatever these are set to.
trusted_proxies
The front ends whose X-Forwarded-For the engine believes about a visitor's address. Four states: absent and empty both mean the private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16); none trusts nothing and ignores the header; a list of addresses or CIDR ranges trusts only those. Only none trusts nothing - removing the line restores the default trust. The setting is safe only when the front end appends the client's address to the header (lazysite's nginx templates do since 0.15.2, SM929: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;). A front end that passes the client's own header through - a vhost rendered from an older template - lets any visitor choose their address, and every control keyed on it (sign-in limit, bad-URL blocker, form limit, address rules) takes their word. That includes a site with no trusted_proxies line at all when its front end is in a private range. Probe it: a request with X-Forwarded-For: 192.0.2.55 must not be seen as 192.0.2.55. lazysite check warns whenever the trust set is not empty (the default included), and the Hestia rollout reports an older vhost on any site not set to none as a failure. The fix is to rebuild the vhost; until then set trusted_proxies: none.
manager_addresses, manager_addresses_deny
Names of address profiles (Access, Address profiles) that limit who may reach the manager and its API by the address they come from, for everyone and apart from any group. manager_addresses: office allows only the addresses in the profile office; manager_addresses_deny: blocked refuses those in the profile blocked, and deny wins. A profile has no kind: the key that names it decides whether it allows or denies. A refused visitor is told "This page is not available from your address." The sign-in pages are outside the manager and stay reachable. Comma separated; empty means any address. Set from the Config page by a signed-in session (a value that would shut your own address out is refused). The address is the visitor's, worked out with trusted_proxies: behind a front end that is not listed there every visitor looks like the proxy and the rule means nothing. A group can be limited the same way on the Groups page.
signin_addresses, signin_addresses_deny
Names of address profiles (Access, Address profiles) that limit where a sign-in may be made from: this site's own sign-in and the Odoo extension's sign-in through a page (its password step and its authenticator-code step). signin_addresses: office allows sign-in only from the addresses in the profile office; signin_addresses_deny: blocked refuses those in the profile blocked, and deny wins. The refusal comes before the password is looked at and before the sign-in rate limit counts the attempt, and the visitor is told "Sign-in is not available from your address." Each address is recorded once an hour as address-refused signin. A session already open is untouched, and signing out is never refused. It is a compensating control beside a second factor, not a second factor. Comma separated; empty means any address. Set from the Config page by a signed-in session (a value that would shut your own address out of signing in is refused). The address is the visitor's, worked out with trusted_proxies, as for manager_addresses.
auth_header_groups_separator
What separates the groups in the groups header. Default ,; Authentik sends |. Each part is trimmed and an empty part dropped. One rule for every reader.
auth_group_map
Maps a directory group name to a lazysite group name, for groups that arrive in the groups header from a trusted proxy. One line, comma-separated entries: auth_group_map: Site Editors = editors, Platform Admins = operators. The directory name is matched ignoring case and extra spaces; the target must be a valid group name (letters, digits, hyphen, underscore, at most 64). A name with no entry is normalised: lowercase, any run of other characters becomes one hyphen, hyphens trimmed at the ends, cut to 64 - so Site Editors is site-editors. A name with nothing left is dropped with a warning in the log. A group that arrives this way gives page and file access by name; it grants manager capabilities only when the operator has ticked the directory mark on the group's record on the Groups page. See Behind an identity provider.
webdav_enabled
true or false (default). Master switch for the /dav WebDAV publishing endpoint. While off, /dav returns 404. See WebDAV publishing.
dav_allow_insecure
true or false (default). Permit WebDAV Basic auth without HTTPS (for a TLS-terminating proxy or trusted LAN). Loopback is always allowed; leave off otherwise.
permissions_policy_allow
Optional. The browser features this site's pages may ask for with <feature>: self in their front matter (see front matter), comma separated, e.g. permissions_policy_allow: camera, geolocation. Unset or empty, a page may ask for any of accelerometer, camera, display-capture, geolocation, gyroscope, magnetometer, microphone, midi, usb and xr-spatial-tracking. Set, a page that asks for an unlisted one keeps it denied (=()), and validate warns naming this key. none lets no page ask. payment and browsing-topics stay denied on every site and cannot be listed; the Config page refuses them and lazysite check names any written by hand. It is the site's, for every domain: it cannot be set per domain alias.
alias_hosts
Comma-separated list of extra hostnames that serve this site as domain aliases, e.g. alias_hosts: blog.example.com, brand2.example. See Domain aliases below.
alias.<host>.<key>
Per-alias-host override for a whitelisted presentation key. See Domain aliases below.

All other keys become TT variables available in page content and the view template.

Example

site_name: My Site
site_url: ${REQUEST_SCHEME}://${SERVER_NAME}
theme: default
nav_file: lazysite/nav.conf
search_default: true
log_level: INFO
log_format: text
manager: enabled
manager_path: /manager
extensions:
  - lazysite-auth.pl
  - extensions/form-handler.pl

Allowlisted environment variables

Only these CGI variables may be used with ${VAR} syntax: SERVER_NAME, REQUEST_SCHEME, SERVER_PORT, HTTPS, REQUEST_URI, REDIRECT_URL, DOCUMENT_ROOT, SERVER_ADMIN.

HTTP_HOST is intentionally excluded - it is request-supplied and therefore untrusted. Use SERVER_NAME for host-based URL construction.

Domain aliases

A domain alias is an additional host that serves the same site - same files, users, and extensions - with its own look: site name, theme (or layout), and navigation. Declare the hosts, then override per host:

alias_hosts: brand2.example, blog.example.com
alias.brand2.example.site_name: Brand Two
alias.brand2.example.theme: dark
alias.brand2.example.nav_file: lazysite/brand2-nav.conf

Rules:

Adding one (SM217). On the Domains page, use Add domain and answer the content-folder question with "The same content as <host>", under Share an existing site. That is the whole of it: the new host serves the content the named one already serves, and the Domains list then tags the row alias of <host> so the relationship is visible without opening anything.

Three things follow from what an alias is, and the engine enforces each rather than trusting the form:

Only a domain that has a named content folder can be shared. A host serving the default site has no folder of its own to share; to add a second name for the default site, add a domain and leave the folder empty.

DNS and a TLS certificate are still yours to arrange - an alias is a new host, and this step does not touch either. The page runs its usual reachability check afterwards and reports what it found. - Two hosts serving one set of files is an alias, and domain-alias-add says so in one call: give it host and alias_of, and the new host is registered with the same content_root as the host it names - read from that host, not typed again. Adding the second domain by hand and copying the path also works and is where the mistake is: a typo in the shared path is accepted, and the new host then serves its own empty folder under a name that says it is the same site. The action takes no content_root of its own and refuses one. Presentation overrides still apply per host, so an alias may look different while serving the same content, and domains-list marks each row alias_of the first host carrying its content root - derived from the roots themselves, never stored, so it cannot disagree with what decides what is served. A host with no content_root reads as an alias of (default).

The web server must route the alias hosts to the same docroot (an Apache/nginx server alias). Registering the domain in lazysite is only the lazysite half; DNS, the server alias and TLS are a precondition handled outside lazysite (your control panel / Hestia).

Multilingual language sets

A multilingual site is a set of hosts - one per language - linked by a shared lang_group. Each language is a first-class domain with its own content root, so the languages are authored and served independently under their own hosts.

lang: en
lang_group: providers
content_root: sites/en

alias_hosts: fr.example.com, th.example.com
alias.fr.example.com.lang: fr
alias.fr.example.com.lang_group: providers
alias.fr.example.com.content_root: sites/fr
alias.th.example.com.lang: th
alias.th.example.com.lang_group: providers
alias.th.example.com.content_root: sites/th

Adding a new language is an operator + DNS act - not something an agent can do on its own:

  1. Point DNS (and TLS) for the new host at this server and add the web-server server-alias. This is outside lazysite (control panel / Hestia; a wildcard record + certificate covers every sub-domain at once).
  2. Register the host as a domain with its own content_root (a new folder - the Domains page or lazysite-domains add), and set its lang plus the shared lang_group.
  3. Populate that content root: copy the source language's files to the same paths and translate the values (never the keys, paths, or structure). The lang-status control-API action reports what is missing or stale per language, so a re-run translates exactly the gap.

A content-capable AI agent can do step 3 (the translation) on its own; steps 1-2 need the operator, because DNS and domain registration sit outside the translation surface.

lazysite/nav.conf defines the site navigation. The processor reads it into a nav TT variable available in every page.

Format

One line per item: the label, a |, and the address. A child is indented under its parent. It is the format the Navigation editor and set_nav write:

# lazysite navigation
Home | /
About | /about
Docs | /docs
  Install | /docs/install
  Authoring | /docs/authoring
Resources
  GitHub | https://github.com/example

Rules:

Addresses

A nav address is a path (/about), an http or https address, mailto: or tel: - the rule | url_safe applies to a link in a template. An address whose scheme can run script or is not a web address (javascript:, data:, vbscript: and the rest) is refused by the Navigation editor, set_nav, nav-save, a WebDAV PUT of a nav file and a save of a nav file through the file editor. One already in a nav file renders as # and is logged; lazysite validate nav.conf and lazysite check name it.

nav is an array of hashrefs. Each item has label, url, and children keys. See Layouts for the variables a layout.tt receives.

If nav.conf is missing, nav is an empty array and the template renders without navigation.

Alternate nav file

Override the default path in lazysite.conf:

nav_file: lazysite/docs-nav.conf

The path is relative to the docroot.

Authentication

Authentication is configured through three mechanisms:

See Authentication for full details.

Forms

Forms are configured in files under lazysite/forms/:

FORMNAME.conf
Per-form configuration. Lists the handlers it calls, by ID.
handlers.conf
The handlers - named functions a form or the schedule calls. Each has an id, type (smtp, file, table or connector), name, and its type's settings. Edited through the Handlers page, the control API, MCP or lazysite-handlers.pl, where the handler's destination decides the capability.
schedule.conf
What the timer calls: a handler, an interval, fixed fields.
smtp.conf
SMTP connection settings shared by all SMTP-type handlers.

See Forms and Forms SMTP for full details.

Extensions

Extensions are CGI scripts and tools that register themselves with the manager through a --describe JSON protocol. They extend the core rather than plug into it: the renderer requires nothing from them, and every one ships in the package rather than being installed. Enabled extensions appear on the Extensions page.

Auto-discovery scans cgi-bin/ and tools/ for scripts supporting --describe. Enable or disable from the Extensions page.

To pre-enable without the manager, list scripts in lazysite.conf:

extensions:
  - lazysite-auth.pl
  - extensions/form-handler.pl
  - extensions/audit.pl

URL remapper

For a site that has replaced another on the same hostname while links to the old one are still in the world - notifications, invoices, helpdesk tickets. A rule sends a path prefix, and everything under it, to a destination you name, keeping the rest of the path and the query string:

/web/order?id=42   ->   https://backend.example.com/order?id=42

It answers only when nothing else does: a real page and an alias both win, so a rule can cover an old link and can never shadow content. Prefixes match on a path boundary - /web covers /web and /web/login, never /website-terms. 302 unless a rule asks for 301; a migration's destination can move, and a cached permanent redirect to a host that later moves is a trap.

Switch it on on the Extensions page (it ships off). Set a domain's rules with the remap-save action, which needs Domains & site packages and replaces that one domain's rules:

{"host": "www.example.com",
 "rules": [ {"prefix": "/web", "destination": "https://backend.example.com"},
            {"prefix": "/helpdesk", "destination": "https://backend.example.com/support", "code": 301} ]}

An empty rules list removes the domain's rules. remap-list returns them. A manager confined to one domain may set rules for that domain only, and never for a host that is not a registered domain.

The destination always comes from the rules, never from the request - there is no ?to=, and there will not be one, because that would make the site an open redirect.

When can a rule go? Rules and use, on the extension's own row on the Extensions -> Configure page, shows how often each rule was followed and when it was last used, read from the visitor log. When the last-used date stops moving, the links it covered have stopped arriving. If the visitor log is not recording, the report says so rather than showing zero - "nobody followed it" and "nothing was recorded" are different answers, and only the first means a rule can go.

Logging

Log level and format are set in lazysite.conf:

log_level: INFO    # ERROR, WARN, INFO, DEBUG
log_format: text   # text or json

Both can be overridden at startup with environment variables:

LAZYSITE_LOG_LEVEL=DEBUG perl tools/lazysite-server.pl ...
LAZYSITE_LOG_FORMAT=json perl tools/lazysite-server.pl ...

To also forward log streams to syslog for an external collector (the "Logging & forwarding" extension manages these keys):

forward_audit: off        # audit-trail entries -> syslog, INFO priority
forward_diagnostics:      # blank = WARN and above (the default)
syslog_facility: daemon   # or local0..local7

Forwarding is best-effort and never blocks the site; the files under lazysite/logs/ remain the record.

Diagnostics are forwarded by default, at WARN and above. The application's own log events go to STDERR, and whether STDERR reaches a log you can read is decided by the web server rather than by the site - on some FastCGI arrangements it reaches nothing at all. syslog is the second sink, so a warning the engine raises is not lost because of how the site is fronted.

forward_diagnostics takes a level rather than a yes/no:

Value Forwards
blank or absent WARN and above - the default
off nothing
error / warn / info / debug that level and above
on every level

forward_audit is separate, is a plain on/off, and stays off by default: it copies an access record rather than a diagnostic.

Layouts and themes

Activate a layout and theme by name in lazysite.conf:

layout: default
theme: odcc

The processor resolves the layout to lazysite/layouts/default/layout.tt and the theme to lazysite/layouts/default/themes/odcc/theme.json. Theme assets are web-served from /lazysite-assets/default/odcc/ (nested).

A theme must declare the active layout in its theme.json's layouts[] array; mismatched themes render layout-only with a warning.

See Layouts and themes for installation and authoring.

The /docs manual

The pages under /docs/, this one included, are the engine's. They ship in the engine tree (lazysite/templates/docs/), every upgrade refreshes them, and they serve on every site, including one installed without the starter pages. Clearing out a site's content for launch leaves them in place. That matters because the agent briefings among them are what the partner brief, .well-known/ai-partner and the MCP server's instructions send agents to.

For a /docs/ path the site's own page wins, then the engine's page, then a 404. To override a page, put your own at the same path: docs/<page>.md in the docroot, for example by editing the page in the manager or writing docs/ai-briefing-layouts.md. It serves in place of the engine's. lazysite check then warns that it shadows a newer version of the engine's page, because upgrades no longer refresh what visitors and agents read there. Keep the warning if the override is deliberate. To go back to the engine's page, delete yours.

The manual serves at /docs/, but it is not listed as the site's content: it stays out of the site's search, sitemap, feeds and llms.txt. The manual's own indexes list it. engine_docs_public: true lists it as the site's too, for a site that is the product's documentation. An access rule on /docs/ governs the engine's pages as it would the site's own.

An upgrade from a release that shipped the manual into the docroot removes each docs/ page the site never edited, so the engine's copy serves. It keeps an edited page, which the upgrade's output names as left in place. A docs/ page the site wrote itself is untouched.

Page scan

The scan: prefix in lazysite.conf or tt_page_var scans a directory and returns an array of page metadata as a TT variable.

blog_pages: scan:/blog/*.md

In a page body:

[% FOREACH post IN blog_pages %]
## [% post.title %]
[% post.subtitle %] - [% post.date %]
[% END %]

Pattern rules

Page object fields

Each item in the returned array carries these built-in fields:

Custom keys pass through. Any other (non-control) front-matter key is exposed on the page object under the same name, so a registry card can be self-describing:

# /gallery/nova.md
title: NOVA
kind: Statement
demo: /nova
accent: "#7C5CFF"
order: 2

[% FOREACH t IN gallery %][% t.kind %] - [% t.demo %] - [% t.accent %][% END %]

Surrounding quotes are stripped (so accent: "#7C5CFF" yields #7C5CFF - quote a value that starts with #, since a bare # … is a YAML comment), and TT markers are stripped for safety. Control keys (layout, theme, auth, register, search, tt_*) are not passed through.

Sort order

Default sort is by filename. Use the sort= modifier to sort by field:

blog_pages: scan:/blog/*.md sort=date desc
gallery:    scan:/gallery/**/*.md sort=order asc

Built-in sort fields are date, title, filename; any custom key also sorts (e.g. sort=order), and numeric values compare numerically (2 before 10), not lexically. Direction: asc or desc (default asc).

For reverse-chronological blog posts, use sort=date desc. Date-prefix filenames (2026-03-20-post-title.md) also sort chronologically by filename without needing the sort modifier.

Per-page scan

Scan variables work in tt_page_var for page-scoped results:

tt_page_var:
  section_pages: scan:/services/*.md sort=title asc

Config path override

The default lazysite.conf path can be overridden via a command-line argument or environment variable. This is rarely needed - each site on a server has its own docroot and therefore its own lazysite.conf automatically. See Config path override for details.