Repository navigation
Convert remaining 6 partials to JinjaΒ #13566
Description
Problem / Opportunity
Some partial endpoints still render Templetor templates. Jinja is the target template language. This matters for two audiences. Developers gain more templates in good shape for the FastAPI migration. Contributors gain clean Jinja examples in the codebase. Success means 6 handlers render Jinja with zero Templetor left in their render paths, and parsed page output stays identical.
Proposal
Convert the last 6 partials, one PR per partial, easiest first. Follow AffiliateLinksPartial and these merged reference PRs: #13554 (related subjects), #13556 (publishing history), #13563 (dropper lists). Each PR adds .jinja templates, renders them from the handler, deletes the old Templetor files, and switches ALL callers of each deleted file in the same PR. No behavior change. No new tests.
Parsed DOM sameness is sufficient. Byte sameness is not needed. Two byte differences always occur: djLint puts attributes on separate lines, and quotes encode as " (Jinja) not " (Templetor). Both forms show the same page.
Breakdown
Implementation details for maintainers.
Related files
openlibrary/fastapi/partials.py(endpoint routes)openlibrary/plugins/openlibrary/partials.py(handlers)openlibrary/core/jinja.py(shared env, helper rules)- Templates per task below.
Full Jinja rule
Each PR must leave zero Templetor in its render path. Convert each nested template and macro too. Shared units give two options: convert the unit and switch ALL its callers in the same PR, or bridge it with render_templetor_template as a stopgap. The per-task notes state the advised option. scripts/check_unused_templates.py must pass.
Playbook
- Pre-encode JSON in Python with
json_encode. Pass the string to the template. |safematches Templetor$:(raw output). Plain{{ }}matches$(escaped output).- Pass one-off helpers as render kwargs (see
core/jinja.pyguidance). Do not add globals for one template. - The env uses
StrictUndefined. Pass each kwarg. Add|default(...)guards where the old template had defaults. - Check each JS consumer.
datasetdecodes entities, so the quote-code difference is safe. But check whether JS parses JSON or uses the raw string. Prove each case. hasattrguards that serve 2 data shapes (Thing and dict) need care in Jinja. Test both shapes.$codeblocks that call Solr or the DB must move to Python, or pass as helpers. Templates must not do IO.- Test with fixtures: on/off states, empty lists, hostile strings (
<script>,&, quotes). Render old and new. Compare the parsed DOM, not raw bytes. - Test each endpoint while logged in (dev user
openlibrary/openlibrary). Runpre-commit run --files ...on touched files. Runtest_templates.pyandtest_jinja.py.
Tasks (easiest first)
1. ReadingGoalProgressPartial β easiest, 3 files. Convert reading_goals/reading_goal_progress.html (29 lines), reading_goals/reading_goal_form.html (24 lines), native_dialog.html (15 lines). The form and the dialog are also used by account/view.html: switch those 2 call sites in the same PR, or bridge them. Endpoint: GET /partials/ReadingGoalProgress.json. Data is only ints and strings. No JSON. No JS contract.
2. BookPageListsPartial β easy-medium, 3 files. Convert lists/carousel.html (14 lines) and lists/list_follow.html (82 lines). Bridge (do not convert) follow/follow.html (32 lines): it has 3 other callers (type/user/view.html, account/follows.html, account/view.html). Endpoint: GET /partials/BPListsSection.json?workId=...&editionId=.... The template calls the DB per list (convert_list, get_patron_showcase, get_user_object): move these calls to Python or pass them as helpers.
3. FullTextSuggestionsPartial β medium, 3 files. Convert macros/FulltextSearchSuggestion.html (34 lines), macros/FulltextSearchSuggestionItem.html (90 lines), macros/FulltextSuggestionSnippet.html (26 lines). All 3 are dedicated to this partial. Bridge (do not convert) LoadingIndicator (13 lines, 10 other callers). Use the Jinja icon global where the old code calls macros.icon. Endpoint: GET /partials/FulltextSearchSuggestion.json. The handler returns a has_error flag beyond HTML: keep that contract exact.
4. CarouselCardPartial β hard, 3 files plus 1 bridge. Convert books/custom_carousel_card.html (101 dense lines), macros/TruncateString.html (3 lines), macros/FormatExpiry.html (10 lines); switch the few other callers of the 2 leaf macros in the same PR. Bridge (do not convert) macros/LoanStatus.html (183 lines, 8 other callers): it deserves its own issue later. Endpoint: GET /partials/CarouselLoadMore.json. Catches: 7 cover fallbacks, loan states, $request.fullpath, plural forms, JSON data attributes. hasattr guards serve 2 data shapes: test both. The card template is shared with books/custom_carousel.html. The handler renders it N times per request. Do this task before task 5.
5. LazyCarouselPartial β very hard, 3 files. Convert macros/RawQueryCarousel.html (76 lines) and books/custom_carousel.html (58 lines). Reuse the card from task 4 (do task 4 first). Bridge (do not convert) LoadingIndicator. Switch the caller: macros/QueryCarousel.html (Templetor) must call the new Jinja macro through render_jinja_template; that file stays Templetor (out of scope). Endpoint: GET /partials/LazyCarousel.json. The macro serves all site carousels, plus a cached path. The template rewrites its own query (safe_mode): move that to Python. The template calls Solr when data is absent: move that to Python.
6. SearchFacetsPartial β hardest, 2 files, do last. Convert search/work_search_facets.html (91 lines) and search/work_search_selected_facets.html (113 lines). No nested templates. Sub-$def blocks become Jinja macros in the same file. Endpoint: GET /partials/SearchFacets.json. Helper closures (add_facet_url, del_facet_url, build_other_chips) must move to Python: pass changequery, get_facet_map, and friends as render kwargs. Triple return contract: sidebar, title, active facets. Title needs entity_decode for document.title. JS reads 3 data attributes. The templates also serve the search page itself, not only the API. Consider one PR per template.
Requirements Checklist
- 6 handlers render Jinja. Zero Templetor remains in these 6 render paths (bridged shared units excepted, listed above).
- No behavior change. Parsed DOM stays identical.
- Existing tests pass (
make test-py-uv). No new tests. - Pre-commit passes on each PR (djLint, ruff, mypy, generate-pot, missing-i18n, unused-templates).
Stakeholders
Lead: @RayBB. The 6 tasks suit contributors after the 3 merged reference PRs above. Tasks 1-3 fit newer contributors. Tasks 4-6 need staff review (blast radius: carousels, search).
Instructions for Contributors
- Before creating a new branch or pushing up changes to a PR, please first run these commands to ensure your repository is up to date, as the pre-commit bot may add commits to your PRs upstream.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
openlibrary-bot commentedon Sep 6, 2026
Context
master(7d9d2cb): none of the 6 handlers have started conversion yet βReadingGoalProgressPartial,CarouselCardPartial, etc. inopenlibrary/plugins/openlibrary/partials.pystill call the legacyrender_template(...)(Templetor) path, e.g. L63-L71 and L136-L142.master, including the macro files, which actually live underopenlibrary/macros/rather thanopenlibrary/templates/macros/(e.g.FulltextSearchSuggestion.htmlis 34 lines,LoanStatus.htmlis 183 lines,RawQueryCarousel.htmlis 76 lines) β just flagging the directory name in case it trips up a contributor searching by the path implied in the issue text.LoanStatus.htmlhas "8 callers." Grepping currentmasterturns up 3 other call sites besidescustom_carousel_card.htmlitself:databarWork.html,SearchResultsWork.html, andedition-sort.html. Doesn't change the bridge-not-convert recommendation, just the stated scope of "its own issue later."render_templetor_templatebridge helper and theiconJinja global referenced in the playbook both exist already inopenlibrary/core/jinja.py#L89-L113, andmacros.iconis indeed called twice inFulltextSearchSuggestion.htmlas task 3 describes.Full triage checklist
Note
This comment was automatically generated by Richy, Open Library's issue enrichment assistant, on behalf of @mekarpeles. This is NOT a sign-off. This issue needs lead review before work begins.
RayBB commentedon Sep 16, 2026
Nice work everyone!