This document briefs an AI assistant helping a user author content on a lazysite site. It covers the page format, front matter, Markdown extensions, and URL conventions. For view/theme authoring, see AI briefing - layouts. For configuration, see AI briefing - configuration. For how a page fits the content/layout/theme model - and why ordinary pages must not use raw mode - read AI briefing - building sites first.
Every page is a plain text file ending in .md. Pages begin with a
YAML front matter block delimited by ---, followed by the page body
in Markdown:
---
title: Page Title
subtitle: Optional subtitle shown below title
register:
- llms.txt
- sitemap.xml
---
Page content in Markdown.
The content body is converted to HTML and inserted into the site's
view template at [% content %]. The view wraps it with navigation,
header, and footer.
title<title> tag and
the page <h1>. The browser <title> renders as "page title - site
name", so on the home page make this differ from site_name in
lazysite.conf - an identical title and site name read as
"My Site - My Site".
subtitlettl.md file edit. Example: ttl: 300.
registerlazysite/templates/registries/ without the .tt
extension. Common values: llms.txt, sitemap.xml, feed.rss,
feed.atom.
dateYYYY-MM-DD format. Used in feed entries.
Falls back to file mtime if not set.
tt_page_varurl: (remote text),
scan: (page list), json: (local JSON file -> decoded data structure), and
${ENV} prefixes. Page variables override site variables.
tt_page_var:
hero_image: /img/landing.jpg
latest_release: url:https://raw.githubusercontent.com/example/repo/main/VERSION
blog_posts: scan:/blog/*.md sort=date desc
matrix: json:/data/comparison.json # then [% FOREACH r IN matrix.rows %]…[% END %]
To build a data-driven table/matrix from a JSON file, use json: - it gives the
body a real data structure to loop. ([% USE JSON %] needs the optional
Template::Plugin::JSON module, which is not bundled; prefer json:.)
rawraw: true outputs the converted body without the view wrapper.
Useful for fragments, AJAX partials, or API-style endpoints.
apiapi: true serves the page as a JSON API endpoint. Default content
type is application/json; charset=utf-8. Combine with
tt_page_var and query_params for dynamic JSON.
content_typecontent_type: text/csv; charset=utf-8. Script-capable types
(text/html, application/xhtml+xml, image/svg+xml) are refused on a
raw/api page (downgraded to text/plain at serve time) because such a
page is served verbatim and unescaped - serve HTML through a layout, or
as a static file.
layoutlazysite/layouts/.
Useful for previewing a staged layout on one page before activation.
authrequired, optional, or none
(default).
auth_groupspaymentquery_params[% query.NAME %] and bypass the cache.
tagssearchtrue or false. Controls whether the page appears in search.
Defaults to the site's search_default setting.
formlazysite/forms/NAME.conf.
# H1 is reserved - the page title is rendered by the view template.
Start content headings at ##.
Standard Markdown. Internal links should be extensionless:
Docs not Docs.
Standard GFM pipe tables are supported.
Fenced code blocks with language identifiers produce highlighted output:
```bash
curl https://example.com/
```
Inline code and fenced code blocks are protected from Template Toolkit
processing - [% tags %] inside code appear literally.
Wrap content in a named CSS class:
::: classname
Content here. Standard Markdown works inside.
:::
Produces <div class="classname">...</div>. Class names must contain
only word characters and hyphens.
Common classes in the default theme:
widebox - full-width coloured bandtextbox - 60% width highlighted boxmarginbox - pull quote in the marginexamplebox - evidence or example highlightIf the active layout ships a component by that name, a ::: block is rendered
through it (Markdown in, layout-supplied HTML out) rather than becoming a plain
div - the way to get a hero/feature-grid/etc. without hand-writing HTML:
::: hero eyebrow="Generative"
# A site that's *alive*.
::: actions
Get started
:::
:::
The inner Markdown is the component's content; key="value" on the opening
line are attrs; a nested ::: <name> block is a named slot. A name with no
matching component falls back to <div class="name">. Available components are
layout-specific. For all-structure pages a layout may also read a front-matter
sections: list and compose the page from components.
::: oembed
https://www.youtube.com/watch?v=abc123
:::
Works with YouTube, Vimeo, SoundCloud, and any oEmbed provider.
Inline local or remote content at render time:
::: include
partials/note.md
:::
.md files have their front matter stripped. Code files are wrapped in
syntax-highlighted code blocks. .html files are inserted bare.
Includes are single-pass - includes inside included files are not processed.
Use the multi-line fenced form above. A one-line ::: include path is left
as literal text, not expanded. Keep consecutive includes contiguous.
You can drop raw HTML into a page, but Markdown's block rules bite - both of these silently mangle it:
<p>. Keep a run of
HTML contiguous - no blank lines inside it.When a fragment is awkward to keep flush and contiguous, move it into a .md
include partial and pull it in with ::: include; the partial's HTML is
inserted as-is. (This is also why an activation cache-clear that removes
generated <page>.html never touches your author partials - keep reusable
chrome in .md/.html partials.)
TT variables are expanded in the page content before Markdown
conversion. Site variables come from lazysite.conf, page variables
from tt_page_var. Automatic variables (page_title, page_subtitle,
content) are set by the processor.
Current version: [% latest_release %]
[% IF beta %]
<div class="textbox">
<p>This feature is in beta.</p>
</div>
[% END %]
Inline code and fenced code blocks are protected from TT. Put TT tags outside code blocks if you want them to render.
No randomness primitive. There is no random/shuffle helper in the template
context, so a "random quote" or "random image" cannot be computed per request.
Either pick one at author time, or render the set as a carousel/cycle in the
page or layout. For list-driven pages (reviews, a homepage highlight strip),
scan: + a FOREACH loop give you the data deterministically.
Markdown link URLs do not reliably resolve TT variables (the Markdown
parser processes links before TT runs). Use HTML <a> tags when the
href contains a TT variable:
<a href="[% download_base %]/release-[% version %].tar.gz">Download</a>
Page URLs derive from file paths, always without extension:
DOCROOT/index.md -> /
DOCROOT/about.md -> /about
DOCROOT/docs/install.md -> /docs/install
DOCROOT/docs/index.md -> /docs/
Always use extensionless URLs for internal links.
A .url file contains a single URL. The processor fetches the Markdown
from that URL, processes it through the full pipeline, and caches the
result.
# File: docs/install.url
https://raw.githubusercontent.com/example/repo/main/docs/INSTALL.md
The remote file should include YAML front matter. Cache TTL defaults to one hour.
scan:/path/*.md returns an array of page metadata. Use in
lazysite.conf or tt_page_var:
tt_page_var:
blog_posts: scan:/blog/*.md sort=date desc
[% FOREACH post IN blog_posts %]
## [% post.title %]
[% post.subtitle %] - [% post.date %]
[% post.url %]
[% END %]
Each item has url, title, subtitle, date, and path.
Pages are searchable by default. Set search: false in front matter to
exclude a page from search. The site default is controlled by
search_default in lazysite.conf.
Pages declare which registry files they appear in via register:.
Common registries: sitemap.xml, llms.txt, feed.rss, feed.atom.
Each name maps to a Template Toolkit template in
lazysite/templates/registries/. Registries regenerate after their
TTL expires (default 4 hours).
DOCROOT/PATH.md (e.g. /docs/install becomes
DOCROOT/docs/install.md).title: and optionally subtitle:.sitemap.xml, llms.txt, and
feed.rss or feed.atom if it is a dated article.##.<file>.brief sidecar capturing the page's intent - purpose,
sections, tone, image/content sources, and a "To change this page…" line.
The brief is the source of intent, the .md is the build: when the owner
edits the brief, refactor the page to match it. See Document your intent:
.brief sidecars in AI briefing - publishing
for the full structure and workflow./blog/.DOCROOT/blog/index.md with tt_page_var:---
title: Blog
tt_page_var:
posts: scan:/blog/*.md sort=date desc
---
[% FOREACH p IN posts %]
## [[% p.title %]]([% p.url %])
[% p.subtitle %] - [% p.date %]
[% END %]
lazysite/auth/groups.---
title: Members area
auth: required
auth_groups:
- members
---
Protected pages are never cached.