Collections
A collection is a named list of other pages on the same site. The compiler fills it when the page is first requested. It is not Postgres, not a JSON store, and not something you INSERT into.
For app records (contacts, tasks, subscribers), use sql --db. See Keep app data in a database.
Done looks like
An index page lists every matching .page.md / .page.html. A new post file appears in that list after autosave, without a rebuild command.
Worked example: a blog index
This site already does it. pages/index.page.html and pages/how-to/index.page.html both declare a glob.
---
title: How to
howtos:
glob: pages/how-to/*.page.md
where: draft != true
sort: nav_order
order: asc
---
{% for page in howtos %}
<a href="/guides/{{ page._url }}">{{ page.title }}</a>
{% endfor %}
From a fresh site:
cp -r /templates/static-blog ./site
site/pages/index.page.html already has posts: with glob: pages/blog/**. Add site/pages/blog/launch.page.md:
---
title: Launch
date: 2026-08-19
draft: false
---
<site-shell title="{{ title }}">
# Launch
Shipped.
</site-shell>
Publish once (publish create site --path site --policy 644). Open /. The homepage loop over posts includes Launch. Edit the file; autosave is enough.
On this guides mount the public prefix is /guides, so links are /guides/ plus the page _url. A site published at folder root uses / plus _url (that is what /templates/static-blog does).
Front-matter keys
Declare the list under any name (posts, howtos, features). Required key is glob.
| Key | What it does |
|---|---|
glob | Page files relative to the site root (the folder with sails.site.json). Example: pages/blog/** |
where | SQL-like filter on front matter. Missing fields are null. There is no filter: key |
sort | Front-matter field name (date, nav_order, title) |
order | asc or desc |
where examples that work:
where: draft != true
where: draft != true and date != null
where: id = $threadId
$threadId is a splat from a file like pages/threads/$threadId.page.html. The captured value is also a template variable named threadId.
Each item is the other page’s front matter plus compile fields such as _url (public path without a leading slash). Read title, date, and _url on each item.
What is not a collection
| Tempting file / key | Reality |
|---|---|
sails.data.json | Do not use. Not how app records work |
Page from: or page sql: | Do not use for records |
collection query contacts | Not a workspace command for this. Query Postgres with sql --db |
| Writing a row from a form | Handler talks to sql --db, not to a glob |
Copy /templates/crm-app (or remix-app / table-app) when the job is rows.
Failure modes
| What you see | What it means | What to do |
|---|---|---|
| Loop is empty | Glob missed, or where dropped every page | Path is from the site root (pages/blog/**, not blog/**). draft: true pages are excluded by draft != true. A page with no draft key is null, so draft != true still includes it |
| New post missing on the live URL | Cache still has the old collection | Autosave the new file. The page recompiles when collection files change. Do not publish create again |
filter: in front matter | That key is not a collection filter | Use where: |
You put contacts in sails.data.json | Wrong store | .db bundle + sql --db. See Keep app data in a database |
check --path template error | The template failed the same way publish would | Fix the loop syntax and quotes. Do not HTML-escape quotes inside if expressions |
How to: Publish a site, Keep app data in a database. Also Static sites.