Template Engine
Pycora Pro uses Jinja2 as its template engine, with a custom loader called PAXLoader that adds file discovery and slice-syntax compatibility.
This page explains how templates work and how to extend them.
Template Types
There are two types of templates:
| Type | Location | Purpose |
|---|---|---|
| Layout | templates/layouts/ | Full page (header, body, footer) |
| Partial | templates/partials/ | Reusable fragment |
Top-level pages like home.pax or about.pax may live directly inside templates/.
Layout Inheritance
Pycora Pro uses Jinja2's {% extends %} for layout inheritance.
A page template typically extends a layout:
{% extends "layouts/base" %}
{% block content %}
<h1>{{ page.title }}</h1>
{{ content | safe }}
{% endblock %}
The layout defines the blocks:
<!DOCTYPE html>
<html lang="{{ site.lang }}">
<head>
<title>{{ page.title }}</title>
</head>
<body>
{% include "partials/nav" %}
<main>{% block content %}{% endblock %}</main>
{% include "partials/footer" %}
</body>
</html>
Partials
Partials are reusable fragments. Include them with {% include %}:
{% include "partials/nav" %}
{% include "partials/footer" %}
Partials receive the same context as the page that includes them. To pass extra data:
{% include "partials/card" with { "post": post } only %}
Context Variables
Every template has access to:
| Variable | Description |
|---|---|
site | Global site config (from _data/site.yaml) |
page | Current page metadata |
content | Rendered body HTML |
collections | All collections (posts, projects, etc.) |
tags | All tags with their posts |
generator | Engine name |
version | Pycora Pro version |
Rendering Content
The page body is rendered from Markdown to HTML. To output it safely:
{{ content | safe }}
Without | safe, HTML tags are escaped.
Iterating Collections
To list posts:
{% for post in collections.posts | limit(6) %}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<p>{{ post.description }}</p>
</article>
{% endfor %}
To list projects:
{% for project in collections.projects %}
<article>
<a href="{{ project.url }}">{{ project.title }}</a>
<span>{{ project.client }}</span>
</article>
{% endfor %}
Filters
Pycora Pro provides a set of filters for common operations:
| Filter | Example | Result |
|---|---|---|
limit(n) | {{ posts | limit(3) }} | First 3 posts |
slice(start, end) | {{ posts | slice(1, 4) }} | Posts 2–4 |
where(key, value) | {{ posts | where("featured", true) }} | Only featured posts |
filter_tag(tag) | {{ posts | filter_tag("python") }} | Posts tagged python |
sort_by(field) | {{ posts | sort_by("date") }} | Sorted by date |
reverse | {{ posts | reverse }} | Reversed order |
first | {{ posts | first }} | First item |
last | {{ posts | last }} | Last item |
slugify | {{ "Hello World" | slugify }} | hello-world |
Slice Syntax
PAXLoader supports both Python-style slicing and the limit filter:
{{ posts[:3] }} → {{ posts | limit(3) }}
{{ posts[1:3] }} → {{ posts | slice(1, 3) }}
Use whichever you prefer.
Conditionals
{% if page.toc %}
<aside>{{ page.toc | safe }}</aside>
{% endif %}
{% if post.image %}
<img src="{{ post.image }}" alt="{{ post.title }}">
{% endif %}
Loops with Index
{% for item in collections.projects %}
<div class="project-{{ loop.index }}">
{{ item.title }}
</div>
{% endfor %}
| Variable | Description |
|---|---|
loop.index | 1-based index |
loop.index0 | 0-based index |
loop.first | True on first item |
loop.last | True on last item |
Escaping
By default, Jinja2 escapes HTML. To output raw HTML:
{{ content | safe }}
Only use | safe on trusted content.
The .pax Extension
The .pax extension is Pycora Pro's convention for templates that use the PAXLoader slice fix. Functionally, .pax and .html files are identical.
The loader searches in this order:
templates/<name>.paxtemplates/<name>.htmltemplates/layouts/<name>.paxtemplates/layouts/<name>.htmltemplates/partials/<name>.paxtemplates/partials/<name>.html
Use .pax for templates that rely on slice syntax. Use .html for everything else.
Custom Filters
To add a custom filter, edit ssg.py and register it on the environment:
self.env.filters["my_filter"] = lambda value: value.upper()
Then use it in any template:
{{ page.title | my_filter }}
Best Practices
- Keep layouts thin — move logic into partials
- Use partials for repeated markup — nav, footer, cards
- Use
{% include %}for fragments — not{% extends %} - Use
{% extends %}only for full page inheritance - Prefer
limitover slice syntax — more explicit - Always escape untrusted content
Troubleshooting
TemplateNotFound
- Check that the template file exists
- Check the spelling of the path
- Check that the file is inside
templates/
Blank output
- Check that the layout extends a base
- Check that
{% block content %}is defined - Confirm
page.titleandcontentare set
Variable not found
- Check the variable name — case-sensitive
- Check that it is passed in the context
- Use
{{ variable | default("fallback") }}for optional values
Slice syntax does not work
- Check that the template uses
.paxextension - Or convert to
| limit(n)syntax
Loop outputs nothing
- Check that the collection is not empty
- Check that the collection name matches the folder
- Confirm filters are not removing all items