Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions openlibrary/core/jinja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from functools import cache as functools_cache
from pathlib import Path


@functools_cache
def get_jinja_env():
"""Lazily initialize and return the Jinja2 environment (cached after first call).

Per Jinja docs, a single Environment instance should be reused to take
advantage of template compilation caching.
"""
from jinja2 import Environment, FileSystemLoader, StrictUndefined

from openlibrary.i18n import gettext as _

env = Environment(
loader=FileSystemLoader(Path(__file__).resolve().parents[1] / "macros"),
autoescape=True,
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
)
env.globals["_"] = _
return env
4 changes: 1 addition & 3 deletions openlibrary/fastapi/partials.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ async def search_facets_partial(


@router.get("/partials/AffiliateLinks.json", include_in_schema=SHOW_PARTIALS_IN_SCHEMA)
async def affiliate_links_partial(
data: Annotated[str, Query(description="JSON-encoded data with book information")],
) -> dict:
async def affiliate_links_partial(data: Annotated[str, Query(description="JSON-encoded data with book information")]) -> dict:
"""
Get affiliate links HTML for a book.

Expand Down
33 changes: 33 additions & 0 deletions openlibrary/macros/AffiliateLinks.html.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% macro affiliate_link(store) %}
<li class="prices-{{ store.key }}">
Comment thread
Sanket17052006 marked this conversation as resolved.
Comment thread
Sanket17052006 marked this conversation as resolved.
Comment thread
Sanket17052006 marked this conversation as resolved.
Comment thread
Sanket17052006 marked this conversation as resolved.
Comment thread
Sanket17052006 marked this conversation as resolved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WCAG 1.3.1: <li> is not contained in a <ul>, <ol>, or <menu>.

<li> elements must be contained in a <ul>, <ol>, or <menu>.

Details

List items (<li>) only have semantic meaning inside a list container (<ul>, <ol>, or <menu>). Outside of these containers, assistive technologies cannot convey the list relationship. Wrap <li> elements in the appropriate list container.

<a href="{{ store.link }}"
title="{{ _('Look for this edition for sale at %(store)s', store=store.name) }}"
data-ol-link-track="BuyLink|{{ store.analytics_key }}"
target="_blank" rel="noopener noreferrer">{{ store.name }}</a>
{% if store.price %}
<br>
<span name="price">{{ store.price }}{{ store.price_note }}</span>
{% endif %}
</li>
{% endmacro %}

<span class="affiliate-links-section">
<ul class="buy-options-table">
Comment thread
Sanket17052006 marked this conversation as resolved.
Comment thread
Sanket17052006 marked this conversation as resolved.
Comment thread
Sanket17052006 marked this conversation as resolved.
Comment thread
Sanket17052006 marked this conversation as resolved.
Comment thread
Sanket17052006 marked this conversation as resolved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WCAG 1.3.1: <ul> contains direct text content. Wrap in <li>.

<ul> and <ol> must only contain <li>, <script>, or <template> as direct children.

Details

Screen readers announce list structure ('list with 5 items') based on proper markup. Placing non-<li> elements directly inside <ul> or <ol> breaks this structure. Wrap content in <li> elements, or if you need wrapper divs for styling, restructure your CSS to style the <li> elements directly.

{% for store in primary_stores %}
{{ affiliate_link(store) | safe }}
{% endfor %}
{% if more_stores %}
<li class="more">
<details>
<summary>{{ _('More') }}</summary>
<ul>
{% for store in more_stores %}
{{ affiliate_link(store) | safe }}
{% endfor %}
</ul>
</details>
</li>
{% endif %}
</ul>
<small>{{ _('When you buy books using these links the Internet Archive may earn a <a class="nostyle" href="/help/faq/about#selling">small commission</a>.') | safe }}</small>
</span>
9 changes: 7 additions & 2 deletions openlibrary/plugins/openlibrary/partials.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from openlibrary.core import cache
from openlibrary.core.fulltext import fulltext_search_async
from openlibrary.core.helpers import affiliate_id
from openlibrary.core.jinja import get_jinja_env
from openlibrary.core.lending import compose_ia_url, get_available_async
from openlibrary.core.vendors import (
BetterWorldBooksMetadata,
Expand Down Expand Up @@ -302,6 +303,7 @@ async def generate_async(
asin: str | None,
prices: bool,
) -> dict:

bwb_metadata = None
amz_metadata = None
should_fetch_prices = not is_bot() and prices
Expand All @@ -317,8 +319,11 @@ async def generate_async(

primary_stores = build_primary_stores(ctx)
more_stores = build_more_stores(ctx)
macro = web.template.Template.globals["macros"].AffiliateLinks(primary_stores, more_stores)
return {"partials": str(macro)}

template = get_jinja_env().get_template("AffiliateLinks.html.jinja")
html = template.render(primary_stores=primary_stores, more_stores=more_stores)

return {"partials": html}


class SearchFacetsPartial:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ httpx==0.28.1
ijson==3.5.0
internetarchive==5.8.0
isbnlib==3.10.14
Jinja2==3.1.6
luqum==0.11.0
lxml==6.1.0
multipart==1.3.1
Expand Down