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:
| Templetor | Jinja | |
|---|---|---|
| Status | Legacy. Most current pages use it. | Preferred for new work. |
| File names | *.html | *.html.jinja |
| Source | web.py framework | jinja2 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.
| Pattern | Example | Fix |
|---|---|---|
| List or dict comprehension | [e for e in items if e] | Build the list in Python |
| Item assignment | context['k'] = v | Compute in Python; pass the value |
| Lambda function | sorted(xs, key=lambda x: x[1]) | Sort in Python |
| Type or attribute tests | isinstance(a, str), hasattr(d, 'x') | Test in Python |
| Side effects | results.append(x) | Collect results in Python |
$while loop | $while n > 0: | Jinja has no while loop. Restructure or compute in Python |
$try / $except | Error handling | Jinja has no try/except. Handle errors in Python |
Notes on loops:
$forconverts to{% for %}.loop.index,loop.first, andloop.lastexist in both engines.- Templetor
loop.parityhas no Jinja form. Use{{ loop.cycle('odd', 'even') }}. $continueand$breakneed thejinja2.ext.loopcontrolsextension. 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.
| Template | Lines | Manual-fix flags | First parse problem |
|---|---|---|---|
type/edition/view.html | 568 | 36 | Bad expression |
type/work/view.html | 568 | 36 | Bad expression |
books/edit/edition.html | 684 | 3 | Stray \ |
work_search.html | 184 | 4 | Stray ? |
diff.html | 160 | 4 | {% elif %} outside its block |
lib/nav_head.html | 137 | 0 | Bad expression |
search/work_search_selected_facets.html | 113 | 27 | Bad expression |
account/create.html | 114 | 0 | $def with argument parsing |
books/edit/excerpts.html | 103 | 0 | Bad expression |
recentchanges/render.html | 67 | 0 | Stray ' |
history/sources.html | 80 | 2 | $def with argument parsing |
widget.html | 50 | 1 | {% elif %} outside its block |
lists/export_as_bibtex.html | 48 | 0 | Bad expression |
lists/showcase.html | 45 | 0 | Stray % |
lib/exports.html | 36 | 1 | Bad expression |
site/footer.html | 29 | 0 | Stray # |
books/RelatedWorksCarousel.html | 23 | 3 | Bad expression |
subjects/notfound.html | 18 | 0 | Bad expression |
showgoogle_books.html | 12 | 0 | Bad 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.htmlrenders the header on every page. Test it with care.search/work_search_selected_facets.htmlneeds 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 withrender_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:
| Task | Templetor | Jinja |
|---|---|---|
| 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:
- Copy the
.htmlfile. Give the copy the extension.html.jinja. - Convert the syntax with the map above.
- Move logic out of the template when you can. Put it in Python. Send the results to the template as arguments.
- Keep translated English strings exactly the same. Then existing translations still match.
- Change the render call. See How to render.
- 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(). Replacewebsafe(x)with0. Do not use the built-inescapefilter here. It does nothing when autoescape is already on. Add| safeonly 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%sdoes not work. - Jinja templates get no automatic variables such as
page,user, orctx. 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:
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:
$: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:
pre-commit run --files openlibrary/templates/my_template.html.jinjaRun the Python tests:
make test-py-uvTwo test suites cover Jinja:
- Every
.html.jinjafile must compile. Seeopenlibrary/tests/test_templates.py. - One test renders every
.jinjafile with no data and checks the HTML structure. Seeopenlibrary/tests/core/test_jinja.py. Your template must render without arguments. Do not call functions on the data inside the template.
If you added or changed English strings, regenerate the POT file:
docker compose run --rm home python ./scripts/i18n-messages extractExamples
openlibrary/macros/AffiliateLinks.html.jinja— a small partial. Shows all translation patterns.openlibrary/templates/design/layout.html.jinja— a full page built from macros.openlibrary/core/jinja.py— the Jinja environment. Globals, filters, and translation setup.