diff --git a/core/seo.py b/core/seo.py index 9f1061f..abd2183 100644 --- a/core/seo.py +++ b/core/seo.py @@ -5,6 +5,7 @@ from typing import TypedDict class SeoMeta(TypedDict, total=False): """Shared typed optional SEO metadata for template context generation.""" + page_url: str | None page_image: str | None page_image_width: int | None page_image_height: int | None diff --git a/core/views.py b/core/views.py index 2cd6d97..797a0a5 100644 --- a/core/views.py +++ b/core/views.py @@ -73,6 +73,7 @@ DEFAULT_SITE_DESCRIPTION = "Archive of Twitch drops, campaigns, rewards, and mor def _build_seo_context( # noqa: PLR0913, PLR0917 page_title: str = "ttvdrops", page_description: str | None = None, + page_url: str | None = None, page_image: str | None = None, page_image_width: int | None = None, page_image_height: int | None = None, @@ -89,6 +90,7 @@ def _build_seo_context( # noqa: PLR0913, PLR0917 Args: page_title: Page title (shown in browser tab, og:title). page_description: Page description (meta description, og:description). + page_url: Canonical absolute URL for the current page. page_image: Image URL for og:image meta tag. page_image_width: Width of the image in pixels. page_image_height: Height of the image in pixels. @@ -115,6 +117,8 @@ def _build_seo_context( # noqa: PLR0913, PLR0917 "og_type": og_type, "robots_directive": robots_directive, } + if page_url: + context["page_url"] = page_url if page_image: context["page_image"] = page_image if page_image_width and page_image_height: @@ -437,6 +441,7 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse: seo_context: dict[str, Any] = _build_seo_context( page_title="Twitch RSS Feeds", page_description="RSS feeds for Twitch drops.", + page_url=request.build_absolute_uri(reverse("core:docs_rss")), ) return render( request, @@ -836,6 +841,7 @@ def search_view(request: HttpRequest) -> HttpResponse: seo_context: dict[str, Any] = _build_seo_context( page_title=page_title, page_description=page_description, + page_url=request.build_absolute_uri(reverse("core:search")), ) return render( request, diff --git a/kick/views.py b/kick/views.py index 33066f4..f87fb08 100644 --- a/kick/views.py +++ b/kick/views.py @@ -56,6 +56,8 @@ def _build_seo_context( "robots_directive": "index, follow", } if seo_meta: + if seo_meta.get("page_url"): + context["page_url"] = seo_meta["page_url"] if seo_meta.get("og_type"): context["og_type"] = seo_meta["og_type"] if seo_meta.get("robots_directive"): @@ -225,6 +227,7 @@ def campaign_list_view(request: HttpRequest) -> HttpResponse: seo_context: dict[str, str] = _build_seo_context( page_title=title, page_description="Browse Kick drop campaigns.", + seo_meta={"page_url": request.build_absolute_uri(base_url)}, ) return render( request, diff --git a/templates/includes/meta_tags.html b/templates/includes/meta_tags.html index 32a0b5f..a8e1797 100644 --- a/templates/includes/meta_tags.html +++ b/templates/includes/meta_tags.html @@ -16,9 +16,6 @@ {# - robots_directive: str - robots meta content (default: "index, follow") #} {# #} {% load static %} -{# Preconnect to external resources for performance #} - - {# Description meta tag #} @@ -26,7 +23,7 @@ {# Author and Copyright #} - + {# Open Graph tags for social sharing #} @@ -45,10 +42,14 @@ {% endif %} {# Twitter Card tags for rich previews #} + content="{% if page_image %} + summary_large_image + {% else %} + summary + {% endif %}" /> + content="{% firstof page_description 'ttvdrops - Twitch and Kick drops.' %}" /> {% if page_image %}{% endif %} {# Article dates for content pages #} {% if published_date %}{% endif %} @@ -61,6 +62,22 @@ {% for link in pagination_info %}{% endfor %} {% endif %} {# Schema.org JSON-LD structured data #} -{% if schema_data %}{% endif %} +{% if schema_data %} + +{% endif %} {# Breadcrumb schema #} -{% if breadcrumb_schema %}{% endif %} +{% if breadcrumb_schema %} + +{% endif %} diff --git a/twitch/views.py b/twitch/views.py index e797cb3..b21cc71 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -116,6 +116,8 @@ def _build_seo_context( "robots_directive": "index, follow", } if seo_meta: + if seo_meta.get("page_url"): + context["page_url"] = seo_meta["page_url"] if seo_meta.get("og_type"): context["og_type"] = seo_meta["og_type"] if seo_meta.get("robots_directive"): @@ -483,6 +485,7 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0 page_title=title, page_description=description, seo_meta={ + "page_url": request.build_absolute_uri(base_url), "pagination_info": pagination_info, "schema_data": collection_schema, }, @@ -1336,6 +1339,7 @@ def reward_campaign_list_view(request: HttpRequest) -> HttpResponse: page_title=title, page_description=description, seo_meta={ + "page_url": request.build_absolute_uri(base_url), "pagination_info": pagination_info, "schema_data": collection_schema, }, @@ -1557,6 +1561,7 @@ class ChannelListView(ListView): page_title="Twitch Channels", page_description="List of Twitch channels participating in drop campaigns.", seo_meta={ + "page_url": self.request.build_absolute_uri(base_url), "pagination_info": pagination_info, "schema_data": collection_schema, },