pythonhtmljinja2pelican

Can I use Jinja macros and variables to dynamically change navbar classes in a template?


I made a website with HTML and CSS that I now want to move into Pelican/Jinja templates. I want the navbar to automatically highlight the current section when the page is generated.

For example, on the "Masthead" page (which is under "About"), I want the "About" tab in the navbar to have a different color. Previously, I did this by adding a CSS class (and a <div>) manually. Now I want to do it more efficiently using Jinja macros and variables. I also want to add another inside current-page nav-buttons.

Here’s the relevant part of base.html:

{% block nav %}
<nav>
  {% set btnH = '<a class="nav-button" href="/">Home</a>'%}
  {% set btnA = '<a class="nav-button" href="/about.html">About</a>'%}

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

And in index.html:

{% 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 %}

But when I run it, the navbar just shows:

<a class="nav-button" href="/">Home</a>
<a class="nav-button" href="/about.html">About</a>

No changed classes or added <div>s. The rest of the template works fine.

Why isn’t this working? What am I misunderstanding about Jinja macros or variable scoping?


Solution

  • Problem is that macro treats it as local variable (similar to varaibles in functions in Python)

    It needs to use namespace() and keep values in this object

    {% set ns = namespace() %}
    
    {% set ns.btnH = '<a class="nav-button" href="/">Home</a>' %}
    {% set ns.btnA = '<a class="nav-button" href="/about.html">About</a>' %}
    

    and use them in macro

    {% macro set_curr(section) %}
      {% if section == 'Home' %}
         {% set ns.btnH = '<a class="nav-button current-page" href="/">Home</a>' %}
      {% elif section == 'About' %}
         {% set ns.btnB = '<a class="nav-button current-page" href="/about.html">About</a>' %}
      {% endif %}
    {% endmacro %}
    

    and use them to generate HTML

    <nav>
      {{ ns.btnH }}
      {{ ns.btnA }}
    </nav>
    

    It has to be {{ set_curr(section) }} instead of {{ set_curr("{{ section }}") }}


    I found namespace in this answer for question flask - Jinja2: local/global variable - Stack Overflow


    Minimal working code for tests using jinja2

    import jinja2
    
    env = jinja2.Environment()
    
    text = """
    {% set ns = namespace() %}
    
    {% set ns.btnH = '<a class="nav-button" href="/">Home</a>'%}
    {% set ns.btnA = '<a class="nav-button" href="/about.html">About</a>'%}
    
    {% macro set_curr(section) %}
      {% if section == 'Home' %}
      {% set ns.btnH = '<a class="nav-button current-page" href="/">Home</a>'%}
      {% elif section == 'About' %}
      {% set ns.btnB = '<a class="nav-button current-page" href="/about.html">About</a>'%}
      {% endif %}
    {% endmacro %}
    
    {% block nav %}
    {{ set_curr(section) }}
    <nav>
      {{ ns.btnH }}
      {{ ns.btnA }}
    </nav>
    {% endblock %}
    """
    
    template = env.from_string(text)
    result = template.render(section="Home")
    print(result)
    

    By The Way:

    Frankly, I would use different macro which could work with any number of buttons without changes in macro.

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

    There is - in {%- and -%} to put value in the same line and skip new lines at both sides.
    There has to be space before current-page to separate it from nav-button.


    I made example with more universal version txt(variable, value, text) which can set any text when variable == value, and I used it to create cur(variable, value) which sets current-page.

    import jinja2
    
    env = jinja2.Environment()
    
    text = """
    {% macro txt(variable, value, text) %}
      {%- if variable == value %}{{ text }}{% endif -%}
    {% endmacro %}
    
    {% macro cur(variable, value) %}
      {{- txt(variable, value, " current-page") -}}
    {% endmacro %}
    
    {% block nav %}
    <nav>
      <a class="nav-button{{ cur(section, "Home") }}" href="/">Home</a>
      <a class="nav-button{{ cur(section, "About") }}" href="/about.html">About</a>
    </nav>
    {% endblock %}
    """
    
    template = env.from_string(text)
    result = template.render(section="Home")
    print(result)
    

    Macros are so short you can put every macro in one line.
    And then they don't need -.

    {% macro txt(variable, value, text) %}{% if variable == value %}{{ text }}{% endif %}{% endmacro %}
    
    {% macro cur(variable, value) %}{{ txt(variable, value, " current-page") }}{% endmacro %}