Skip to content

Jinja Templates

This page gives the basic facts about Jinja in Open Library. It tells you what Jinja is, why we are moving to it, how to convert a Templetor template, and how to test the result. For deep details about translated text, see the i18n guide.

What is Jinja?

Jinja is a template engine for Python. A template is a text file with placeholders. Python code sends data to the template. The engine puts the data into the placeholders and makes HTML.

Jinja is an industry standard. Many Python projects use it. Read the official documentation at https://jinja.palletsprojects.com/.

Open Library uses two template engines today:

TempletorJinja
StatusLegacy. Most current pages use it.Preferred for new work.
File names*.html*.html.jinja
Sourceweb.py frameworkjinja2 library

Why we move to Jinja

Templetor is old and rare. Only the web.py framework uses it. Few tools understand its syntax, so editors, linters, and formatters give almost no help.

Jinja is more standard and more reliable to work with:

  • Many editors, linters, and formatters support it.
  • It escapes values by default. This prevents XSS bugs.
  • Bad data stops the render with a clear error. It does not make broken HTML.

We move to Jinja over time. There is no large migration project and no deadline. The rules:

  • New UI work uses Jinja.
  • Convert an old Templetor file when you change it.

Why there is no automatic converter

We built an experimental converter for this. It lives in draft pull request #12941. It is not merged, so treat it as an experiment. But its test runs taught us what converts cleanly and what does not.

The converter processed all ~370 Templetor templates and macros. Results:

  • About 94% produced output that Jinja accepts as-is.
  • About 250 places needed manual fixes. The converter marked them with comments.
  • 22 templates did not compile even after conversion.

These numbers tell us two things. Most Templetor syntax maps cleanly to Jinja. But a tool cannot do all the work, because some patterns have no Jinja form at all. We may be able to automatically convert some simple templates one day, but we are not at that point yet. For now, a person converts each file. Treat each conversion as normal refactoring work.

Patterns with no Jinja form

Jinja cannot express these patterns. Do not try to convert them. Move the logic into Python instead, and pass the results to the template as arguments.

PatternExampleFix
List or dict comprehension[e for e in items if e]Build the list in Python
Item assignmentcontext['k'] = vCompute in Python; pass the value
Lambda functionsorted(xs, key=lambda x: x[1])Sort in Python
Type or attribute testsisinstance(a, str), hasattr(d, 'x')Test in Python
Side effectsresults.append(x)Collect results in Python
$while loop$while n > 0:Jinja has no while loop. Restructure or compute in Python
$try / $exceptError handlingJinja has no try/except. Handle errors in Python

Notes on loops:

  • $for converts to {% for %}. loop.index, loop.first, and loop.last exist in both engines.
  • Templetor loop.parity has no Jinja form. Use {{ loop.cycle('odd', 'even') }}.
  • $continue and $break need the jinja2.ext.loopcontrols extension. Our environment does not enable it. Avoid them.

Templates that fail automatic conversion

A re-run of the draft converter against this codebase in August 2026 gives the list below. These 19 templates produce output that Jinja cannot parse. The causes are small: stray characters, bad expressions, or block nesting mistakes. One error can hide another, because Jinja stops at the first error it finds. So a template can hold more failures than the table shows. Fix one error and check again until the file parses. Then finish the conversion by hand.

"Manual-fix flags" is the number of places the converter marked for manual work.

TemplateLinesManual-fix flagsFirst parse problem
type/edition/view.html56836Bad expression
type/work/view.html56836Bad expression
books/edit/edition.html6843Stray \
work_search.html1844Stray ?
diff.html1604{% elif %} outside its block
lib/nav_head.html1370Bad expression
search/work_search_selected_facets.html11327Bad expression
account/create.html1140$def with argument parsing
books/edit/excerpts.html1030Bad expression
recentchanges/render.html670Stray '
history/sources.html802$def with argument parsing
widget.html501{% elif %} outside its block
lists/export_as_bibtex.html480Bad expression
lists/showcase.html450Stray %
lib/exports.html361Bad expression
site/footer.html290Stray #
books/RelatedWorksCarousel.html233Bad expression
subjects/notfound.html180Bad expression
showgoogle_books.html120Bad expression

