Controllers
A controller is a Markdown file that generates a listing page. It does not become a page itself — it lists items from a collection.
Controllers are how you build:
- Blog listings
- Portfolio listings
- Category pages
- Archive pages
What Makes a Controller
A file is a controller if its frontmatter contains a collection: key.
Example — content/journal.md:
---
layout: layouts/blog
collection: posts
pagination: 6
sort_by: date
sort_order: desc
title: "Journal"
description: "All posts"
---
This file does not appear at /journal/ as a post. Instead, it generates a listing page at /journal/ that shows posts from the posts collection.
Controller Frontmatter
| Field | Required | Description |
|---|---|---|
layout | Yes | Template for the listing page |
collection | Yes | Which collection to list |
pagination | No | Items per page (default: no pagination) |
sort_by | No | Field to sort by (default: date) |
sort_order | No | asc or desc (default: desc) |
title | No | Page title |
description | No | Meta description |
image | No | Social share image |
keywords | No | SEO keywords |
filter | No | Metadata filter (see below) |
limit | No | Maximum items (overrides pagination) |
How Controllers Work
When ssg.py scans content/, it looks for files with a collection: key. For each one:
- Loads the linked collection
- Applies filters (if any)
- Applies sorting
- Applies pagination (if any)
- Renders the listing template
- Writes to
output/<output-name>/index.html
The output name is the file name without .md. For example:
| File | Output |
|---|---|
content/journal.md | output/journal/index.html |
content/projects.md | output/projects/index.html |
content/blog.md | output/blog/index.html |
Pagination
If pagination is set to a number, the controller generates multiple pages:
pagination: 6
With 18 posts, this generates:
output/journal/index.html (posts 1–6)
output/journal/page/2/index.html (posts 7–12)
output/journal/page/3/index.html (posts 13–18)
Each page receives a pagination object in the template context:
| Variable | Description |
|---|---|
pagination.items | Items on the current page |
pagination.current_page | Current page number |
pagination.total_pages | Total number of pages |
pagination.total_items | Total number of items |
pagination.prev_url | URL of the previous page (or None) |
pagination.next_url | URL of the next page (or None) |
pagination.first_url | URL of the first page |
pagination.last_url | URL of the last page |
Example Template
{% for post in pagination.items %}
<article>
<a href="{{ post.url }}">{{ post.title }}</a>
</article>
{% endfor %}
{% if pagination.prev_url %}
<a href="{{ pagination.prev_url }}">← Previous</a>
{% endif %}
{% if pagination.next_url %}
<a href="{{ pagination.next_url }}">Next →</a>
{% endif %}
Filtering
You can filter the collection with a filter key:
filter:
featured: true
This only lists posts where featured: true in their frontmatter.
Multiple filters:
filter:
category: "tutorial"
featured: true
Nested filters:
filter:
tags: "python"
Filters are applied before pagination.
Sorting
Sorting is controlled by sort_by and sort_order:
sort_by: date
sort_order: desc
| Sort By | Best For |
|---|---|
date | Blog posts, news |
title | Alphabetical lists |
client | Projects by client |
featured | Featured first |
Limit
To show a fixed number of items without pagination:
limit: 3
This is useful for "latest posts" sections on the home page.
Controllers vs Pages
| Page | Controller | |
|---|---|---|
Has collection: | No | Yes |
| Outputs a page | Yes | Yes (listing) |
| Outputs a post | Yes | No |
| Has body | Optional | Optional |
| Can paginate | No | Yes |
Building a Controller
Step 1 — Create a Collection
Create a folder under content/:
mkdir content/services
Add Markdown files inside.
Step 2 — Create a Controller
Create content/services.md:
---
layout: layouts/services
collection: services
pagination: 9
sort_by: date
sort_order: desc
title: "Services"
description: "Our services"
---
Step 3 — Create the Layout
Create templates/layouts/services.pax:
{% extends "layouts/base" %}
{% block content %}
<h1>{{ page.title }}</h1>
<div class="grid">
{% for service in pagination.items %}
<article>
<h2><a href="{{ service.url }}">{{ service.title }}</a></h2>
<p>{{ service.description }}</p>
</article>
{% endfor %}
</div>
{% if pagination.next_url %}
<a href="{{ pagination.next_url }}">Next →</a>
{% endif %}
{% endblock %}
Step 4 — Build
python ssg.py
The listing appears at /services/.
Best Practices
- One controller per collection — avoid duplicates
- Use pagination for long lists — 6–12 items per page
- Sort by
date+desc— the expected default - Keep the layout simple — listing pages are about content
- Use filters sparingly — they hide content from the list
Troubleshooting
Controller does not generate a page
- Check that the
collection:key exists in the frontmatter - Check that the collection folder exists in
content/ - Check that the layout file exists
Pagination does not work
- Check that
paginationis a number greater than 0 - Check that there are more items than the pagination value
- Check that the template uses
pagination.items
Listing is empty
- Check that the collection has items
- Check that filters are not removing all items
- Check that sort fields exist in the items
Wrong items appear
- Check the
collectionvalue - Check filters
- Check sorting
Duplicate listing pages
Two controllers may share the same collection. Rename one or remove it.