diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2c5fb94..050a0d8 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,7 +27,7 @@ repos:
args: [--target-version, "6.0"]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.15.8
+ rev: v0.15.9
hooks:
- id: ruff-check
args: ["--fix", "--exit-non-zero-on-fix"]
diff --git a/chzzk/management/commands/import_chzzk_campaign.py b/chzzk/management/commands/import_chzzk_campaign.py
index 3ba11ed..c8944ca 100644
--- a/chzzk/management/commands/import_chzzk_campaign.py
+++ b/chzzk/management/commands/import_chzzk_campaign.py
@@ -1,13 +1,6 @@
from typing import TYPE_CHECKING
from typing import Any
-if TYPE_CHECKING:
- import argparse
-
- from chzzk.schemas import ChzzkCampaignV2
- from chzzk.schemas import ChzzkRewardV2
-
-
import requests
from django.core.management.base import BaseCommand
from django.core.management.base import CommandError
@@ -17,6 +10,12 @@ from chzzk.models import ChzzkCampaign
from chzzk.models import ChzzkReward
from chzzk.schemas import ChzzkApiResponseV2
+if TYPE_CHECKING:
+ import argparse
+
+ from chzzk.schemas import ChzzkCampaignV2
+ from chzzk.schemas import ChzzkRewardV2
+
MAX_CAMPAIGN_OUTLIER_THRESHOLD: int = 100_000_000
MAX_CAMPAIGN_OUTLIER_GAP: int = 1_000
diff --git a/config/settings.py b/config/settings.py
index dbb1285..a084435 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -167,6 +167,7 @@ TEMPLATES: list[dict[str, Any]] = [
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
+ "core.context_processors.base_url",
],
},
},
@@ -263,87 +264,3 @@ CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
BASE_URL: str = "https://ttvdrops.lovinator.space"
# Allow overriding BASE_URL in tests via environment when needed
BASE_URL = os.getenv("BASE_URL", BASE_URL)
-
-# Monkeypatch HttpRequest.build_absolute_uri to prefer BASE_URL for absolute URLs
-try:
- from django.http.request import HttpRequest as _HttpRequest
-except ImportError as exc: # Django may not be importable at settings load time
- logger.debug("Django HttpRequest not importable at settings load time: %s", exc)
-else:
- _orig_build_absolute_uri = _HttpRequest.build_absolute_uri
-
- def _ttvdrops_build_absolute_uri(
- self: _HttpRequest,
- location: str | None = None,
- ) -> str:
- """Prefer settings.BASE_URL when building absolute URIs for relative paths.
-
- This makes test output deterministic (uses https://ttvdrops.lovinator.space)
- instead of Django's test client default of http://testserver.
-
- Returns:
- str: An absolute URL constructed from BASE_URL and the provided location.
- """
- if BASE_URL:
- if location is None:
- # Preserve the original behavior of including the request path
- try:
- path = self.get_full_path()
- return BASE_URL.rstrip("/") + path
- except AttributeError as exc:
- logger.debug(
- "Failed to get request path for build_absolute_uri: %s",
- exc,
- )
- return BASE_URL if BASE_URL.endswith("/") else f"{BASE_URL}/"
- if isinstance(location, str) and location.startswith("/"):
- return BASE_URL.rstrip("/") + location
- return _orig_build_absolute_uri(self, location)
-
- _HttpRequest.build_absolute_uri = _ttvdrops_build_absolute_uri
-
- # Ensure request.is_secure reports True so syndication.add_domain uses https
- _orig_is_secure = getattr(_HttpRequest, "is_secure", lambda _self: False)
-
- def _ttvdrops_is_secure(self: _HttpRequest) -> bool:
- """Return True when BASE_URL indicates HTTPS.
-
- Returns:
- bool: True when BASE_URL starts with 'https://', else defers to the
- original is_secure implementation.
- """
- return BASE_URL.startswith("https://")
-
- _HttpRequest.is_secure = _ttvdrops_is_secure
-
-# Monkeypatch django.contrib.sites.shortcuts.get_current_site to prefer BASE_URL
-try:
- from dataclasses import dataclass
- from typing import Any
- from urllib.parse import urlsplit
-
- from django.contrib.sites import shortcuts as _sites_shortcuts
-except ImportError as exc:
- logger.debug("Django sites.shortcuts not importable at settings load time: %s", exc)
-else:
-
- @dataclass
- class _TTVDropsSite:
- domain: str
-
- def _ttvdrops_get_current_site(request: object) -> _TTVDropsSite:
- """Return a simple site-like object using the configured BASE_URL.
-
- Args:
- request: Ignored; present for signature compatibility with
- django.contrib.sites.shortcuts.get_current_site.
-
- Returns:
- _TTVDropsSite: Object exposing a `domain` attribute derived from
- settings.BASE_URL.
- """
- parts = urlsplit(BASE_URL)
- domain = parts.netloc or parts.path
- return _TTVDropsSite(domain=domain)
-
- _sites_shortcuts.get_current_site = _ttvdrops_get_current_site
diff --git a/config/tests/test_seo.py b/config/tests/test_seo.py
index 86f3e85..dcf5411 100644
--- a/config/tests/test_seo.py
+++ b/config/tests/test_seo.py
@@ -52,10 +52,10 @@ def test_meta_tags_use_request_absolute_url_for_og_url_and_canonical() -> None:
_extract_meta_content(content, "og:url")
== "https://ttvdrops.lovinator.space/drops/"
)
- assert (
- ''
- in content
+ canonical_pattern: re.Pattern[str] = re.compile(
+ r'',
)
+ assert canonical_pattern.search(content) is not None
def test_meta_tags_use_explicit_page_url_for_og_url_and_canonical() -> None:
@@ -71,10 +71,10 @@ def test_meta_tags_use_explicit_page_url_for_og_url_and_canonical() -> None:
_extract_meta_content(content, "og:url")
== "https://ttvdrops.lovinator.space/custom-page/"
)
- assert (
- ''
- in content
+ canonical_pattern: re.Pattern[str] = re.compile(
+ r'',
)
+ assert canonical_pattern.search(content) is not None
def test_meta_tags_twitter_card_is_summary_without_image() -> None:
diff --git a/core/base_url.py b/core/base_url.py
new file mode 100644
index 0000000..66efa86
--- /dev/null
+++ b/core/base_url.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
+from urllib.parse import urlsplit
+
+from django.conf import settings
+
+if TYPE_CHECKING:
+ from django.http import HttpRequest
+
+
+def _get_base_url() -> str:
+ """Get normalized BASE_URL from settings.
+
+ Returns:
+ str: The configured BASE_URL without trailing slash.
+ """
+ base_url = getattr(settings, "BASE_URL", "")
+ return base_url.rstrip("/") if base_url else ""
+
+
+def build_absolute_uri(
+ location: str | None = None,
+ request: HttpRequest | None = None,
+) -> str:
+ """Build an absolute URI via BASE_URL (preferred) or request fallback.
+
+ Args:
+ location: Relative path ('/foo/') or absolute URL.
+ request: Optional HttpRequest to resolve path when location is None.
+
+ Returns:
+ str: Fully resolved absolute URL.
+ """
+ base_url = _get_base_url()
+
+ if location is None:
+ if request is not None:
+ location = request.get_full_path()
+ else:
+ return f"{base_url}/" if base_url else "/"
+
+ parsed = urlsplit(location)
+ if parsed.scheme and parsed.netloc:
+ return location
+
+ if base_url:
+ if location.startswith("/"):
+ return f"{base_url}{location}"
+ return f"{base_url}/{location.lstrip('/')}"
+
+ if request is not None:
+ return request.build_absolute_uri(location)
+
+ return location
+
+
+def is_secure() -> bool:
+ """Return whether the configured BASE_URL uses HTTPS."""
+ base_url = _get_base_url()
+ return base_url.startswith("https://") if base_url else False
+
+
+@dataclass
+class _TTVDropsSite:
+ domain: str
+
+
+def get_current_site(request: object) -> _TTVDropsSite:
+ """Return a site-like object with domain derived from BASE_URL."""
+ base_url = _get_base_url()
+ parts = urlsplit(base_url)
+ domain = parts.netloc or parts.path
+ return _TTVDropsSite(domain=domain)
+
+
+def apply_base_url_patches() -> None:
+ """No-op; use build_absolute_uri() helper explicitly."""
+ return
diff --git a/core/context_processors.py b/core/context_processors.py
new file mode 100644
index 0000000..66476b8
--- /dev/null
+++ b/core/context_processors.py
@@ -0,0 +1,17 @@
+from typing import TYPE_CHECKING
+
+from django.conf import settings
+
+if TYPE_CHECKING:
+ from django.http import HttpRequest
+
+
+def base_url(request: HttpRequest) -> dict[str, str]:
+ """Provide BASE_URL to templates for deterministic absolute URL creation.
+
+ Returns:
+ dict[str, str]: A dictionary containing the BASE_URL.
+ """
+ return {
+ "BASE_URL": getattr(settings, "BASE_URL", ""),
+ }
diff --git a/core/tests/test_base_url.py b/core/tests/test_base_url.py
new file mode 100644
index 0000000..ee0c8c4
--- /dev/null
+++ b/core/tests/test_base_url.py
@@ -0,0 +1,25 @@
+from typing import TYPE_CHECKING
+
+from django.test import RequestFactory
+
+from core.base_url import build_absolute_uri
+from core.base_url import get_current_site
+
+if TYPE_CHECKING:
+ from config.tests.test_seo import WSGIRequest
+ from core.base_url import _TTVDropsSite
+
+
+def test_build_absolute_uri_uses_base_url() -> None:
+ """Test that build_absolute_uri uses the base URL from settings."""
+ request: WSGIRequest = RequestFactory().get("/test-path/")
+ assert (
+ build_absolute_uri(request=request)
+ == "https://ttvdrops.lovinator.space/test-path/"
+ )
+
+
+def test_get_current_site_from_base_url() -> None:
+ """Test that get_current_site returns the correct site based on the base URL."""
+ site: _TTVDropsSite = get_current_site(None)
+ assert site.domain == "ttvdrops.lovinator.space"
diff --git a/core/views.py b/core/views.py
index 2c6e8b1..c040c4e 100644
--- a/core/views.py
+++ b/core/views.py
@@ -26,6 +26,7 @@ from django.template.defaultfilters import filesizeformat
from django.urls import reverse
from django.utils import timezone
+from core.base_url import build_absolute_uri
from kick.models import KickChannel
from kick.models import KickDropCampaign
from twitch.models import Channel
@@ -507,7 +508,7 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
seo_context: dict[str, Any] = _build_seo_context(
page_title="Feed Documentation",
page_description="Documentation for the RSS feeds available on ttvdrops.lovinator.space, including how to use them and what data they contain.",
- page_url=request.build_absolute_uri(reverse("core:docs_rss")),
+ page_url=build_absolute_uri(reverse("core:docs_rss")),
)
return render(
@@ -721,7 +722,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
dataset_distributions.append({
"@type": "DataDownload",
"name": dataset["name"],
- "contentUrl": request.build_absolute_uri(
+ "contentUrl": build_absolute_uri(
reverse("core:dataset_backup_download", args=[download_path]),
),
"encodingFormat": "application/zstd",
@@ -731,9 +732,9 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
"@context": "https://schema.org",
"@type": "Dataset",
"name": "Historical archive of Twitch and Kick drop data",
- "identifier": request.build_absolute_uri(reverse("core:dataset_backups")),
+ "identifier": build_absolute_uri(reverse("core:dataset_backups")),
"temporalCoverage": "2024-07-17/..",
- "url": request.build_absolute_uri(reverse("core:dataset_backups")),
+ "url": build_absolute_uri(reverse("core:dataset_backups")),
"license": "https://creativecommons.org/publicdomain/zero/1.0/",
"isAccessibleForFree": True,
"description": (
@@ -753,7 +754,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
"includedInDataCatalog": {
"@type": "DataCatalog",
"name": "ttvdrops.lovinator.space",
- "url": request.build_absolute_uri(reverse("core:dataset_backups")),
+ "url": build_absolute_uri(reverse("core:dataset_backups")),
},
}
if dataset_distributions:
@@ -905,7 +906,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")),
+ page_url=build_absolute_uri(reverse("core:search")),
)
return render(
request,
@@ -1006,12 +1007,12 @@ def dashboard(request: HttpRequest) -> HttpResponse:
"@context": "https://schema.org",
"@type": "WebSite",
"name": "ttvdrops",
- "url": request.build_absolute_uri("/"),
+ "url": build_absolute_uri("/"),
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
- "urlTemplate": request.build_absolute_uri(
+ "urlTemplate": build_absolute_uri(
"/search/?q={search_term_string}",
),
},
diff --git a/kick/views.py b/kick/views.py
index 511447f..b8cc126 100644
--- a/kick/views.py
+++ b/kick/views.py
@@ -15,6 +15,7 @@ from django.shortcuts import render
from django.urls import reverse
from django.utils import timezone
+from core.base_url import build_absolute_uri
from kick.models import KickCategory
from kick.models import KickDropCampaign
from kick.models import KickOrganization
@@ -127,14 +128,14 @@ def _build_pagination_info(
if page_obj.has_previous():
links.append({
"rel": "prev",
- "url": request.build_absolute_uri(
+ "url": build_absolute_uri(
f"{base_url}{sep}page={page_obj.previous_page_number()}",
),
})
if page_obj.has_next():
links.append({
"rel": "next",
- "url": request.build_absolute_uri(
+ "url": build_absolute_uri(
f"{base_url}{sep}page={page_obj.next_page_number()}",
),
})
@@ -239,7 +240,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)},
+ seo_meta={"page_url": build_absolute_uri(base_url)},
)
return render(
request,
@@ -291,20 +292,20 @@ def campaign_detail_view(request: HttpRequest, kick_id: str) -> HttpResponse:
channels: list[KickChannel] = list(campaign.channels.select_related("user"))
breadcrumb_schema: str = _build_breadcrumb_schema([
- {"name": "Home", "url": request.build_absolute_uri("/")},
+ {"name": "Home", "url": build_absolute_uri("/")},
{
"name": "Kick Campaigns",
- "url": request.build_absolute_uri(reverse("kick:campaign_list")),
+ "url": build_absolute_uri(reverse("kick:campaign_list")),
},
{
"name": campaign.name,
- "url": request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse("kick:campaign_detail", args=[campaign.kick_id]),
),
},
])
- campaign_url: str = request.build_absolute_uri(
+ campaign_url: str = build_absolute_uri(
reverse("kick:campaign_detail", args=[campaign.kick_id]),
)
campaign_event: dict[str, Any] = {
@@ -431,20 +432,20 @@ def category_detail_view(request: HttpRequest, kick_id: int) -> HttpResponse:
]
breadcrumb_schema: str = _build_breadcrumb_schema([
- {"name": "Home", "url": request.build_absolute_uri("/")},
+ {"name": "Home", "url": build_absolute_uri("/")},
{
"name": "Kick Games",
- "url": request.build_absolute_uri(reverse("kick:game_list")),
+ "url": build_absolute_uri(reverse("kick:game_list")),
},
{
"name": category.name,
- "url": request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse("kick:game_detail", args=[category.kick_id]),
),
},
])
- category_url: str = request.build_absolute_uri(
+ category_url: str = build_absolute_uri(
reverse("kick:game_detail", args=[category.kick_id]),
)
category_schema: dict[str, Any] = {
@@ -536,20 +537,20 @@ def organization_detail_view(request: HttpRequest, kick_id: str) -> HttpResponse
)
breadcrumb_schema: str = _build_breadcrumb_schema([
- {"name": "Home", "url": request.build_absolute_uri("/")},
+ {"name": "Home", "url": build_absolute_uri("/")},
{
"name": "Kick Organizations",
- "url": request.build_absolute_uri(reverse("kick:organization_list")),
+ "url": build_absolute_uri(reverse("kick:organization_list")),
},
{
"name": org.name,
- "url": request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse("kick:organization_detail", args=[org.kick_id]),
),
},
])
- org_url: str = request.build_absolute_uri(
+ org_url: str = build_absolute_uri(
reverse("kick:organization_detail", args=[org.kick_id]),
)
organization_node: dict[str, Any] = {
diff --git a/templates/includes/meta_tags.html b/templates/includes/meta_tags.html
index d90c294..8715692 100644
--- a/templates/includes/meta_tags.html
+++ b/templates/includes/meta_tags.html
@@ -32,7 +32,7 @@
content="{% firstof page_description 'ttvdrops - Track Twitch drops.' %}" />
+ content="{% if page_url %}{{ page_url }}{% else %}{{ BASE_URL }}{{ request.get_full_path }}{% endif %}" />
{% if page_image %}
{% if page_image_width and page_image_height %}
@@ -41,7 +41,8 @@
{% endif %}
{% endif %}
{# Twitter Card tags for rich previews #}
-
+
@@ -50,20 +51,13 @@
{% if published_date %}{% endif %}
{% if modified_date %}{% endif %}
{# Canonical tag #}
-
+
{# Pagination links (for crawler efficiency) #}
{% if pagination_info %}
{% 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/feeds.py b/twitch/feeds.py
index dda8a71..b06c02b 100644
--- a/twitch/feeds.py
+++ b/twitch/feeds.py
@@ -1,9 +1,13 @@
import logging
import re
+from collections.abc import Callable
from typing import TYPE_CHECKING
from typing import Literal
+import django.contrib.syndication.views as syndication_views
+from django.conf import settings
from django.contrib.humanize.templatetags.humanize import naturaltime
+from django.contrib.sites.shortcuts import get_current_site
from django.contrib.syndication.views import Feed
from django.db.models import Prefetch
from django.db.models.query import QuerySet
@@ -16,6 +20,8 @@ from django.utils.html import format_html
from django.utils.html import format_html_join
from django.utils.safestring import SafeText
+from core.base_url import build_absolute_uri
+from core.base_url import get_current_site # noqa: F811
from twitch.models import Channel
from twitch.models import ChatBadge
from twitch.models import DropCampaign
@@ -26,11 +32,15 @@ from twitch.models import TimeBasedDrop
if TYPE_CHECKING:
import datetime
+ from collections.abc import Callable
+ from django.contrib.sites.models import Site
+ from django.contrib.sites.requests import RequestSite
from django.db.models import Model
from django.db.models import QuerySet
from django.http import HttpRequest
from django.http import HttpResponse
+ from django.utils.feedgenerator import SyndicationFeed
from django.utils.safestring import SafeString
from twitch.models import DropBenefit
@@ -99,14 +109,14 @@ class TTVDropsBaseFeed(Feed):
"""
if self._request is None:
return url
- return self._request.build_absolute_uri(url)
+ return build_absolute_uri(url, request=self._request)
def _absolute_stylesheet_urls(self, request: HttpRequest) -> list[str]:
"""Return stylesheet URLs as absolute HTTP URLs for browser/file compatibility."""
return [
href
if href.startswith(("http://", "https://"))
- else request.build_absolute_uri(href)
+ else build_absolute_uri(href, request=request)
for href in self.stylesheets
]
@@ -151,6 +161,41 @@ class TTVDropsBaseFeed(Feed):
response.content = content.encode(encoding)
+ def get_feed(self, obj: object, request: HttpRequest) -> SyndicationFeed:
+ """Use deterministic BASE_URL handling for syndication feed generation.
+
+ Returns:
+ SyndicationFeed: The feed generator instance with the correct site and URL context for absolute URL generation.
+ """
+ # TODO(TheLovinator): Refactor to avoid this mess. # noqa: TD003
+ try:
+ from django.contrib.sites import shortcuts as sites_shortcuts # noqa: I001, PLC0415
+ except ImportError:
+ sites_shortcuts = None
+
+ original_get_current_site: Callable[..., Site | RequestSite] | None = (
+ sites_shortcuts.get_current_site if sites_shortcuts else None
+ )
+ original_is_secure: Callable[[], bool] = request.is_secure
+
+ if sites_shortcuts is not None:
+ sites_shortcuts.get_current_site = get_current_site
+
+ original_syndication_get_current_site: (
+ Callable[..., Site | RequestSite] | None
+ ) = syndication_views.get_current_site # pyright: ignore[reportAttributeAccessIssue]
+ syndication_views.get_current_site = get_current_site # pyright: ignore[reportAttributeAccessIssue]
+
+ request.is_secure = lambda: settings.BASE_URL.startswith("https://")
+
+ try:
+ return super().get_feed(obj, request)
+ finally:
+ if sites_shortcuts is not None and original_get_current_site is not None:
+ sites_shortcuts.get_current_site = original_get_current_site
+ syndication_views.get_current_site = original_syndication_get_current_site # pyright: ignore[reportAttributeAccessIssue]
+ request.is_secure = original_is_secure
+
def __call__(
self,
request: HttpRequest,
diff --git a/twitch/views.py b/twitch/views.py
index a541e29..ed1c0c7 100644
--- a/twitch/views.py
+++ b/twitch/views.py
@@ -28,6 +28,7 @@ from django.utils import timezone
from django.views.generic import DetailView
from django.views.generic import ListView
+from core.base_url import build_absolute_uri
from twitch.models import Channel
from twitch.models import ChatBadge
from twitch.models import ChatBadgeSet
@@ -99,7 +100,7 @@ def _build_image_object(
return {
"@type": "ImageObject",
- "contentUrl": request.build_absolute_uri(image_url),
+ "contentUrl": build_absolute_uri(image_url),
"creditText": creator_name,
"copyrightNotice": copyright_notice or creator_name,
"creator": creator,
@@ -241,7 +242,7 @@ def _build_pagination_info(
prev_url = f"{base_url}&page={page_obj.previous_page_number()}"
pagination_links.append({
"rel": "prev",
- "url": request.build_absolute_uri(prev_url),
+ "url": build_absolute_uri(prev_url),
})
if page_obj.has_next():
@@ -251,7 +252,7 @@ def _build_pagination_info(
next_url = f"{base_url}&page={page_obj.next_page_number()}"
pagination_links.append({
"rel": "next",
- "url": request.build_absolute_uri(next_url),
+ "url": build_absolute_uri(next_url),
})
return pagination_links or None
@@ -320,7 +321,7 @@ def org_list_view(request: HttpRequest) -> HttpResponse:
"@type": "CollectionPage",
"name": "Twitch Organizations",
"description": "List of Twitch organizations.",
- "url": request.build_absolute_uri("/organizations/"),
+ "url": build_absolute_uri("/organizations/"),
}
seo_context: dict[str, Any] = _build_seo_context(
@@ -363,7 +364,7 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon
s: Literal["", "s"] = "" if games_count == 1 else "s"
org_description: str = f"{org_name} has {games_count} game{s}."
- url: str = request.build_absolute_uri(
+ url: str = build_absolute_uri(
reverse("twitch:organization_detail", args=[organization.twitch_id]),
)
organization_node: dict[str, Any] = {
@@ -388,11 +389,11 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon
# Breadcrumb schema
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
- {"name": "Home", "url": request.build_absolute_uri("/")},
- {"name": "Organizations", "url": request.build_absolute_uri("/organizations/")},
+ {"name": "Home", "url": build_absolute_uri("/")},
+ {"name": "Organizations", "url": build_absolute_uri("/organizations/")},
{
"name": org_name,
- "url": request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse("twitch:organization_detail", args=[organization.twitch_id]),
),
},
@@ -496,14 +497,14 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0
"@type": "CollectionPage",
"name": title,
"description": description,
- "url": request.build_absolute_uri(base_url),
+ "url": build_absolute_uri(base_url),
}
seo_context: dict[str, Any] = _build_seo_context(
page_title=title,
page_description=description,
seo_meta={
- "page_url": request.build_absolute_uri(base_url),
+ "page_url": build_absolute_uri(base_url),
"pagination_info": pagination_info,
"schema_data": collection_schema,
},
@@ -636,7 +637,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
campaign.image_height if campaign.image_file else None
)
- url: str = request.build_absolute_uri(
+ url: str = build_absolute_uri(
reverse("twitch:campaign_detail", args=[campaign.twitch_id]),
)
@@ -667,7 +668,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
else "Twitch"
)
campaign_owner_url: str = (
- request.build_absolute_uri(
+ build_absolute_uri(
reverse("twitch:organization_detail", args=[campaign_owner.twitch_id]),
)
if campaign_owner
@@ -701,17 +702,17 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
campaign.game.display_name or campaign.game.name or campaign.game.twitch_id
)
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
- {"name": "Home", "url": request.build_absolute_uri("/")},
- {"name": "Games", "url": request.build_absolute_uri("/games/")},
+ {"name": "Home", "url": build_absolute_uri("/")},
+ {"name": "Games", "url": build_absolute_uri("/games/")},
{
"name": game_name,
- "url": request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse("twitch:game_detail", args=[campaign.game.twitch_id]),
),
},
{
"name": campaign_name,
- "url": request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse("twitch:campaign_detail", args=[campaign.twitch_id]),
),
},
@@ -821,7 +822,7 @@ class GamesGridView(ListView):
"@type": "CollectionPage",
"name": "Twitch Games",
"description": "Twitch games that had or have Twitch drops.",
- "url": self.request.build_absolute_uri("/games/"),
+ "url": build_absolute_uri("/games/"),
}
seo_context: dict[str, Any] = _build_seo_context(
@@ -977,7 +978,7 @@ class GameDetailView(DetailView):
"@type": "VideoGame",
"name": game_name,
"description": game_description,
- "url": self.request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse("twitch:game_detail", args=[game.twitch_id]),
),
}
@@ -992,7 +993,7 @@ class GameDetailView(DetailView):
else "Twitch"
)
owner_url: str = (
- self.request.build_absolute_uri(
+ build_absolute_uri(
reverse("twitch:organization_detail", args=[preferred_owner.twitch_id]),
)
if preferred_owner
@@ -1014,11 +1015,11 @@ class GameDetailView(DetailView):
# Breadcrumb schema
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
- {"name": "Home", "url": self.request.build_absolute_uri("/")},
- {"name": "Games", "url": self.request.build_absolute_uri("/games/")},
+ {"name": "Home", "url": build_absolute_uri("/")},
+ {"name": "Games", "url": build_absolute_uri("/games/")},
{
"name": game_name,
- "url": self.request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse("twitch:game_detail", args=[game.twitch_id]),
),
},
@@ -1114,12 +1115,12 @@ def dashboard(request: HttpRequest) -> HttpResponse:
"@context": "https://schema.org",
"@type": "WebSite",
"name": "ttvdrops",
- "url": request.build_absolute_uri("/"),
+ "url": build_absolute_uri("/"),
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
- "urlTemplate": request.build_absolute_uri(
+ "urlTemplate": build_absolute_uri(
"/search/?q={search_term_string}",
),
},
@@ -1219,14 +1220,14 @@ def reward_campaign_list_view(request: HttpRequest) -> HttpResponse:
"@type": "CollectionPage",
"name": title,
"description": description,
- "url": request.build_absolute_uri(base_url),
+ "url": build_absolute_uri(base_url),
}
seo_context: dict[str, Any] = _build_seo_context(
page_title=title,
page_description=description,
seo_meta={
- "page_url": request.build_absolute_uri(base_url),
+ "page_url": build_absolute_uri(base_url),
"pagination_info": pagination_info,
"schema_data": collection_schema,
},
@@ -1275,7 +1276,7 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes
else f"{campaign_name}"
)
- reward_url: str = request.build_absolute_uri(
+ reward_url: str = build_absolute_uri(
reverse("twitch:reward_campaign_detail", args=[reward_campaign.twitch_id]),
)
@@ -1316,14 +1317,14 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes
# Breadcrumb schema
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
- {"name": "Home", "url": request.build_absolute_uri("/")},
+ {"name": "Home", "url": build_absolute_uri("/")},
{
"name": "Reward Campaigns",
- "url": request.build_absolute_uri("/reward-campaigns/"),
+ "url": build_absolute_uri("/reward-campaigns/"),
},
{
"name": campaign_name,
- "url": request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse(
"twitch:reward_campaign_detail",
args=[reward_campaign.twitch_id],
@@ -1418,14 +1419,14 @@ class ChannelListView(ListView):
"@type": "CollectionPage",
"name": "Twitch Channels",
"description": "List of Twitch channels participating in drop campaigns.",
- "url": self.request.build_absolute_uri("/channels/"),
+ "url": build_absolute_uri("/channels/"),
}
seo_context: dict[str, Any] = _build_seo_context(
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),
+ "page_url": build_absolute_uri(base_url),
"pagination_info": pagination_info,
"schema_data": collection_schema,
},
@@ -1542,7 +1543,7 @@ class ChannelDetailView(DetailView):
if total_campaigns > 1:
description += "s"
- channel_url: str = self.request.build_absolute_uri(
+ channel_url: str = build_absolute_uri(
reverse("twitch:channel_detail", args=[channel.twitch_id]),
)
channel_node: dict[str, Any] = {
@@ -1569,11 +1570,11 @@ class ChannelDetailView(DetailView):
# Breadcrumb schema
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
- {"name": "Home", "url": self.request.build_absolute_uri("/")},
- {"name": "Channels", "url": self.request.build_absolute_uri("/channels/")},
+ {"name": "Home", "url": build_absolute_uri("/")},
+ {"name": "Channels", "url": build_absolute_uri("/channels/")},
{
"name": name,
- "url": self.request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse("twitch:channel_detail", args=[channel.twitch_id]),
),
},
@@ -1638,7 +1639,7 @@ def badge_list_view(request: HttpRequest) -> HttpResponse:
"@type": "CollectionPage",
"name": "Twitch chat badges",
"description": "List of Twitch chat badges awarded through drop campaigns.",
- "url": request.build_absolute_uri("/badges/"),
+ "url": build_absolute_uri("/badges/"),
}
seo_context: dict[str, Any] = _build_seo_context(
@@ -1722,7 +1723,7 @@ def badge_set_detail_view(request: HttpRequest, set_id: str) -> HttpResponse:
"@type": "ItemList",
"name": badge_set_name,
"description": badge_set_description,
- "url": request.build_absolute_uri(
+ "url": build_absolute_uri(
reverse("twitch:badge_set_detail", args=[badge_set.set_id]),
),
}
diff --git a/youtube/views.py b/youtube/views.py
index 5695dba..dd6aa18 100644
--- a/youtube/views.py
+++ b/youtube/views.py
@@ -5,6 +5,8 @@ from typing import Any
from django.shortcuts import render
+from core.base_url import build_absolute_uri
+
if TYPE_CHECKING:
from django.http import HttpRequest
from django.http import HttpResponse
@@ -136,7 +138,7 @@ def index(request: HttpRequest) -> HttpResponse:
"@type": "ItemList",
"name": PAGE_TITLE,
"description": PAGE_DESCRIPTION,
- "url": request.build_absolute_uri(),
+ "url": build_absolute_uri(request=request),
"itemListElement": list_items,
}