2025, Dec 24 01:00

Jinja2 Macro Scoping in Pelican: How to Highlight the Current Page in a Navbar the Right Way

Learn to fix Jinja2 macro scoping in Pelican navbars: pass variables correctly, use namespace(), or compute classes inline for reliable current-page highlights.

Pelican sites often start as plain HTML/CSS and then move into Jinja templates. One of the first dynamic touches people add is a navbar that automatically highlights the current section when the site is generated. If the highlight doesn’t switch, the culprit is almost always macro scoping or how variables are passed into the macro.

The failing snippet

The setup looks straightforward: define two links, use a macro to mark the active one, and render both. Yet nothing changes at render time.

{% block nav %}
<nav>
  {% set linkH = '<a class="nav-button" href="/">Home</a>' %}
  {% set linkA = '<a class="nav-button" href="/about.html">About</a>' %}
  {% macro mark_active(sec) %}
    {% if sec == 'Home' %}
      {% set linkH = '<a class="nav-button current-page" href="/">Home</a>' %}
    {% elif sec == 'About' %}
      {% set linkA = '<a class="nav-button current-page" href="/about.html">About</a>' %}
    {% endif %}
  {% endmacro %}
  {{ mark_active("{{ section }}") }}
  {{ linkH }}
  {{ linkA }}
</nav>
{% endblock nav %}

And a page that sets the current section while extending the base:

{% extends "base.html" %}
{% block title %}Homepage{% endblock title %}
{% block section %}{% set section = 'Home' %}{% endblock section %}
{% block content %}
  <content><p>Lorem ipsum</p></content>
{% endblock content %}

The output remains unchanged, with no current-page class applied.

What actually breaks

There are two separate issues at play. First, assignments inside a Jinja macro operate on local variables. The macro sets linkH or linkA, but those names are local to the macro call and do not mutate the outer variables, so the outer values remain untouched. Second, the macro is called with a string literal "{{ section }}" instead of the variable section. That passes the literal curly-brace text rather than the value of section.

Fix 1: share state via namespace and pass the variable correctly

To persist changes made inside a macro, store the values in a namespace object. Assign to attributes on that object inside the macro, and read those attributes outside. Also, pass section directly, not as a quoted string.

{% set state = namespace() %}
{% set state.home = '<a class="nav-button" href="/">Home</a>' %}
{% set state.about = '<a class="nav-button" href="/about.html">About</a>' %}
{% macro activate(sec) %}
  {% if sec == 'Home' %}
    {% set state.home = '<a class="nav-button current-page" href="/">Home</a>' %}
  {% elif sec == 'About' %}
    {% set state.about = '<a class="nav-button current-page" href="/about.html">About</a>' %}
  {% endif %}
{% endmacro %}
{% block nav %}
{{ activate(section) }}
<nav>
  {{ state.home }}
  {{ state.about }}
</nav>
{% endblock nav %}

If you are debugging, it’s fine to output diagnostic text inside the macro because a macro returns a string. But the key is that the mutation must target attributes of a namespace created with namespace(). If you try to assign an attribute on something that isn’t a namespace, you’ll hit “TemplateRuntimeError: cannot assign attribute on non-namespace object”.

Fix 2: skip mutation and compose the class with a small macro

A leaner approach is to avoid state mutation entirely. Compute the class fragment inline using a tiny macro that returns a space-prefixed class name when the values match.

{% macro when_current(var, val) %}
  {%- if var == val %} current-page{% endif -%}
{% endmacro %}
<a class="nav-button{{ when_current(section, 'Home') }}" href="/">Home</a>
<a class="nav-button{{ when_current(section, 'About') }}" href="/about.html">About</a>

The hyphen in {%- and -%} trims surrounding newlines so the output stays on one line. Note the leading space before current-page so it doesn’t glue to nav-button.

Template inheritance gotcha: define first, then extend

When a child template needs to provide a variable that the parent reads, define it before the extends statement. Placing the assignment after extends can prevent the parent from seeing it.

{% set section = 'Home' %}
{% extends "base.html" %}

Doing it the other way around can leave section undefined at the time the parent evaluates its blocks.

Minimal runnable check in pure Jinja2

If you want to verify the behavior outside a static site generator, you can render a string template with jinja2 directly.

import jinja2
env = jinja2.Environment()
tpl_text = """
{% set state = namespace() %}
{% set state.home = '<a class="nav-button" href="/">Home</a>' %}
{% set state.about = '<a class="nav-button" href="/about.html">About</a>' %}
{% macro activate(sec) %}
  {% if sec == 'Home' %}
  {% set state.home = '<a class="nav-button current-page" href="/">Home</a>' %}
  {% elif sec == 'About' %}
  {% set state.about = '<a class="nav-button current-page" href="/about.html">About</a>' %}
  {% endif %}
{% endmacro %}
{% block nav %}
{{ activate(section) }}
<nav>
  {{ state.home }}
  {{ state.about }}
</nav>
{% endblock %}
"""
tmpl = env.from_string(tpl_text)
print(tmpl.render(section="Home"))

Why this matters

Jinja macro scoping is easy to overlook because the syntax looks like Python, but the variable rules are closer to function-local behavior. If you treat macro assignments as global mutations, templates will silently render stale values. Understanding when to use namespace() versus when to compute strings inline keeps navbars, breadcrumbs, and other UI highlights reliable across Pelican/Jinja sites.

Wrap-up

If you must mutate shared values from a macro, funnel those values through a namespace and assign to its attributes. Call the macro with the real variable, not a quoted "{{ ... }}" string. If mutation is unnecessary, prefer a small class-computing macro that returns the class fragment inline. And when using template inheritance, set the variables the base expects before you extend it. These patterns make “current page” highlights predictable and keep your templates easy to extend.