Skip to main content

Pelican data processing with jinja2content

Exploring how to use the jinja2content plugin to process complex data structures in Pelican themes

Introduction

Continuing our investigations from our previous article about data processing in Pelican themes, we'll now explore another powerful plugin: jinja2content. This plugin extends Pelican's capabilities by allowing us to use Jinja2 code directly within our content files.

With this plugin, we can implement dynamic content generation right in our project's Markdown files. For example:

<ul>
{% for href, caption in [('index.html', 'Index'), ('about.html', 'About'),
                        ('downloads.html', 'Downloads')] %}
    <li><a href="{{ href }}">{{ caption }}</a></li>
{% endfor %}
</ul>

When rendered in the browser, this code produces a neatly formatted list of links. Without the plugin, this code would be displayed literally on the page rather than executed.

In this article, we'll explore:

  • What data structures we can use in Pelican's content files
  • Whether these data structures will be available for processing by the theme
  • How to make this data available for theme processing

How jinja2content works

Before diving into implementation details, it's important to understand how this plugin operates behind the scenes.

Note

The jinja2content plugin processes your content before Pelican's native parser. This means Pelican variables typically available to your templates are not accessible during this initial rendering phase.

According to the plugin's documentation:

This plugin allows the use of Jinja2 directives inside your Pelican articles and pages.

In this approach, your content is first rendered by the Jinja template engine. The result is then passed to the normal Pelican reader as usual. There are two consequences for usage. First, this means the Pelican context and Jinja variables usually visible to your article or page template are not available at rendering time. Second, it means that if any of your input content could be parsed as Jinja directives, they will be rendered as such. This is unlikely to happen accidentally, but it's good to be aware of.

This processing order has significant implications for how we structure our data, as we'll see next.

Compatibility issues with YAML frontmatter

The YAML frontmatter problem

Almost immediately upon testing more complex data structures, we encountered compatibility issues. When this plugin is activated, complex YAML frontmatter structures become inaccessible in our templates.

This occurs because the plugin parses content before passing it to Pelican's parser. Since the content (including YAML frontmatter) is processed by Jinja2 first, structured YAML data can be mangled before Pelican has a chance to process it properly.

The compatibility challenge

If the content is pre-processed by the plugin's reader before reaching Pelican's native parser, we need an alternative approach. Ideally, we would split the data before the initial parsing and reconnect it for later processing by Pelican, but this isn't supported by the plugin architecture.

We need to find another way to represent structured data that works with this processing pipeline.

Data structures in Pelican content

Since complex YAML frontmatter isn't compatible with this plugin, we need alternative ways to define structured data within our content files.

Original YAML approach (incompatible)

For reference, here's how we defined testimonial data in our previous article:

---
title: Testimonial
data:
    testimonials:
        - name: John
          position: Client
          blurb: Will buy again.
        - name: Jill
          position: Business owner
          blurb: Great service!
---

Using nested lists

One approach is to use Jinja2's variable declaration syntax with nested lists:

{% set testimonialsA = ['John', 'Client', 'Will buy again.'] %}
{% set testimonialsB = ['Jill', 'Business owner', 'Great service!'] %}
{% set testimonialsList = [testimonialsA, testimonialsB] %}

Using nested dictionaries

For more semantic clarity, we can use nested dictionaries:

{% set testimonialsDict = {
'testimonial01': {'name': 'John', 'position': 'Client', 'blurb': 'Will buy again.'},
'testimonial02': {'name': 'Jill', 'position': 'Business owner', 'blurb': 'Great service!'},
} %}

This approach preserves the semantic structure while remaining compatible with the jinja2content processing pipeline.

Processing data with theme templates

Consistent with our previous article, our goal remains to keep data processing within the theme's templates. This separation of concerns helps maintain a clean architecture and makes our themes more reusable.

Let's explore three different approaches to processing data defined in our content files.

Including template files

In this approach, we include templates from our theme directly within our content pages.

For example, to display our testimonials, we could add this to our Markdown file:

{% include 'partials/sections/about.html' %}

Then, in our theme's partials/sections/about.html file, we can access and display the data:

<div class="testimonials">
    {% for testimonial in testimonialsList %}
        <div class="testimonial">
            <p class="name">{{ testimonial[0] }}</p>
            <p class="position">{{ testimonial[1] }}</p>
            <p class="blurb">{{ testimonial[2] }}</p>
        </div>
    {% endfor %}
</div>

This works because the data variables are defined before the include statement in our content file.

Importing macros

For more sophisticated processing, we can import macros from our theme and apply them to our data.

In our content file (content/pages/about.md), we define the data and import the macro:

{% set testimonialsDict = {
'testimonial01': {'name': 'John', 'position': 'Client', 'blurb': 'Will buy again.'},
'testimonial02': {'name': 'Jill', 'position': 'Business owner', 'blurb': 'Great service!'},
} %}

{% from 'macros.html' import process_testimonials %}
{{ process_testimonials(testimonialsDict) }}

In our theme's templates/macros.html file, we define the processing macro:

{% macro process_testimonials(testimonialsDict) -%}
    <dl class="testimonials">
        {% for item, dict in testimonialsDict.items() %}
            {% for key, value in dict.items() %}
                <dt>
                    {{ key|e }}
                </dt>
                <dd>
                    {{ value|e }}
                </dd>
            {% endfor %}
        {% endfor %}
    </dl>
{%- endmacro %}

When the site is generated, the macro processes our data into well-formed HTML.

Using inline Jinja

Although it goes against our aim to separate data from presentation, the plugin also allows inline Jinja processing directly in content files:

{% set testimonialsDict = {
'testimonial01': {'name': 'John', 'position': 'Client', 'blurb': 'Will buy again.'},
'testimonial02': {'name': 'Jill', 'position': 'Business owner', 'blurb': 'Great service!'},
} %}

<dl class="testimonials">
    {% for item, dict in testimonialsDict.items() %}
        {% for key, value in dict.items() %}
            <dt>
                {{ key|e }}
            </dt>
            <dd>
                {{ value|e }}
            </dd>
        {% endfor %}
    {% endfor %}
</dl>

This approach might be useful for quick prototyping or simpler sites but generally should be avoided for maintainability.

Best practices

Based on our experiments, here are some recommendations for using jinja2content effectively:

Tip

When using jinja2content, define your data structures at the beginning of your content file, before any content that uses them. This ensures the variables are available when needed.

Warning

Avoid using complex data structures in YAML frontmatter when using this plugin. Instead, define them using Jinja2's variable syntax within the content body.

Important

Always escape user-generated content with the `|e` filter to prevent potential XSS vulnerabilities when displaying the data.

Conclusion

The jinja2content plugin offers powerful capabilities for dynamic content generation in Pelican sites, but comes with some important limitations to consider:

  • Complex YAML frontmatter becomes inaccessible when using this plugin
  • Data structures must be defined within the content body using Jinja2 syntax
  • Several approaches exist for processing this data: including templates, importing macros, or using inline Jinja

By understanding these constraints and leveraging the alternative approaches we've explored, you can effectively use complex data structures in your Pelican projects while maintaining a clean separation between data and presentation.

For more advanced use cases, consider combining this approach with custom plugins that might offer more flexibility in how data is accessed and processed during the site generation pipeline.