Skip to main content

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​

FieldRequiredDescription
layoutYesTemplate for the listing page
collectionYesWhich collection to list
paginationNoItems per page (default: no pagination)
sort_byNoField to sort by (default: date)
sort_orderNoasc or desc (default: desc)
titleNoPage title
descriptionNoMeta description
imageNoSocial share image
keywordsNoSEO keywords
filterNoMetadata filter (see below)
limitNoMaximum items (overrides pagination)

How Controllers Work​

When ssg.py scans content/, it looks for files with a collection: key. For each one:

  1. Loads the linked collection
  2. Applies filters (if any)
  3. Applies sorting
  4. Applies pagination (if any)
  5. Renders the listing template
  6. Writes to output/<output-name>/index.html

The output name is the file name without .md. For example:

FileOutput
content/journal.mdoutput/journal/index.html
content/projects.mdoutput/projects/index.html
content/blog.mdoutput/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:

VariableDescription
pagination.itemsItems on the current page
pagination.current_pageCurrent page number
pagination.total_pagesTotal number of pages
pagination.total_itemsTotal number of items
pagination.prev_urlURL of the previous page (or None)
pagination.next_urlURL of the next page (or None)
pagination.first_urlURL of the first page
pagination.last_urlURL 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 ByBest For
dateBlog posts, news
titleAlphabetical lists
clientProjects by client
featuredFeatured 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​

PageController
Has collection:NoYes
Outputs a pageYesYes (listing)
Outputs a postYesNo
Has bodyOptionalOptional
Can paginateNoYes

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​

  1. Check that the collection: key exists in the frontmatter
  2. Check that the collection folder exists in content/
  3. Check that the layout file exists

Pagination does not work​

  1. Check that pagination is a number greater than 0
  2. Check that there are more items than the pagination value
  3. Check that the template uses pagination.items

Listing is empty​

  1. Check that the collection has items
  2. Check that filters are not removing all items
  3. Check that sort fields exist in the items

Wrong items appear​

  1. Check the collection value
  2. Check filters
  3. Check sorting

Duplicate listing pages​

Two controllers may share the same collection. Rename one or remove it.


What's Next?​