Refactor pagination to support multiple pagination links

This commit is contained in:
Joakim Hellsén 2026-02-12 04:22:01 +01:00
commit f004307c9c
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
4 changed files with 49 additions and 42 deletions

View file

@ -63,6 +63,7 @@
"rewardcampaign",
"runserver",
"sendgrid",
"sitelinks",
"sitewide",
"speculationrules",
"staticfiles",

View file

@ -54,15 +54,7 @@
href="{% firstof page_url request.build_absolute_uri %}" />
{# Pagination links (for crawler efficiency) #}
{% if pagination_info %}
{% if pagination_info.rel == "prev" %}
<link rel="prev" href="{{ pagination_info.url }}" />
{% elif pagination_info.rel == "next" %}
<link rel="next" href="{{ pagination_info.url }}" />
{% elif pagination_info.rel == "first" %}
<link rel="first" href="{{ pagination_info.url }}" />
{% elif pagination_info.rel == "last" %}
<link rel="last" href="{{ pagination_info.url }}" />
{% endif %}
{% for link in pagination_info %}<link rel="{{ link.rel }}" href="{{ link.url }}" />{% endfor %}
{% endif %}
{# Schema.org JSON-LD structured data #}
{% if schema_data %}<script type="application/ld+json">{{ schema_data|safe }}</script>{% endif %}

View file

@ -938,13 +938,12 @@ class TestSEOHelperFunctions:
paginator: Paginator[int] = Paginator(items, 10)
page: Page[int] = paginator.get_page(1)
info: dict[str, str] | None = _build_pagination_info(request, page, "/campaigns/")
info: list[dict[str, str]] | None = _build_pagination_info(request, page, "/campaigns/")
assert info is not None
assert "url" in info
assert "rel" in info
assert info["rel"] == "next"
assert "page=2" in info["url"]
assert len(info) == 1
assert info[0]["rel"] == "next"
assert "page=2" in info[0]["url"]
def test_build_pagination_info_with_prev_page(self) -> None:
"""Test _build_pagination_info extracts prev page URL."""
@ -955,13 +954,14 @@ class TestSEOHelperFunctions:
paginator: Paginator[int] = Paginator(items, 10)
page: Page[int] = paginator.get_page(2)
info: dict[str, str] | None = _build_pagination_info(request, page, "/campaigns/")
info: list[dict[str, str]] | None = _build_pagination_info(request, page, "/campaigns/")
assert info is not None
assert "url" in info
assert "rel" in info
assert info["rel"] == "prev"
assert "page=1" in info["url"]
assert len(info) == 2
assert info[0]["rel"] == "prev"
assert "page=1" in info[0]["url"]
assert info[1]["rel"] == "next"
assert "page=3" in info[1]["url"]
@pytest.mark.django_db

View file

@ -98,7 +98,7 @@ def _build_seo_context( # noqa: PLR0913, PLR0917
og_type: str = "website",
schema_data: dict[str, Any] | None = None,
breadcrumb_schema: dict[str, Any] | None = None,
pagination_info: dict[str, str] | None = None,
pagination_info: list[dict[str, str]] | None = None,
published_date: str | None = None,
modified_date: str | None = None,
robots_directive: str = "index, follow",
@ -112,7 +112,7 @@ def _build_seo_context( # noqa: PLR0913, PLR0917
og_type: OpenGraph type (e.g., "website", "article").
schema_data: Dict representation of Schema.org JSON-LD data.
breadcrumb_schema: Breadcrumb schema dict for navigation hierarchy.
pagination_info: Dict with "rel" (prev|next|first|last) and "url".
pagination_info: List of dicts with "rel" (prev|next|first|last) and "url".
published_date: ISO 8601 published date (e.g., "2025-01-01T00:00:00Z").
modified_date: ISO 8601 modified date.
robots_directive: Robots meta content (e.g., "index, follow" or "noindex").
@ -173,7 +173,7 @@ def _build_pagination_info(
request: HttpRequest,
page_obj: Page,
base_url: str,
) -> dict[str, str] | None:
) -> list[dict[str, str]] | None:
"""Build pagination link info for rel="next"/"prev" tags.
Args:
@ -182,30 +182,30 @@ def _build_pagination_info(
base_url: Base URL for pagination (e.g., "/campaigns/?status=active").
Returns:
Dict with rel and url, or None if no prev/next.
List of dicts with rel and url, or None if no prev/next.
"""
pagination_info: dict[str, str] | None = None
pagination_links: list[dict[str, str]] = []
if page_obj.has_previous():
prev_url: str = f"{base_url}?page={page_obj.previous_page_number()}"
if "?" in base_url:
prev_url = f"{base_url}&page={page_obj.previous_page_number()}"
pagination_links.append({
"rel": "prev",
"url": request.build_absolute_uri(prev_url),
})
if page_obj.has_next():
next_url: str = f"{base_url}?page={page_obj.next_page_number()}"
if "?" in base_url:
# Preserve existing query params
next_url = f"{base_url}&page={page_obj.next_page_number()}"
pagination_info = {
pagination_links.append({
"rel": "next",
"url": request.build_absolute_uri(next_url),
}
})
if page_obj.has_previous():
prev_url: str = f"{base_url}?page={page_obj.previous_page_number()}"
if "?" in base_url:
prev_url = f"{base_url}&page={page_obj.previous_page_number()}"
pagination_info = {
"rel": "prev",
"url": request.build_absolute_uri(prev_url),
}
return pagination_info
return pagination_links or None
def emote_gallery_view(request: HttpRequest) -> HttpResponse:
@ -522,7 +522,7 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0
elif game_filter:
base_url += f"?game={game_filter}"
pagination_info: dict[str, str] | None = _build_pagination_info(request, campaigns, base_url)
pagination_info: list[dict[str, str]] | None = _build_pagination_info(request, campaigns, base_url)
# CollectionPage schema for campaign list
collection_schema: dict[str, str] = {
@ -1062,11 +1062,25 @@ class GameDetailView(DetailView):
campaign__game=game,
).prefetch_related("benefits")
for drop in drops:
# Materialize drops so we can iterate multiple times without extra DB hits
drops_list: list[TimeBasedDrop] = list(drops)
# Collect all benefit names that award badges
benefit_badge_titles: set[str] = set()
for drop in drops_list:
for benefit in drop.benefits.all():
if benefit.distribution_type == "BADGE" and benefit.name:
benefit_badge_titles.add(benefit.name)
# Bulk-load all matching ChatBadge instances to avoid N+1 queries
badges_by_title: dict[str, ChatBadge] = {
badge.title: badge for badge in ChatBadge.objects.filter(title__in=benefit_badge_titles)
}
for drop in drops_list:
for benefit in drop.benefits.all():
if benefit.distribution_type == "BADGE":
# Find badge by title
badge: ChatBadge | None = ChatBadge.objects.filter(title=benefit.name).first()
badge: ChatBadge | None = badges_by_title.get(benefit.name)
if badge:
drop_awarded_badges[drop.twitch_id] = badge
@ -1360,7 +1374,7 @@ def reward_campaign_list_view(request: HttpRequest) -> HttpResponse:
elif game_filter:
base_url += f"?game={game_filter}"
pagination_info: dict[str, str] | None = _build_pagination_info(request, reward_campaigns, base_url)
pagination_info: list[dict[str, str]] | None = _build_pagination_info(request, reward_campaigns, base_url)
# CollectionPage schema for reward campaigns list
collection_schema: dict[str, str | dict[str, str | dict[str, str]]] = {
@ -1800,7 +1814,7 @@ class ChannelListView(ListView):
base_url += f"?search={search_query}"
page_obj: Page | None = context.get("page_obj")
pagination_info: dict[str, str] | None = (
pagination_info: list[dict[str, str]] | None = (
_build_pagination_info(self.request, page_obj, base_url) if isinstance(page_obj, Page) else None
)