Skip to main content

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:

TypeLocationPurpose
Layouttemplates/layouts/Full page (header, body, footer)
Partialtemplates/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:

VariableDescription
siteGlobal site config (from _data/site.yaml)
pageCurrent page metadata
contentRendered body HTML
collectionsAll collections (posts, projects, etc.)
tagsAll tags with their posts
generatorEngine name
versionPycora 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:

FilterExampleResult
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 %}
VariableDescription
loop.index1-based index
loop.index00-based index
loop.firstTrue on first item
loop.lastTrue 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:

  1. templates/<name>.pax
  2. templates/<name>.html
  3. templates/layouts/<name>.pax
  4. templates/layouts/<name>.html
  5. templates/partials/<name>.pax
  6. templates/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 limit over slice syntax — more explicit
  • Always escape untrusted content

Troubleshooting​

TemplateNotFound​

  1. Check that the template file exists
  2. Check the spelling of the path
  3. Check that the file is inside templates/

Blank output​

  1. Check that the layout extends a base
  2. Check that {% block content %} is defined
  3. Confirm page.title and content are set

Variable not found​

  1. Check the variable name — case-sensitive
  2. Check that it is passed in the context
  3. Use {{ variable | default("fallback") }} for optional values

Slice syntax does not work​

  1. Check that the template uses .pax extension
  2. Or convert to | limit(n) syntax

Loop outputs nothing​

  1. Check that the collection is not empty
  2. Check that the collection name matches the folder
  3. Confirm filters are not removing all items

What's Next?​