diff --git a/.vscode/settings.json b/.vscode/settings.json index 1302d02..71950f9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -63,6 +63,7 @@ "rewardcampaign", "runserver", "sendgrid", + "sitelinks", "sitewide", "speculationrules", "staticfiles", diff --git a/templates/includes/meta_tags.html b/templates/includes/meta_tags.html index 68b3d97..642eef2 100644 --- a/templates/includes/meta_tags.html +++ b/templates/includes/meta_tags.html @@ -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" %} - - {% elif pagination_info.rel == "next" %} - - {% elif pagination_info.rel == "first" %} - - {% elif pagination_info.rel == "last" %} - - {% endif %} + {% for link in pagination_info %}{% endfor %} {% endif %} {# Schema.org JSON-LD structured data #} {% if schema_data %}{% endif %} diff --git a/twitch/tests/test_views.py b/twitch/tests/test_views.py index d5a99bb..170b8c1 100644 --- a/twitch/tests/test_views.py +++ b/twitch/tests/test_views.py @@ -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 diff --git a/twitch/views.py b/twitch/views.py index ca3fc11..cc57bc0 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -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 )