Tips for priority:

  • The two book pages (type/edition/view.html, type/work/view.html) are the core pages of the site. They are also the largest jobs.
  • lib/nav_head.html renders the header on every page. Test it with care.
  • search/work_search_selected_facets.html needs many manual fixes for its size.
  • The small files near the end of the table are good first conversions.

You can also try the draft converter yourself on any template. The code lives in pull request #12941. It is not merged and not supported, so treat its output as a starting point, not a finished conversion.

Expect a few cleanups in its output:

  • The converter wraps every template in a {% macro name(...) %} block. Remove this wrapper. It exists only so the output can compile as a standalone file. A real conversion renders the template directly with render_jinja_template().
  • The converter copies websafe() calls unchanged. Replace them yourself. See the escaping note below.
  • The converter can silently corrupt expressions with brackets or lists. For example, it turned $cond(name in ["a", "b"], "x", None) into invalid Jinja. Diff-check every line where it rewrote an expression.

How to convert a template

Use this syntax map:

TaskTempletorJinja
Output a value$name{{ name }}
Output raw HTML$:value{{ value | safe }}
Condition$if x: / $elif y: / $else:{% if x %} / {% elif y %} / {% else %} ... {% endif %}
Loop$for b in books:{% for b in books %} ... {% endfor %}
Assign a variable$ x = expr{% set x = expr %}
Declare parameters$def with (a, b=None)No header. Pass arguments at render time.
Comment$# note{# note #}
Literal dollar sign$$$
Inline condition$cond(x, a, b){{ a if x else b }}
Translate$_("text"){{ _('text') }} or {% trans %}text{% endtrans %}
Translate with plural$ungettext(s1, s2, n){{ ngettext(s1, s2, n) }}

Conversion steps:

  1. Copy the .html file. Give the copy the extension .html.jinja.
  2. Convert the syntax with the map above.
  3. Move logic out of the template when you can. Put it in Python. Send the results to the template as arguments.
  4. Keep translated English strings exactly the same. Then existing translations still match.
  5. Change the render call. See How to render.
  6. Run the tests. See How to test.

Know these differences before you start:

  • Jinja escapes values by default. Templetor escapes nothing until you call websafe(). Replace websafe(x) with 0. Do not use the built-in escape filter here. It does nothing when autoescape is already on. Add | safe only for HTML that you trust. When you convert $: to | safe, check the value first. If it holds user data, let Jinja escape it.
  • Translated strings need named placeholders, for example %(name)s. Positional %s does not work.
  • Jinja templates get no automatic variables such as page, user, or ctx. Pass all data as arguments.
  • Undefined variables raise an error at render time.
  • Variables set inside a {% for %} or {% if %} block do not exist after the block ends. Templetor does not have this limit. Use {% set ns = namespace(total=0) %} objects, or compute the value before the loop.
  • Jinja reads attributes with getattr first, then item lookup. So {{ d.key }} works for both objects and dicts.
  • Calls to Templetor macros ($:macros.Name(...)) do not work in Jinja. Import Jinja macros with {% from "file.html.jinja" import name %}, or render the Templetor macro from Python.

How to render a Jinja template

From Python, use the helper in openlibrary/core/jinja.py:

py
from openlibrary.core.jinja import render_jinja_template

html = render_jinja_template("interstitial.html.jinja", url=url, wait=5)

The loader looks in openlibrary/templates/ and openlibrary/macros/. Always include the file extension.

From inside a Templetor template:

html
$:render_jinja_template("my_partial.html.jinja", foo="bar")

How to test

Run pre-commit on your changed files. djLint formats and lints all .jinja files:

sh
pre-commit run --files openlibrary/templates/my_template.html.jinja

Run the Python tests:

sh
make test-py-uv

Two test suites cover Jinja:

If you added or changed English strings, regenerate the POT file:

sh
docker compose run --rm home python ./scripts/i18n-messages extract

Examples