AI briefing - content authoring
Guide for AI assistants helping users write content on a lazysite site.
Who this is for
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.
Page format
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.
Front matter keys
title-
Page title. Required for most pages. Used in the
<title>tag and the page<h1>. The browser<title>renders as "page title - site name", so on the home page make this differ fromsite_nameinlazysite.conf- an identical title and site name read as "My Site - My Site". subtitle-
Short description shown below the title. Optional. It also supplies the
page's
<meta name="description">and its description inllms.txt, unlessmeta_descoverrides them. meta_desc-
The page's
<meta name="description">and itsllms.txtdescription, without printing anything on the page. Optional; falls back tosubtitle.
Use it on a page whose design does not want a visible subheading - a page with a hero section, for instance, where a subtitle renders directly above the hero. Before this existed the author had to choose between a subheading they had not designed for and no description at all for search engines and AI clients, and the page most likely to face that choice is the home page.
meta_title-
The
<title>tag, when it should differ from the on-page title. Optional; falls back totitle. ttl-
Cache TTL in seconds. The page regenerates after this interval rather
than on
.mdfile edit. Example:ttl: 300. register-
List of registry files the page appears in. Values match template
filenames in
lazysite/templates/registries/without the.ttextension. Common values:llms.txt,sitemap.xml,feed.rss,feed.atom. date-
Publication date in
YYYY-MM-DDformat. Used in feed entries. Falls back to file mtime if not set. tt_page_var-
Page-scoped Template Toolkit variables. Supports
url:(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:.)
raw-
raw: trueoutputs the converted body without the view wrapper. Useful for fragments, AJAX partials, or API-style endpoints. api-
api: trueserves the page as a JSON API endpoint. Default content type isapplication/json; charset=utf-8. Combine withtt_page_varandquery_paramsfor dynamic JSON. content_type-
Overrides the HTTP Content-type header for a raw/api data artifact.
Example:
content_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 totext/plainat serve time) because such a page is served verbatim and unescaped - serve HTML through a layout, or as a static file. layout-
Per-page layout override - names a layout under
lazysite/layouts/. Useful for previewing a staged layout on one page before activation. auth-
Authentication requirement.
required,optional, ornone(default). auth_groups- List of group names. User must be in one of them.
payment- Payment requirement for the x402 payment flow. See Payment.
query_params-
List of accepted URL query parameter names. Matched parameters are
available as
[% query.NAME %]and bypass the cache. tags- Tags for page scan results. YAML list, comma-separated, or single value.
search-
trueorfalse. Controls whether the page appears in search. Defaults to the site'ssearch_defaultsetting. form-
Enables form processing for the page and names the form. Requires a
matching
lazysite/forms/NAME.conf.
Markdown elements
Headings
# H1 is reserved - the page title is rendered by the view template.
Start content headings at ##.
Text, links, lists
Standard Markdown. Internal links should be extensionless:
Docs not Docs.
Tables
Standard GFM pipe tables are supported.
Code blocks
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.
Fenced divs
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 highlight
Content components
If 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
::: oembed
https://www.youtube.com/watch?v=abc123
:::
Works with YouTube, Vimeo, SoundCloud, and any oEmbed provider.
Content includes
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.
A local include path is resolved inside your site's content root, and cannot reach outside it. On a multi-domain instance that means a domain includes its own partials and never another domain's - the confinement is not a setting you can relax. (SM275: the shipped behaviour is stricter than earlier wording implied, and a reader who believed the looser version would design a shared-partials layout that cannot work.)
Use the multi-line fenced form above. A one-line ::: include path is left
as literal text, not expanded. Keep consecutive includes contiguous.
Embedded HTML
You can drop raw HTML into a page, but Markdown's block rules bite - both of these silently mangle it:
- HTML indented 4 or more spaces is treated as a code block. Keep embedded HTML flush to the left margin.
- HTML blocks separated by blank lines get wrapped in
<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.)
Template Toolkit in page content
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.
The theme variables resolve in a page body
theme_assets, theme_name, theme, theme_css and layout_name resolve in a
page body as well as in layout.tt. So this works, and is the right way to
reference a themed asset from a page:
<img src="[% theme_assets %]/hero.jpg">
theme_assets expands to /lazysite-assets/LAYOUT/THEME, so the path follows
the site when its layout or theme changes. Prefer it over writing
/lazysite-assets/<layout>/<theme>/hero.jpg literally: a hard-coded path is
correct only until someone activates a different theme, and then it 404s with no
indication of why.
They resolve on ordinary pages, which are the ones with a layout. A raw page
(raw: true / api: true) has no layout, so it has no theme either and these
are empty there - which is the correct answer for a page that renders no chrome.
If the asset belongs to the design rather than to this one page, it still belongs in the layout or the theme's CSS rather than in a page body.
On an older engine. Before 0.10.5 these were layout-scope only, and a page
body using one got the empty string with no error - [% theme_assets %]/hero.jpg
rendered as /hero.jpg and 404'd. If you are working against an older engine, or
reading guidance that says to write the path literally, that is why.
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>
URL structure
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.
Remote pages
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.
Page scan
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.
Search
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.
Registries
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).
Tasks
Creating a new page
- Ask the user for: title, URL path, brief description.
- Create a file at
DOCROOT/PATH.md(e.g./docs/installbecomesDOCROOT/docs/install.md). - Write front matter with
title:and optionallysubtitle:. - Register in relevant feeds:
sitemap.xml,llms.txt, andfeed.rssorfeed.atomif it is a dated article. - Write the body in Markdown, starting headings at
##. - Write a
<file>.briefsidecar 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.mdis the build: when the owner edits the brief, refactor the page to match it. See Document your intent:.briefsidecars in AI briefing - publishing for the full structure and workflow.
Creating a blog/news index with scan
- Confirm blog posts live in a single directory, e.g.
/blog/. - Create
DOCROOT/blog/index.mdwithtt_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 %]
Creating a members-only page
- Confirm the required group name in
lazysite/auth/groups. - Write the page normally, then set:
---
title: Members area
auth: required
auth_groups:
- members
---
Protected pages are never cached.