-
-
Notifications
You must be signed in to change notification settings - Fork 2k
jinja
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.
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 |
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.
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.
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.
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.
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)with{{ x | force_escape }}. 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.
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")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 extractRecent conversions (/openlibrary/templates/search/work_search_facets.html.jinja, /openlibrary/templates/reading_goals/reading_goal_progress.html.jinja) showed these pitfalls. They do not overlap with the patterns above — they are about how to structure the new Jinja code, not what Jinja can express.
-
Use a macro for a reusable piece. If the HTML needs inputs like
goalandyear, define{% macro goal_form(goal, year) %}in/openlibrary/templates/reading_goals/reading_goal_form.html.jinjaand call it with{{ goal_form(entry.goal, entry.year) }}. Do not use{% set %}plus{% include %}to pass the inputs — it hides the interface. -
Do not pollute the outer scope.
{% set goal = entry.goal %}before an{% include %}stays for the next loop iteration. Use{% with goal=entry.goal %}{% include %}{% endwith %}or a macro call. The macro form is preferred. -
Do not put code at the top of a macro file. The environment in
/openlibrary/core/jinja.pyusesStrictUndefined. Top-level code like{{ goal_form(goal, year) }}runs when another template does{% from "/openlibrary/templates/reading_goals/reading_goal_form.html.jinja" import goal_form %}and fails withyear is undefined. Keep macro files to definitions only. -
Do not make a thin Python wrapper. If the helper only does
return render_jinja_template("/openlibrary/templates/reading_goals/yearly_goal_dialog.html.jinja", year=year), delete it and callrender_jinja_templatedirectly in the Templetor file that needs it, for example/openlibrary/templates/account/view.html. -
Make the unused-template check pass. New
*.html.jinjafiles must begit added and referenced by a quoted literalrender_jinja_template("/openlibrary/templates/reading_goals/yearly_goal_dialog.html.jinja", ...)or{% from "/openlibrary/templates/reading_goals/reading_goal_form.html.jinja" import goal_form %}. Otherwisescripts/check_unused_templates.pyreports them as unused. -
Update the translation template when you move strings. When you move
_()from a template to Python, rundocker compose run --rm home python ./scripts/i18n-messages extractand verify/openlibrary/i18n/messages.pothas no extra leading or trailing whitespace. -
Use a data helper, not a render helper. For a list, make one helper that collects data.
ReadingGoalProgressPartialin/openlibrary/plugins/openlibrary/partials.pycollectsYearlyGoaland callsrender_jinja_template("reading_goals/reading_goal_progress.html.jinja", entries=entries). For the carousel,get_carousel_card_data()returns adict. ThenCarouselCardPartial.generate_async()and/openlibrary/templates/books/custom_carousel.htmlcallrender_jinja_template(..., **data). Do not make@public def render_carousel_card()that callsget_jinja_env().get_template().render(). -
Do not split a tag across
if/else. Write<img {% if lazy %} data-lazy="..." src="placeholder" {% else %} src="..." {% endif %} />inside one tag. Do not close the tag in each branch. This keepstest_all_jinja_templates_render_valid_htmlin/openlibrary/tests/core/test_jinja.pyvalid. -
Keep macro files short. Use one or two lines for the header comment, for example
{# Renders loan expiry if present. #}. Do not copy the old Templetor source into the header. Keep macros to one line where possible; see/openlibrary/macros/TruncateString.html.jinjafor an example./openlibrary/core/jinja.pyalready setstrim_blocksandlstrip_blocks, so{{-/{%-on each line is not necessary.
-
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.