Refactor URL handling to use BASE_URL across the application and add base_url context processor
All checks were successful
Deploy to Server / deploy (push) Successful in 22s
All checks were successful
Deploy to Server / deploy (push) Successful in 22s
This commit is contained in:
parent
999ab368e2
commit
d4fd35769d
11 changed files with 250 additions and 167 deletions
|
|
@ -167,6 +167,7 @@ TEMPLATES: list[dict[str, Any]] = [
|
||||||
"context_processors": [
|
"context_processors": [
|
||||||
"django.template.context_processors.debug",
|
"django.template.context_processors.debug",
|
||||||
"django.template.context_processors.request",
|
"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"
|
BASE_URL: str = "https://ttvdrops.lovinator.space"
|
||||||
# Allow overriding BASE_URL in tests via environment when needed
|
# Allow overriding BASE_URL in tests via environment when needed
|
||||||
BASE_URL = os.getenv("BASE_URL", BASE_URL)
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,10 @@ def test_meta_tags_use_request_absolute_url_for_og_url_and_canonical() -> None:
|
||||||
_extract_meta_content(content, "og:url")
|
_extract_meta_content(content, "og:url")
|
||||||
== "https://ttvdrops.lovinator.space/drops/"
|
== "https://ttvdrops.lovinator.space/drops/"
|
||||||
)
|
)
|
||||||
assert (
|
canonical_pattern: re.Pattern[str] = re.compile(
|
||||||
'<link rel="canonical" href="https://ttvdrops.lovinator.space/drops/" />'
|
r'<link\s+rel="canonical"\s+href="https://ttvdrops\.lovinator\.space/drops/"\s*/?>',
|
||||||
in content
|
|
||||||
)
|
)
|
||||||
|
assert canonical_pattern.search(content) is not None
|
||||||
|
|
||||||
|
|
||||||
def test_meta_tags_use_explicit_page_url_for_og_url_and_canonical() -> 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")
|
_extract_meta_content(content, "og:url")
|
||||||
== "https://ttvdrops.lovinator.space/custom-page/"
|
== "https://ttvdrops.lovinator.space/custom-page/"
|
||||||
)
|
)
|
||||||
assert (
|
canonical_pattern: re.Pattern[str] = re.compile(
|
||||||
'<link rel="canonical" href="https://ttvdrops.lovinator.space/custom-page/" />'
|
r'<link\s+rel="canonical"\s+href="https://ttvdrops\.lovinator\.space/custom-page/"\s*/?>',
|
||||||
in content
|
|
||||||
)
|
)
|
||||||
|
assert canonical_pattern.search(content) is not None
|
||||||
|
|
||||||
|
|
||||||
def test_meta_tags_twitter_card_is_summary_without_image() -> None:
|
def test_meta_tags_twitter_card_is_summary_without_image() -> None:
|
||||||
|
|
|
||||||
80
core/base_url.py
Normal file
80
core/base_url.py
Normal file
|
|
@ -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
|
||||||
17
core/context_processors.py
Normal file
17
core/context_processors.py
Normal file
|
|
@ -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", ""),
|
||||||
|
}
|
||||||
25
core/tests/test_base_url.py
Normal file
25
core/tests/test_base_url.py
Normal file
|
|
@ -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"
|
||||||
|
|
@ -26,6 +26,7 @@ from django.template.defaultfilters import filesizeformat
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from core.base_url import build_absolute_uri
|
||||||
from kick.models import KickChannel
|
from kick.models import KickChannel
|
||||||
from kick.models import KickDropCampaign
|
from kick.models import KickDropCampaign
|
||||||
from twitch.models import Channel
|
from twitch.models import Channel
|
||||||
|
|
@ -507,7 +508,7 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Feed Documentation",
|
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_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(
|
return render(
|
||||||
|
|
@ -721,7 +722,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
|
||||||
dataset_distributions.append({
|
dataset_distributions.append({
|
||||||
"@type": "DataDownload",
|
"@type": "DataDownload",
|
||||||
"name": dataset["name"],
|
"name": dataset["name"],
|
||||||
"contentUrl": request.build_absolute_uri(
|
"contentUrl": build_absolute_uri(
|
||||||
reverse("core:dataset_backup_download", args=[download_path]),
|
reverse("core:dataset_backup_download", args=[download_path]),
|
||||||
),
|
),
|
||||||
"encodingFormat": "application/zstd",
|
"encodingFormat": "application/zstd",
|
||||||
|
|
@ -731,9 +732,9 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Dataset",
|
"@type": "Dataset",
|
||||||
"name": "Historical archive of Twitch and Kick drop data",
|
"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/..",
|
"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/",
|
"license": "https://creativecommons.org/publicdomain/zero/1.0/",
|
||||||
"isAccessibleForFree": True,
|
"isAccessibleForFree": True,
|
||||||
"description": (
|
"description": (
|
||||||
|
|
@ -753,7 +754,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
|
||||||
"includedInDataCatalog": {
|
"includedInDataCatalog": {
|
||||||
"@type": "DataCatalog",
|
"@type": "DataCatalog",
|
||||||
"name": "ttvdrops.lovinator.space",
|
"name": "ttvdrops.lovinator.space",
|
||||||
"url": request.build_absolute_uri(reverse("core:dataset_backups")),
|
"url": build_absolute_uri(reverse("core:dataset_backups")),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if dataset_distributions:
|
if dataset_distributions:
|
||||||
|
|
@ -905,7 +906,7 @@ def search_view(request: HttpRequest) -> HttpResponse:
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=page_title,
|
page_title=page_title,
|
||||||
page_description=page_description,
|
page_description=page_description,
|
||||||
page_url=request.build_absolute_uri(reverse("core:search")),
|
page_url=build_absolute_uri(reverse("core:search")),
|
||||||
)
|
)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|
@ -1006,12 +1007,12 @@ def dashboard(request: HttpRequest) -> HttpResponse:
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": "ttvdrops",
|
"name": "ttvdrops",
|
||||||
"url": request.build_absolute_uri("/"),
|
"url": build_absolute_uri("/"),
|
||||||
"potentialAction": {
|
"potentialAction": {
|
||||||
"@type": "SearchAction",
|
"@type": "SearchAction",
|
||||||
"target": {
|
"target": {
|
||||||
"@type": "EntryPoint",
|
"@type": "EntryPoint",
|
||||||
"urlTemplate": request.build_absolute_uri(
|
"urlTemplate": build_absolute_uri(
|
||||||
"/search/?q={search_term_string}",
|
"/search/?q={search_term_string}",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ from django.shortcuts import render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from core.base_url import build_absolute_uri
|
||||||
from kick.models import KickCategory
|
from kick.models import KickCategory
|
||||||
from kick.models import KickDropCampaign
|
from kick.models import KickDropCampaign
|
||||||
from kick.models import KickOrganization
|
from kick.models import KickOrganization
|
||||||
|
|
@ -127,14 +128,14 @@ def _build_pagination_info(
|
||||||
if page_obj.has_previous():
|
if page_obj.has_previous():
|
||||||
links.append({
|
links.append({
|
||||||
"rel": "prev",
|
"rel": "prev",
|
||||||
"url": request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
f"{base_url}{sep}page={page_obj.previous_page_number()}",
|
f"{base_url}{sep}page={page_obj.previous_page_number()}",
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
if page_obj.has_next():
|
if page_obj.has_next():
|
||||||
links.append({
|
links.append({
|
||||||
"rel": "next",
|
"rel": "next",
|
||||||
"url": request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
f"{base_url}{sep}page={page_obj.next_page_number()}",
|
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(
|
seo_context: dict[str, str] = _build_seo_context(
|
||||||
page_title=title,
|
page_title=title,
|
||||||
page_description="Browse Kick drop campaigns.",
|
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(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|
@ -291,20 +292,20 @@ def campaign_detail_view(request: HttpRequest, kick_id: str) -> HttpResponse:
|
||||||
channels: list[KickChannel] = list(campaign.channels.select_related("user"))
|
channels: list[KickChannel] = list(campaign.channels.select_related("user"))
|
||||||
|
|
||||||
breadcrumb_schema: str = _build_breadcrumb_schema([
|
breadcrumb_schema: str = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
{"name": "Home", "url": build_absolute_uri("/")},
|
||||||
{
|
{
|
||||||
"name": "Kick Campaigns",
|
"name": "Kick Campaigns",
|
||||||
"url": request.build_absolute_uri(reverse("kick:campaign_list")),
|
"url": build_absolute_uri(reverse("kick:campaign_list")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": campaign.name,
|
"name": campaign.name,
|
||||||
"url": request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse("kick:campaign_detail", args=[campaign.kick_id]),
|
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]),
|
reverse("kick:campaign_detail", args=[campaign.kick_id]),
|
||||||
)
|
)
|
||||||
campaign_event: dict[str, Any] = {
|
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([
|
breadcrumb_schema: str = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
{"name": "Home", "url": build_absolute_uri("/")},
|
||||||
{
|
{
|
||||||
"name": "Kick Games",
|
"name": "Kick Games",
|
||||||
"url": request.build_absolute_uri(reverse("kick:game_list")),
|
"url": build_absolute_uri(reverse("kick:game_list")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": category.name,
|
"name": category.name,
|
||||||
"url": request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse("kick:game_detail", args=[category.kick_id]),
|
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]),
|
reverse("kick:game_detail", args=[category.kick_id]),
|
||||||
)
|
)
|
||||||
category_schema: dict[str, Any] = {
|
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([
|
breadcrumb_schema: str = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
{"name": "Home", "url": build_absolute_uri("/")},
|
||||||
{
|
{
|
||||||
"name": "Kick Organizations",
|
"name": "Kick Organizations",
|
||||||
"url": request.build_absolute_uri(reverse("kick:organization_list")),
|
"url": build_absolute_uri(reverse("kick:organization_list")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": org.name,
|
"name": org.name,
|
||||||
"url": request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse("kick:organization_detail", args=[org.kick_id]),
|
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]),
|
reverse("kick:organization_detail", args=[org.kick_id]),
|
||||||
)
|
)
|
||||||
organization_node: dict[str, Any] = {
|
organization_node: dict[str, Any] = {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
content="{% firstof page_description 'ttvdrops - Track Twitch drops.' %}" />
|
content="{% firstof page_description 'ttvdrops - Track Twitch drops.' %}" />
|
||||||
<meta property="og:type" content="{% firstof og_type 'website' %}" />
|
<meta property="og:type" content="{% firstof og_type 'website' %}" />
|
||||||
<meta property="og:url"
|
<meta property="og:url"
|
||||||
content="{% firstof page_url request.build_absolute_uri %}" />
|
content="{% if page_url %}{{ page_url }}{% else %}{{ BASE_URL }}{{ request.get_full_path }}{% endif %}" />
|
||||||
{% if page_image %}
|
{% if page_image %}
|
||||||
<meta property="og:image" content="{{ page_image }}" />
|
<meta property="og:image" content="{{ page_image }}" />
|
||||||
{% if page_image_width and page_image_height %}
|
{% if page_image_width and page_image_height %}
|
||||||
|
|
@ -41,7 +41,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Twitter Card tags for rich previews #}
|
{# Twitter Card tags for rich previews #}
|
||||||
<meta name="twitter:card" content="{% if page_image %}summary_large_image{% else %}summary{% endif %}" />
|
<meta name="twitter:card"
|
||||||
|
content="{% if page_image %}summary_large_image{% else %}summary{% endif %}" />
|
||||||
<meta name="twitter:title" content="{% firstof page_title 'ttvdrops' %}" />
|
<meta name="twitter:title" content="{% firstof page_title 'ttvdrops' %}" />
|
||||||
<meta name="twitter:description"
|
<meta name="twitter:description"
|
||||||
content="{% firstof page_description 'ttvdrops - Twitch and Kick drops.' %}" />
|
content="{% firstof page_description 'ttvdrops - Twitch and Kick drops.' %}" />
|
||||||
|
|
@ -50,20 +51,13 @@
|
||||||
{% if published_date %}<meta property="article:published_time" content="{{ published_date }}" />{% endif %}
|
{% if published_date %}<meta property="article:published_time" content="{{ published_date }}" />{% endif %}
|
||||||
{% if modified_date %}<meta property="article:modified_time" content="{{ modified_date }}" />{% endif %}
|
{% if modified_date %}<meta property="article:modified_time" content="{{ modified_date }}" />{% endif %}
|
||||||
{# Canonical tag #}
|
{# Canonical tag #}
|
||||||
<link rel="canonical" href="{% firstof page_url request.build_absolute_uri %}" />
|
<link rel="canonical"
|
||||||
|
href="{% if page_url %}{{ page_url }}{% else %}{{ BASE_URL }}{{ request.get_full_path }}{% endif %}" />
|
||||||
{# Pagination links (for crawler efficiency) #}
|
{# Pagination links (for crawler efficiency) #}
|
||||||
{% if pagination_info %}
|
{% if pagination_info %}
|
||||||
{% for link in pagination_info %}<link rel="{{ link.rel }}" href="{{ link.url }}" />{% endfor %}
|
{% for link in pagination_info %}<link rel="{{ link.rel }}" href="{{ link.url }}" />{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Schema.org JSON-LD structured data #}
|
{# Schema.org JSON-LD structured data #}
|
||||||
{% if schema_data %}
|
{% if schema_data %}<script type="application/ld+json">{{ schema_data|safe }}</script>{% endif %}
|
||||||
<script type="application/ld+json">
|
|
||||||
{{ schema_data|safe }}
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
{# Breadcrumb schema #}
|
{# Breadcrumb schema #}
|
||||||
{% if breadcrumb_schema %}
|
{% if breadcrumb_schema %}<script type="application/ld+json">{{ breadcrumb_schema|safe }}</script>{% endif %}
|
||||||
<script type="application/ld+json">
|
|
||||||
{{ breadcrumb_schema|safe }}
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from collections.abc import Callable
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Literal
|
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.humanize.templatetags.humanize import naturaltime
|
||||||
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.contrib.syndication.views import Feed
|
from django.contrib.syndication.views import Feed
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.db.models.query import QuerySet
|
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.html import format_html_join
|
||||||
from django.utils.safestring import SafeText
|
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 Channel
|
||||||
from twitch.models import ChatBadge
|
from twitch.models import ChatBadge
|
||||||
from twitch.models import DropCampaign
|
from twitch.models import DropCampaign
|
||||||
|
|
@ -26,11 +32,15 @@ from twitch.models import TimeBasedDrop
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import datetime
|
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 Model
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.utils.feedgenerator import SyndicationFeed
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
|
|
||||||
from twitch.models import DropBenefit
|
from twitch.models import DropBenefit
|
||||||
|
|
@ -99,14 +109,14 @@ class TTVDropsBaseFeed(Feed):
|
||||||
"""
|
"""
|
||||||
if self._request is None:
|
if self._request is None:
|
||||||
return url
|
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]:
|
def _absolute_stylesheet_urls(self, request: HttpRequest) -> list[str]:
|
||||||
"""Return stylesheet URLs as absolute HTTP URLs for browser/file compatibility."""
|
"""Return stylesheet URLs as absolute HTTP URLs for browser/file compatibility."""
|
||||||
return [
|
return [
|
||||||
href
|
href
|
||||||
if href.startswith(("http://", "https://"))
|
if href.startswith(("http://", "https://"))
|
||||||
else request.build_absolute_uri(href)
|
else build_absolute_uri(href, request=request)
|
||||||
for href in self.stylesheets
|
for href in self.stylesheets
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -151,6 +161,41 @@ class TTVDropsBaseFeed(Feed):
|
||||||
|
|
||||||
response.content = content.encode(encoding)
|
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__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ from django.utils import timezone
|
||||||
from django.views.generic import DetailView
|
from django.views.generic import DetailView
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
||||||
|
from core.base_url import build_absolute_uri
|
||||||
from twitch.models import Channel
|
from twitch.models import Channel
|
||||||
from twitch.models import ChatBadge
|
from twitch.models import ChatBadge
|
||||||
from twitch.models import ChatBadgeSet
|
from twitch.models import ChatBadgeSet
|
||||||
|
|
@ -99,7 +100,7 @@ def _build_image_object(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"@type": "ImageObject",
|
"@type": "ImageObject",
|
||||||
"contentUrl": request.build_absolute_uri(image_url),
|
"contentUrl": build_absolute_uri(image_url),
|
||||||
"creditText": creator_name,
|
"creditText": creator_name,
|
||||||
"copyrightNotice": copyright_notice or creator_name,
|
"copyrightNotice": copyright_notice or creator_name,
|
||||||
"creator": creator,
|
"creator": creator,
|
||||||
|
|
@ -241,7 +242,7 @@ def _build_pagination_info(
|
||||||
prev_url = f"{base_url}&page={page_obj.previous_page_number()}"
|
prev_url = f"{base_url}&page={page_obj.previous_page_number()}"
|
||||||
pagination_links.append({
|
pagination_links.append({
|
||||||
"rel": "prev",
|
"rel": "prev",
|
||||||
"url": request.build_absolute_uri(prev_url),
|
"url": build_absolute_uri(prev_url),
|
||||||
})
|
})
|
||||||
|
|
||||||
if page_obj.has_next():
|
if page_obj.has_next():
|
||||||
|
|
@ -251,7 +252,7 @@ def _build_pagination_info(
|
||||||
next_url = f"{base_url}&page={page_obj.next_page_number()}"
|
next_url = f"{base_url}&page={page_obj.next_page_number()}"
|
||||||
pagination_links.append({
|
pagination_links.append({
|
||||||
"rel": "next",
|
"rel": "next",
|
||||||
"url": request.build_absolute_uri(next_url),
|
"url": build_absolute_uri(next_url),
|
||||||
})
|
})
|
||||||
|
|
||||||
return pagination_links or None
|
return pagination_links or None
|
||||||
|
|
@ -320,7 +321,7 @@ def org_list_view(request: HttpRequest) -> HttpResponse:
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "Twitch Organizations",
|
"name": "Twitch Organizations",
|
||||||
"description": "List of 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(
|
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"
|
s: Literal["", "s"] = "" if games_count == 1 else "s"
|
||||||
org_description: str = f"{org_name} has {games_count} game{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]),
|
reverse("twitch:organization_detail", args=[organization.twitch_id]),
|
||||||
)
|
)
|
||||||
organization_node: dict[str, Any] = {
|
organization_node: dict[str, Any] = {
|
||||||
|
|
@ -388,11 +389,11 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon
|
||||||
|
|
||||||
# Breadcrumb schema
|
# Breadcrumb schema
|
||||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
{"name": "Home", "url": build_absolute_uri("/")},
|
||||||
{"name": "Organizations", "url": request.build_absolute_uri("/organizations/")},
|
{"name": "Organizations", "url": build_absolute_uri("/organizations/")},
|
||||||
{
|
{
|
||||||
"name": org_name,
|
"name": org_name,
|
||||||
"url": request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse("twitch:organization_detail", args=[organization.twitch_id]),
|
reverse("twitch:organization_detail", args=[organization.twitch_id]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -496,14 +497,14 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": title,
|
"name": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": request.build_absolute_uri(base_url),
|
"url": build_absolute_uri(base_url),
|
||||||
}
|
}
|
||||||
|
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=title,
|
page_title=title,
|
||||||
page_description=description,
|
page_description=description,
|
||||||
seo_meta={
|
seo_meta={
|
||||||
"page_url": request.build_absolute_uri(base_url),
|
"page_url": build_absolute_uri(base_url),
|
||||||
"pagination_info": pagination_info,
|
"pagination_info": pagination_info,
|
||||||
"schema_data": collection_schema,
|
"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
|
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]),
|
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"
|
else "Twitch"
|
||||||
)
|
)
|
||||||
campaign_owner_url: str = (
|
campaign_owner_url: str = (
|
||||||
request.build_absolute_uri(
|
build_absolute_uri(
|
||||||
reverse("twitch:organization_detail", args=[campaign_owner.twitch_id]),
|
reverse("twitch:organization_detail", args=[campaign_owner.twitch_id]),
|
||||||
)
|
)
|
||||||
if campaign_owner
|
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
|
campaign.game.display_name or campaign.game.name or campaign.game.twitch_id
|
||||||
)
|
)
|
||||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
{"name": "Home", "url": build_absolute_uri("/")},
|
||||||
{"name": "Games", "url": request.build_absolute_uri("/games/")},
|
{"name": "Games", "url": build_absolute_uri("/games/")},
|
||||||
{
|
{
|
||||||
"name": game_name,
|
"name": game_name,
|
||||||
"url": request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse("twitch:game_detail", args=[campaign.game.twitch_id]),
|
reverse("twitch:game_detail", args=[campaign.game.twitch_id]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": campaign_name,
|
"name": campaign_name,
|
||||||
"url": request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse("twitch:campaign_detail", args=[campaign.twitch_id]),
|
reverse("twitch:campaign_detail", args=[campaign.twitch_id]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -821,7 +822,7 @@ class GamesGridView(ListView):
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "Twitch Games",
|
"name": "Twitch Games",
|
||||||
"description": "Twitch games that had or have Twitch drops.",
|
"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(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
|
|
@ -977,7 +978,7 @@ class GameDetailView(DetailView):
|
||||||
"@type": "VideoGame",
|
"@type": "VideoGame",
|
||||||
"name": game_name,
|
"name": game_name,
|
||||||
"description": game_description,
|
"description": game_description,
|
||||||
"url": self.request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse("twitch:game_detail", args=[game.twitch_id]),
|
reverse("twitch:game_detail", args=[game.twitch_id]),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
@ -992,7 +993,7 @@ class GameDetailView(DetailView):
|
||||||
else "Twitch"
|
else "Twitch"
|
||||||
)
|
)
|
||||||
owner_url: str = (
|
owner_url: str = (
|
||||||
self.request.build_absolute_uri(
|
build_absolute_uri(
|
||||||
reverse("twitch:organization_detail", args=[preferred_owner.twitch_id]),
|
reverse("twitch:organization_detail", args=[preferred_owner.twitch_id]),
|
||||||
)
|
)
|
||||||
if preferred_owner
|
if preferred_owner
|
||||||
|
|
@ -1014,11 +1015,11 @@ class GameDetailView(DetailView):
|
||||||
|
|
||||||
# Breadcrumb schema
|
# Breadcrumb schema
|
||||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": self.request.build_absolute_uri("/")},
|
{"name": "Home", "url": build_absolute_uri("/")},
|
||||||
{"name": "Games", "url": self.request.build_absolute_uri("/games/")},
|
{"name": "Games", "url": build_absolute_uri("/games/")},
|
||||||
{
|
{
|
||||||
"name": game_name,
|
"name": game_name,
|
||||||
"url": self.request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse("twitch:game_detail", args=[game.twitch_id]),
|
reverse("twitch:game_detail", args=[game.twitch_id]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -1114,12 +1115,12 @@ def dashboard(request: HttpRequest) -> HttpResponse:
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": "ttvdrops",
|
"name": "ttvdrops",
|
||||||
"url": request.build_absolute_uri("/"),
|
"url": build_absolute_uri("/"),
|
||||||
"potentialAction": {
|
"potentialAction": {
|
||||||
"@type": "SearchAction",
|
"@type": "SearchAction",
|
||||||
"target": {
|
"target": {
|
||||||
"@type": "EntryPoint",
|
"@type": "EntryPoint",
|
||||||
"urlTemplate": request.build_absolute_uri(
|
"urlTemplate": build_absolute_uri(
|
||||||
"/search/?q={search_term_string}",
|
"/search/?q={search_term_string}",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -1219,14 +1220,14 @@ def reward_campaign_list_view(request: HttpRequest) -> HttpResponse:
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": title,
|
"name": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": request.build_absolute_uri(base_url),
|
"url": build_absolute_uri(base_url),
|
||||||
}
|
}
|
||||||
|
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=title,
|
page_title=title,
|
||||||
page_description=description,
|
page_description=description,
|
||||||
seo_meta={
|
seo_meta={
|
||||||
"page_url": request.build_absolute_uri(base_url),
|
"page_url": build_absolute_uri(base_url),
|
||||||
"pagination_info": pagination_info,
|
"pagination_info": pagination_info,
|
||||||
"schema_data": collection_schema,
|
"schema_data": collection_schema,
|
||||||
},
|
},
|
||||||
|
|
@ -1275,7 +1276,7 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes
|
||||||
else f"{campaign_name}"
|
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]),
|
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
|
||||||
breadcrumb_schema: dict[str, Any] = _build_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",
|
"name": "Reward Campaigns",
|
||||||
"url": request.build_absolute_uri("/reward-campaigns/"),
|
"url": build_absolute_uri("/reward-campaigns/"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": campaign_name,
|
"name": campaign_name,
|
||||||
"url": request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse(
|
reverse(
|
||||||
"twitch:reward_campaign_detail",
|
"twitch:reward_campaign_detail",
|
||||||
args=[reward_campaign.twitch_id],
|
args=[reward_campaign.twitch_id],
|
||||||
|
|
@ -1418,14 +1419,14 @@ class ChannelListView(ListView):
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "Twitch Channels",
|
"name": "Twitch Channels",
|
||||||
"description": "List of Twitch channels participating in drop campaigns.",
|
"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(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Twitch Channels",
|
page_title="Twitch Channels",
|
||||||
page_description="List of Twitch channels participating in drop campaigns.",
|
page_description="List of Twitch channels participating in drop campaigns.",
|
||||||
seo_meta={
|
seo_meta={
|
||||||
"page_url": self.request.build_absolute_uri(base_url),
|
"page_url": build_absolute_uri(base_url),
|
||||||
"pagination_info": pagination_info,
|
"pagination_info": pagination_info,
|
||||||
"schema_data": collection_schema,
|
"schema_data": collection_schema,
|
||||||
},
|
},
|
||||||
|
|
@ -1542,7 +1543,7 @@ class ChannelDetailView(DetailView):
|
||||||
if total_campaigns > 1:
|
if total_campaigns > 1:
|
||||||
description += "s"
|
description += "s"
|
||||||
|
|
||||||
channel_url: str = self.request.build_absolute_uri(
|
channel_url: str = build_absolute_uri(
|
||||||
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
||||||
)
|
)
|
||||||
channel_node: dict[str, Any] = {
|
channel_node: dict[str, Any] = {
|
||||||
|
|
@ -1569,11 +1570,11 @@ class ChannelDetailView(DetailView):
|
||||||
|
|
||||||
# Breadcrumb schema
|
# Breadcrumb schema
|
||||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": self.request.build_absolute_uri("/")},
|
{"name": "Home", "url": build_absolute_uri("/")},
|
||||||
{"name": "Channels", "url": self.request.build_absolute_uri("/channels/")},
|
{"name": "Channels", "url": build_absolute_uri("/channels/")},
|
||||||
{
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"url": self.request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -1638,7 +1639,7 @@ def badge_list_view(request: HttpRequest) -> HttpResponse:
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "Twitch chat badges",
|
"name": "Twitch chat badges",
|
||||||
"description": "List of Twitch chat badges awarded through drop campaigns.",
|
"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(
|
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",
|
"@type": "ItemList",
|
||||||
"name": badge_set_name,
|
"name": badge_set_name,
|
||||||
"description": badge_set_description,
|
"description": badge_set_description,
|
||||||
"url": request.build_absolute_uri(
|
"url": build_absolute_uri(
|
||||||
reverse("twitch:badge_set_detail", args=[badge_set.set_id]),
|
reverse("twitch:badge_set_detail", args=[badge_set.set_id]),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ from typing import Any
|
||||||
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from core.base_url import build_absolute_uri
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
@ -136,7 +138,7 @@ def index(request: HttpRequest) -> HttpResponse:
|
||||||
"@type": "ItemList",
|
"@type": "ItemList",
|
||||||
"name": PAGE_TITLE,
|
"name": PAGE_TITLE,
|
||||||
"description": PAGE_DESCRIPTION,
|
"description": PAGE_DESCRIPTION,
|
||||||
"url": request.build_absolute_uri(),
|
"url": build_absolute_uri(request=request),
|
||||||
"itemListElement": list_items,
|
"itemListElement": list_items,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue