Listings
Anywhere you want the list, add a listing for the same collection. Inside it,
sub-commands do the work: operations (filter, filter-any,
sort, group, limit) and templates
(item, plus optional separator, heading, start,
end and empty). The operations run in the order you write
them:
<!-- localhoster:listing collection="posts" -->
<!-- localhoster:listing-filter field="status" op="eq" value="published" -->
<!-- localhoster:listing-filter-any -->
<!-- localhoster:listing-filter field="category" op="eq" value="news" -->
<!-- localhoster:listing-filter field="category" op="eq" value="updates" -->
<!-- localhoster:/listing-filter-any -->
<!-- localhoster:listing-group by="category" using="date" order="desc" -->
<!-- localhoster:listing-sort field="category" order="asc" -->
<!-- localhoster:listing-limit start="0" count="20" -->
<!-- localhoster:listing-start
```
<ul class="posts">
``` -->
<!-- localhoster:listing-heading
```
<h3>{{category}}</h3>
``` -->
<!-- localhoster:listing-item
```
<li><a href="{{url}}">{{title}}</a></li>
``` -->
<!-- localhoster:listing-end
```
</ul>
``` -->
<!-- localhoster:listing-empty
```
<p class="muted">No posts yet.</p>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="posts" -->
Start & End
listing-start and listing-end bracket the list: rendered once each, before
the first item and after the last, but only when the list is not empty. Both are optional:
<!-- localhoster:listing collection="blog" -->
<!-- localhoster:listing-start
```
<ul class="posts">
``` -->
<!-- localhoster:listing-item
```
<li><a href="{{url}}">{{title}}</a></li>
``` -->
<!-- localhoster:listing-end
```
</ul>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="blog" -->
Empty
listing-empty (optional) is rendered instead of the whole list when nothing matches, so a
filtered listing shows a friendly placeholder rather than blank space:
<!-- localhoster:listing collection="blog" -->
<!-- localhoster:listing-filter field="status" op="eq" value="published" -->
<!-- localhoster:listing-item
```
<li><a href="{{url}}">{{title}}</a></li>
``` -->
<!-- localhoster:listing-empty
```
<p class="muted">No posts yet.</p>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="blog" -->
Items
listing-item (required) is the markup rendered once per record:
<!-- localhoster:listing collection="blog" -->
<!-- localhoster:listing-item
```
<article class="card">
<h3><a href="{{url}}">{{title}}</a></h3>
<p class="muted">{{date}} · {{tags}}</p>
{{summary}}
</article>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="blog" -->
| Placeholder | Description |
|---|---|
{{field}} | any field the record declares (e.g. title, date, tags) |
{{url}} | the record page’s own path |
Separators
listing-separator (optional) is inserted between items, never before the first or after
the last. A listing uses either a separator or a heading, not both:
<!-- localhoster:listing collection="blog" -->
<!-- localhoster:listing-item
```
<li><a href="{{url}}">{{title}}</a></li>
``` -->
<!-- localhoster:listing-separator
```
<hr>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="blog" -->
Headings
listing-heading (optional) is rendered per item but shown only when it changes from
the item before it, so a sorted list subdivides into headed groups; put any rule or spacing into the
heading markup itself:
<!-- localhoster:listing collection="posts" -->
<!-- localhoster:listing-sort field="category" -->
<!-- localhoster:listing-heading
```
<h3>{{category}}</h3>
``` -->
<!-- localhoster:listing-item
```
<li><a href="{{url}}">{{title}}</a></li>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="posts" -->
| Placeholder | Description |
|---|---|
{{field}} | any field the record declares; the heading shows whenever this value changes |
{{url}} | the record page’s own path |
Filtering
listing-filter keeps records where field compares to value by
op. Repeating it requires all of the conditions; for any-of (OR)
instead, group filters in a filter-any (below):
<!-- localhoster:listing collection="guides" -->
<!-- localhoster:listing-filter field="topic" op="contains" value="caddy" -->
<!-- localhoster:listing-filter field="date" op="gte" value="2026-01-01" -->
<!-- localhoster:listing-item
```
<li><a href="{{url}}">{{title}}</a></li>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="guides" -->
| Parameter | Description | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
field | the record field to test | ||||||||||||||||||||||||||||||
op | how to compare the field to the value | ||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||
value | the value to compare against |
Filter Any
listing-filter-any is a paired block holding two or more plain listing-filters;
a record passes if it matches any of them (OR). It sits anywhere in the pipeline, like a
filter, and may contain only listing-filter commands:
<!-- localhoster:listing collection="posts" -->
<!-- localhoster:listing-filter-any -->
<!-- localhoster:listing-filter field="category" op="eq" value="news" -->
<!-- localhoster:listing-filter field="category" op="eq" value="updates" -->
<!-- localhoster:/listing-filter-any -->
<!-- localhoster:listing-item
```
<li><a href="{{url}}">{{title}}</a> ({{category}})</li>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="posts" -->
Sorting
listing-sort orders the set by a field:
<!-- localhoster:listing collection="blog" -->
<!-- localhoster:listing-sort field="date" order="desc" -->
<!-- localhoster:listing-item
```
<li><a href="{{url}}">{{title}}</a></li>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="blog" -->
| Parameter | Description | ||||||
|---|---|---|---|---|---|---|---|
field | the field to order by | ||||||
order | the direction | ||||||
|
Grouping
listing-group groups records by by, sorts each group by
using/order, then takes a slice with start and count
(default 0 and 1, so one representative per group). Raise count for the top N from each
group, or set start to pick a position, like the second; a group may yield fewer:
<!-- localhoster:listing collection="posts" -->
<!-- localhoster:listing-group by="category" using="date" order="desc" count="3" -->
<!-- localhoster:listing-item
```
<li><a href="{{url}}">{{title}}</a> ({{category}})</li>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="posts" -->
| Parameter | Description | ||||||
|---|---|---|---|---|---|---|---|
by | the field to group on | ||||||
using | the field that sorts within each group | ||||||
order | the sort direction within the group | ||||||
| |||||||
start | how many to skip within each group (default 0) | ||||||
count | how many to keep from each group — a maximum (default 1) |
Limiting
listing-limit takes a slice with start and count. It can sit
anywhere in the pipeline, so you can trim before or after a group, or page through results:
<!-- localhoster:listing collection="blog" -->
<!-- localhoster:listing-sort field="date" order="desc" -->
<!-- localhoster:listing-limit start="0" count="5" -->
<!-- localhoster:listing-item
```
<li><a href="{{url}}">{{title}}</a></li>
``` -->
… localhoster writes the generated list here …
<!-- localhoster:/listing collection="blog" -->
| Parameter | Description |
|---|---|
start | how many records to skip (the offset) |
count | how many records to render |
Errors
A listing is validated before its list is generated; a problem is reported against the page and the listing, and the site is left unchanged until it is fixed (other sites still run):
| Message | When it appears |
|---|---|
listing "…": a listing needs a listing-item template | no listing-item (the one required template) |
listing "…" may not contain … | an unrecognised sub-command inside the listing |
listing "…": listing-filter needs field="…" (also op, value) | a filter, sort or group missing a required attribute |
listing "…": listing-filter op="…" is not one of … | an unknown comparison operator |
listing "…": listing-sort order must be asc or desc | a sort or group with an order other than asc/desc |
listing "…": listing-limit needs start and/or count | a listing-limit with neither bound |
listing "…": listing-filter-any needs at least one listing-filter | an empty filter-any block |
listing "…": listing-filter-any may only contain listing-filter (found …) | a filter-any holding anything but plain filters |