diff --git a/config/urls.py b/config/urls.py index 22186a7..15b37bb 100644 --- a/config/urls.py +++ b/config/urls.py @@ -13,6 +13,36 @@ if TYPE_CHECKING: urlpatterns: list[URLPattern | URLResolver] = [ path(route="sitemap.xml", view=core_views.sitemap_view, name="sitemap"), + path( + route="sitemap-static.xml", + view=core_views.sitemap_static_view, + name="sitemap-static", + ), + path( + route="sitemap-twitch-channels.xml", + view=core_views.sitemap_twitch_channels_view, + name="sitemap-twitch-channels", + ), + path( + route="sitemap-twitch-drops.xml", + view=core_views.sitemap_twitch_drops_view, + name="sitemap-twitch-drops", + ), + path( + route="sitemap-twitch-others.xml", + view=core_views.sitemap_twitch_others_view, + name="sitemap-twitch-others", + ), + path( + route="sitemap-kick.xml", + view=core_views.sitemap_kick_view, + name="sitemap-kick", + ), + path( + route="sitemap-youtube.xml", + view=core_views.sitemap_youtube_view, + name="sitemap-youtube", + ), # Core app path(route="", view=include("core.urls", namespace="core")), # Twitch app diff --git a/core/views.py b/core/views.py index 2b97f8f..c340e08 100644 --- a/core/views.py +++ b/core/views.py @@ -11,6 +11,7 @@ from django.db import connection from django.db.models import Count from django.db.models import Exists from django.db.models import F +from django.db.models import Max from django.db.models import OuterRef from django.db.models import Prefetch from django.db.models import Q @@ -119,143 +120,357 @@ def _build_seo_context( # noqa: PLR0913, PLR0917 return context +def _render_urlset_xml( + url_entries: list[dict[str, str | dict[str, str]]], +) -> str: + """Render a sitemap XML string from URL entries. + + Args: + url_entries: List of dictionaries containing URL entry data. + + Returns: + A string containing the rendered XML. + + """ + xml = '\n' + xml += '\n' + for url_entry in url_entries: + xml += " \n" + xml += f" {url_entry['url']}\n" + if url_entry.get("lastmod"): + xml += f" {url_entry['lastmod']}\n" + xml += " \n" + xml += "" + return xml + + +def _render_sitemap_index_xml(sitemap_entries: list[dict[str, str]]) -> str: + """Render a XML string listing sitemap URLs. + + Args: + sitemap_entries: List of dictionaries with "loc" and optional "lastmod". + + Returns: + A string containing the rendered XML. + """ + xml = '\n' + xml += '\n' + for entry in sitemap_entries: + xml += " \n" + xml += f" {entry['loc']}\n" + if entry.get("lastmod"): + xml += f" {entry['lastmod']}\n" + xml += " \n" + xml += "" + return xml + + +def _build_base_url(request: HttpRequest) -> str: + """Return base url including scheme and host.""" + return f"{request.scheme}://{request.get_host()}" + + # MARK: /sitemap.xml -def sitemap_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0915 - """Generate a dynamic XML sitemap for search engines. +def sitemap_view(request: HttpRequest) -> HttpResponse: + """Sitemap index pointing to per-section sitemap files. Args: request: The HTTP request. Returns: - HttpResponse: XML sitemap. + HttpResponse: The rendered sitemap index XML. """ - base_url: str = f"{request.scheme}://{request.get_host()}" + base_url: str = _build_base_url(request) - # Start building sitemap XML + # Compute last modified per-section so search engines can more intelligently crawl. + # Do not fabricate a lastmod date if the section has no data. + twitch_channels_lastmod = Channel.objects.aggregate(max=Max("updated_at"))["max"] + twitch_drops_lastmod = max( + [ + dt + for dt in [ + DropCampaign.objects.aggregate(max=Max("updated_at"))["max"], + RewardCampaign.objects.aggregate(max=Max("updated_at"))["max"], + ] + if dt is not None + ] + or [None], + ) + twitch_others_lastmod = max( + [ + dt + for dt in [ + Game.objects.aggregate(max=Max("updated_at"))["max"], + Organization.objects.aggregate(max=Max("updated_at"))["max"], + ChatBadgeSet.objects.aggregate(max=Max("updated_at"))["max"], + ] + if dt is not None + ] + or [None], + ) + kick_lastmod = KickDropCampaign.objects.aggregate(max=Max("updated_at"))["max"] + + sitemap_entries: list[dict[str, str]] = [ + {"loc": f"{base_url}/sitemap-static.xml"}, + {"loc": f"{base_url}/sitemap-twitch-channels.xml"}, + {"loc": f"{base_url}/sitemap-twitch-drops.xml"}, + {"loc": f"{base_url}/sitemap-twitch-others.xml"}, + {"loc": f"{base_url}/sitemap-kick.xml"}, + {"loc": f"{base_url}/sitemap-youtube.xml"}, + ] + + if twitch_channels_lastmod is not None: + sitemap_entries[1]["lastmod"] = twitch_channels_lastmod.isoformat() + if twitch_drops_lastmod is not None: + sitemap_entries[2]["lastmod"] = twitch_drops_lastmod.isoformat() + if twitch_others_lastmod is not None: + sitemap_entries[3]["lastmod"] = twitch_others_lastmod.isoformat() + if kick_lastmod is not None: + sitemap_entries[4]["lastmod"] = kick_lastmod.isoformat() + + xml_content: str = _render_sitemap_index_xml(sitemap_entries) + return HttpResponse(xml_content, content_type="application/xml") + + +# MARK: /sitemap-static.xml +def sitemap_static_view(request: HttpRequest) -> HttpResponse: + """Sitemap containing the main static pages. + + Args: + request: The HTTP request. + + Returns: + HttpResponse: The rendered sitemap XML. + """ + base_url: str = _build_base_url(request) + + # Include the canonical top-level pages, the main app dashboards, and key list views. + # Using reverse() keeps these URLs correct if route patterns change. + static_route_names: list[str] = [ + "core:dashboard", + # "core:search", + "core:dataset_backups", + "core:docs_rss", + # Core RSS/Atom feeds + # "core:campaign_feed", + # "core:game_feed", + # "core:organization_feed", + # "core:reward_campaign_feed", + # "core:campaign_feed_atom", + # "core:game_feed_atom", + # "core:organization_feed_atom", + # "core:reward_campaign_feed_atom", + # "core:campaign_feed_discord", + # "core:game_feed_discord", + # "core:organization_feed_discord", + # "core:reward_campaign_feed_discord", + # Twitch pages + "twitch:dashboard", + "twitch:badge_list", + "twitch:campaign_list", + "twitch:channel_list", + "twitch:emote_gallery", + "twitch:games_grid", + "twitch:games_list", + "twitch:org_list", + "twitch:reward_campaign_list", + # Kick pages + "kick:dashboard", + "kick:campaign_list", + "kick:game_list", + "kick:category_list", + "kick:organization_list", + # Kick RSS/Atom feeds + # "kick:campaign_feed", + # "kick:game_feed", + # "kick:category_feed", + # "kick:organization_feed", + # "kick:campaign_feed_atom", + # "kick:game_feed_atom", + # "kick:category_feed_atom", + # "kick:organization_feed_atom", + # "kick:campaign_feed_discord", + # "kick:game_feed_discord", + # "kick:category_feed_discord", + # "kick:organization_feed_discord", + # YouTube + "youtube:index", + ] + + sitemap_urls: list[dict[str, str | dict[str, str]]] = [] + for route_name in static_route_names: + url = reverse(route_name) + sitemap_urls.append({"url": f"{base_url}{url}"}) + + xml_content: str = _render_urlset_xml(sitemap_urls) + return HttpResponse(xml_content, content_type="application/xml") + + +# MARK: /sitemap-twitch-channels.xml +def sitemap_twitch_channels_view(request: HttpRequest) -> HttpResponse: + """Sitemap containing Twitch channel pages. + + Args: + request: The HTTP request. + + Returns: + HttpResponse: The rendered sitemap XML. + """ + base_url: str = _build_base_url(request) sitemap_urls: list[dict[str, str | dict[str, str]]] = [] - # Static pages - sitemap_urls.extend([ - {"url": f"{base_url}/", "priority": "1.0", "changefreq": "daily"}, - {"url": f"{base_url}/campaigns/", "priority": "0.9", "changefreq": "daily"}, - { - "url": f"{base_url}/reward-campaigns/", - "priority": "0.9", - "changefreq": "daily", - }, - {"url": f"{base_url}/games/", "priority": "0.9", "changefreq": "weekly"}, - { - "url": f"{base_url}/organizations/", - "priority": "0.8", - "changefreq": "weekly", - }, - {"url": f"{base_url}/channels/", "priority": "0.8", "changefreq": "weekly"}, - {"url": f"{base_url}/badges/", "priority": "0.7", "changefreq": "monthly"}, - {"url": f"{base_url}/emotes/", "priority": "0.7", "changefreq": "monthly"}, - {"url": f"{base_url}/search/", "priority": "0.6", "changefreq": "monthly"}, - ]) - - # Dynamic detail pages - Games - games: QuerySet[Game] = Game.objects.all() - for game in games: - entry: dict[str, str | dict[str, str]] = { - "url": f"{base_url}{reverse('twitch:game_detail', args=[game.twitch_id])}", - "priority": "0.8", - "changefreq": "weekly", - } - if game.updated_at: - entry["lastmod"] = game.updated_at.isoformat() - sitemap_urls.append(entry) - - # Dynamic detail pages - Campaigns - campaigns: QuerySet[DropCampaign] = DropCampaign.objects.all() - for campaign in campaigns: - resource_url: str = reverse("twitch:campaign_detail", args=[campaign.twitch_id]) - full_url: str = f"{base_url}{resource_url}" - entry: dict[str, str | dict[str, str]] = { - "url": full_url, - "priority": "0.7", - "changefreq": "weekly", - } - if campaign.updated_at: - entry["lastmod"] = campaign.updated_at.isoformat() - sitemap_urls.append(entry) - - # Dynamic detail pages - Organizations - orgs: QuerySet[Organization] = Organization.objects.all() - for org in orgs: - resource_url = reverse("twitch:organization_detail", args=[org.twitch_id]) - full_url: str = f"{base_url}{resource_url}" - entry: dict[str, str | dict[str, str]] = { - "url": full_url, - "priority": "0.7", - "changefreq": "weekly", - } - if org.updated_at: - entry["lastmod"] = org.updated_at.isoformat() - sitemap_urls.append(entry) - - # Dynamic detail pages - Channels channels: QuerySet[Channel] = Channel.objects.all() for channel in channels: - resource_url = reverse("twitch:channel_detail", args=[channel.twitch_id]) + resource_url: str = reverse("twitch:channel_detail", args=[channel.twitch_id]) full_url: str = f"{base_url}{resource_url}" - entry: dict[str, str | dict[str, str]] = { - "url": full_url, - "priority": "0.6", - "changefreq": "weekly", - } + entry: dict[str, str | dict[str, str]] = {"url": full_url} if channel.updated_at: entry["lastmod"] = channel.updated_at.isoformat() sitemap_urls.append(entry) - # Dynamic detail pages - Badges - badge_sets: QuerySet[ChatBadgeSet] = ChatBadgeSet.objects.all() - for badge_set in badge_sets: - resource_url = reverse("twitch:badge_set_detail", args=[badge_set.set_id]) - full_url: str = f"{base_url}{resource_url}" - sitemap_urls.append({ - "url": full_url, - "priority": "0.5", - "changefreq": "monthly", - }) + xml_content: str = _render_urlset_xml(sitemap_urls) + return HttpResponse(xml_content, content_type="application/xml") + + +# MARK: /sitemap-twitch-drops.xml +def sitemap_twitch_drops_view(request: HttpRequest) -> HttpResponse: + """Sitemap containing Twitch drop campaign pages. + + Args: + request: The HTTP request. + + Returns: + HttpResponse: The rendered sitemap XML. + """ + base_url: str = _build_base_url(request) + sitemap_urls: list[dict[str, str | dict[str, str]]] = [] + + campaigns: QuerySet[DropCampaign] = DropCampaign.objects.all() + for campaign in campaigns: + resource_url: str = reverse("twitch:campaign_detail", args=[campaign.twitch_id]) + full_url: str = f"{base_url}{resource_url}" + entry: dict[str, str] = {"url": full_url} + if campaign.updated_at: + entry["lastmod"] = campaign.updated_at.isoformat() + sitemap_urls.append(entry) - # Dynamic detail pages - Reward Campaigns reward_campaigns: QuerySet[RewardCampaign] = RewardCampaign.objects.all() for reward_campaign in reward_campaigns: resource_url = reverse( "twitch:reward_campaign_detail", - args=[ - reward_campaign.twitch_id, - ], + args=[reward_campaign.twitch_id], ) + full_url: str = f"{base_url}{resource_url}" - entry: dict[str, str | dict[str, str]] = { - "url": full_url, - "priority": "0.6", - "changefreq": "weekly", - } + entry: dict[str, str | dict[str, str]] = {"url": full_url} if reward_campaign.updated_at: entry["lastmod"] = reward_campaign.updated_at.isoformat() + sitemap_urls.append(entry) - # Build XML - xml_content = '\n' - xml_content += '\n' + xml_content: str = _render_urlset_xml(sitemap_urls) + return HttpResponse(xml_content, content_type="application/xml") - for url_entry in sitemap_urls: - xml_content += " \n" - xml_content += f" {url_entry['url']}\n" - if url_entry.get("lastmod"): - xml_content += f" {url_entry['lastmod']}\n" - xml_content += ( - f" {url_entry.get('changefreq', 'monthly')}\n" - ) - xml_content += f" {url_entry.get('priority', '0.5')}\n" - xml_content += " \n" - xml_content += "" +# MARK: /sitemap-twitch-others.xml +def sitemap_twitch_others_view(request: HttpRequest) -> HttpResponse: + """Sitemap containing other Twitch pages (games, organizations, badges, emotes). + Args: + request: The HTTP request. + + Returns: + HttpResponse: The rendered sitemap XML. + """ + base_url: str = _build_base_url(request) + sitemap_urls: list[dict[str, str | dict[str, str]]] = [] + + games: QuerySet[Game] = Game.objects.all() + for game in games: + resource_url: str = reverse("twitch:game_detail", args=[game.twitch_id]) + full_url: str = f"{base_url}{resource_url}" + entry: dict[str, str | dict[str, str]] = {"url": full_url} + + if game.updated_at: + entry["lastmod"] = game.updated_at.isoformat() + + sitemap_urls.append(entry) + + orgs: QuerySet[Organization] = Organization.objects.all() + for org in orgs: + resource_url: str = reverse("twitch:organization_detail", args=[org.twitch_id]) + full_url: str = f"{base_url}{resource_url}" + entry: dict[str, str | dict[str, str]] = {"url": full_url} + + if org.updated_at: + entry["lastmod"] = org.updated_at.isoformat() + + sitemap_urls.append(entry) + + badge_sets: QuerySet[ChatBadgeSet] = ChatBadgeSet.objects.all() + for badge_set in badge_sets: + resource_url = reverse("twitch:badge_set_detail", args=[badge_set.set_id]) + full_url = f"{base_url}{resource_url}" + sitemap_urls.append({"url": full_url}) + + # Emotes currently don't have individual detail pages, but keep a listing here. + sitemap_urls.append({"url": f"{base_url}/emotes/"}) + + xml_content: str = _render_urlset_xml(sitemap_urls) + return HttpResponse(xml_content, content_type="application/xml") + + +# MARK: /sitemap-kick.xml +def sitemap_kick_view(request: HttpRequest) -> HttpResponse: + """Sitemap containing Kick drops and related pages. + + Args: + request: The HTTP request. + + Returns: + HttpResponse: The rendered sitemap XML. + """ + base_url: str = _build_base_url(request) + sitemap_urls: list[dict[str, str | dict[str, str]]] = [] + + kick_campaigns: QuerySet[KickDropCampaign] = KickDropCampaign.objects.all() + for campaign in kick_campaigns: + resource_url: str = reverse("kick:campaign_detail", args=[campaign.kick_id]) + full_url: str = f"{base_url}{resource_url}" + entry: dict[str, str | dict[str, str]] = {"url": full_url} + + if campaign.updated_at: + entry["lastmod"] = campaign.updated_at.isoformat() + + sitemap_urls.append(entry) + + # Include Kick organizations and game list pages + sitemap_urls.extend(( + {"url": f"{base_url}/kick/organizations/"}, + {"url": f"{base_url}/kick/games/"}, + )) + + xml_content: str = _render_urlset_xml(sitemap_urls) + return HttpResponse(xml_content, content_type="application/xml") + + +# MARK: /sitemap-youtube.xml +def sitemap_youtube_view(request: HttpRequest) -> HttpResponse: + """Sitemap containing the YouTube page(s). + + Args: + request: The HTTP request. + + Returns: + HttpResponse: The rendered sitemap XML. + """ + base_url: str = _build_base_url(request) + + sitemap_urls: list[dict[str, str | dict[str, str]]] = [ + {"url": f"{base_url}{reverse('youtube:index')}"}, + ] + + xml_content: str = _render_urlset_xml(sitemap_urls) return HttpResponse(xml_content, content_type="application/xml") diff --git a/twitch/tests/test_views.py b/twitch/tests/test_views.py index 64fe90c..8c50e74 100644 --- a/twitch/tests/test_views.py +++ b/twitch/tests/test_views.py @@ -12,6 +12,9 @@ from django.test import RequestFactory from django.urls import reverse from django.utils import timezone +from kick.models import KickCategory +from kick.models import KickDropCampaign +from kick.models import KickOrganization from twitch.models import Channel from twitch.models import ChatBadge from twitch.models import ChatBadgeSet @@ -1321,19 +1324,61 @@ class TestSitemapView: name="ch1", display_name="Channel 1", ) + now: datetime = timezone.now() campaign: DropCampaign = DropCampaign.objects.create( twitch_id="camp1", name="Test Campaign", description="Desc", game=game, operation_names=["DropCampaignDetails"], + start_at=now - datetime.timedelta(days=1), + end_at=now + datetime.timedelta(days=1), ) + inactive_campaign: DropCampaign = DropCampaign.objects.create( + twitch_id="camp2", + name="Inactive Campaign", + description="Desc", + game=game, + operation_names=["DropCampaignDetails"], + start_at=now - datetime.timedelta(days=10), + end_at=now - datetime.timedelta(days=5), + ) + + kick_org: KickOrganization = KickOrganization.objects.create( + kick_id="org1", + name="Kick Org", + ) + kick_cat: KickCategory = KickCategory.objects.create( + kick_id=1, + name="Kick Game", + slug="kick-game", + ) + kick_active: KickDropCampaign = KickDropCampaign.objects.create( + kick_id="kcamp1", + name="Kick Active Campaign", + organization=kick_org, + category=kick_cat, + starts_at=now - datetime.timedelta(days=1), + ends_at=now + datetime.timedelta(days=1), + ) + kick_inactive: KickDropCampaign = KickDropCampaign.objects.create( + kick_id="kcamp2", + name="Kick Inactive Campaign", + organization=kick_org, + category=kick_cat, + starts_at=now - datetime.timedelta(days=10), + ends_at=now - datetime.timedelta(days=5), + ) + badge: ChatBadgeSet = ChatBadgeSet.objects.create(set_id="badge1") return { "org": org, "game": game, "channel": channel, "campaign": campaign, + "inactive_campaign": inactive_campaign, + "kick_active": kick_active, + "kick_inactive": kick_inactive, "badge": badge, } @@ -1357,16 +1402,24 @@ class TestSitemapView: content = response.content.decode() assert content.startswith('') - def test_sitemap_contains_urlset( + def test_sitemap_contains_sitemap_index( self, client: Client, sample_entities: dict[str, Any], ) -> None: - """Test sitemap contains urlset element.""" + """Test sitemap index contains sitemap locations.""" response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml") content: str = response.content.decode() - assert "" in content + assert "" in content + assert "/sitemap-static.xml" in content + assert "/sitemap-twitch-channels.xml" in content + assert "/sitemap-twitch-drops.xml" in content + assert "/sitemap-kick.xml" in content + assert "/sitemap-youtube.xml" in content + + # Ensure at least one entry includes a lastmod (there are entities created by the fixture) + assert "" in content def test_sitemap_contains_static_pages( self, @@ -1374,15 +1427,19 @@ class TestSitemapView: sample_entities: dict[str, Any], ) -> None: """Test sitemap includes static pages.""" - response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml") + response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-static.xml") content: str = response.content.decode() - # Check for some static pages + + # Check for the homepage and a few key list views across apps. assert ( "http://testserver/" in content or "http://localhost:8000/" in content ) - assert "/campaigns/" in content - assert "/games/" in content + assert "http://testserver/twitch/" in content + assert "http://testserver/kick/" in content + assert "http://testserver/youtube/" in content + assert "http://testserver/twitch/campaigns/" in content + assert "http://testserver/twitch/games/" in content def test_sitemap_contains_game_detail_pages( self, @@ -1391,7 +1448,7 @@ class TestSitemapView: ) -> None: """Test sitemap includes game detail pages.""" game: Game = sample_entities["game"] - response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml") + response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-others.xml") content: str = response.content.decode() assert f"/games/{game.twitch_id}/" in content @@ -1402,10 +1459,62 @@ class TestSitemapView: ) -> None: """Test sitemap includes campaign detail pages.""" campaign: DropCampaign = sample_entities["campaign"] - response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml") + response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-drops.xml") content: str = response.content.decode() assert f"/campaigns/{campaign.twitch_id}/" in content + def test_sitemap_prioritizes_active_drops( + self, + client: Client, + sample_entities: dict[str, Any], + ) -> None: + """Test active drops are prioritised and crawled more frequently.""" + active_campaign: DropCampaign = sample_entities["campaign"] + inactive_campaign: DropCampaign = sample_entities["inactive_campaign"] + + response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-drops.xml") + content: str = response.content.decode() + + active_loc: str = f"http://testserver/twitch/campaigns/{active_campaign.twitch_id}/" + active_index: int = content.find(active_loc) + assert active_index != -1 + active_end: int = content.find("", active_index) + assert active_end != -1 + + inactive_loc: str = f"http://testserver/twitch/campaigns/{inactive_campaign.twitch_id}/" + inactive_index: int = content.find(inactive_loc) + assert inactive_index != -1 + inactive_end: int = content.find("", inactive_index) + assert inactive_end != -1 + + def test_sitemap_prioritizes_active_kick_campaigns( + self, + client: Client, + sample_entities: dict[str, Any], + ) -> None: + """Test active Kick campaigns are prioritised and crawled more frequently.""" + active_campaign: KickDropCampaign = sample_entities["kick_active"] + inactive_campaign: KickDropCampaign = sample_entities["kick_inactive"] + + response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-kick.xml") + content: str = response.content.decode() + + active_loc: str = ( + f"http://testserver/kick/campaigns/{active_campaign.kick_id}/" + ) + active_index: int = content.find(active_loc) + assert active_index != -1 + active_end: int = content.find("", active_index) + assert active_end != -1 + + inactive_loc: str = ( + f"http://testserver/kick/campaigns/{inactive_campaign.kick_id}/" + ) + inactive_index: int = content.find(inactive_loc) + assert inactive_index != -1 + inactive_end: int = content.find("", inactive_index) + assert inactive_end != -1 + def test_sitemap_contains_organization_detail_pages( self, client: Client, @@ -1413,7 +1522,7 @@ class TestSitemapView: ) -> None: """Test sitemap includes organization detail pages.""" org: Organization = sample_entities["org"] - response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml") + response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-others.xml") content: str = response.content.decode() assert f"/organizations/{org.twitch_id}/" in content @@ -1424,7 +1533,9 @@ class TestSitemapView: ) -> None: """Test sitemap includes channel detail pages.""" channel: Channel = sample_entities["channel"] - response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml") + response: _MonkeyPatchedWSGIResponse = client.get( + "/sitemap-twitch-channels.xml", + ) content: str = response.content.decode() assert f"/twitch/channels/{channel.twitch_id}/" in content @@ -1435,31 +1546,19 @@ class TestSitemapView: ) -> None: """Test sitemap includes badge detail pages.""" badge: ChatBadge = sample_entities["badge"] - response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml") + response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-others.xml") content: str = response.content.decode() assert f"/badges/{badge.set_id}/" in content # pyright: ignore[reportAttributeAccessIssue] - def test_sitemap_includes_priority( + def test_sitemap_contains_youtube_pages( self, client: Client, sample_entities: dict[str, Any], ) -> None: - """Test sitemap includes priority values.""" - response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml") + """Test sitemap includes YouTube landing page.""" + response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-youtube.xml") content: str = response.content.decode() - assert "" in content - assert "" in content - - def test_sitemap_includes_changefreq( - self, - client: Client, - sample_entities: dict[str, Any], - ) -> None: - """Test sitemap includes changefreq values.""" - response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml") - content: str = response.content.decode() - assert "" in content - assert "" in content + assert "/youtube/" in content def test_sitemap_includes_lastmod( self, @@ -1467,7 +1566,7 @@ class TestSitemapView: sample_entities: dict[str, Any], ) -> None: """Test sitemap includes lastmod for detail pages.""" - response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml") + response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-others.xml") content: str = response.content.decode() # Check for lastmod in game or campaign entries assert "" in content @@ -1730,7 +1829,7 @@ class TestImageObjectStructuredData: org: Organization, monkeypatch: pytest.MonkeyPatch, ) -> None: - """VideoGame ImageObject should carry attribution and license metadata.""" + """VideoGame ImageObject should carry attribution metadata.""" url: str = reverse("twitch:game_detail", args=[game.twitch_id]) response: _MonkeyPatchedWSGIResponse = client.get(url) @@ -1743,14 +1842,6 @@ class TestImageObjectStructuredData: "name": org.name, "url": f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}", } - assert ( - img["license"] - == f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}" - ) - assert ( - img["acquireLicensePage"] - == f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}" - ) def test_game_schema_no_image_when_no_box_art( self, @@ -1850,7 +1941,7 @@ class TestImageObjectStructuredData: org: Organization, monkeypatch: pytest.MonkeyPatch, ) -> None: - """Event ImageObject should carry attribution and license metadata.""" + """Event ImageObject should carry attribution metadata.""" url: str = reverse("twitch:campaign_detail", args=[campaign.twitch_id]) response: _MonkeyPatchedWSGIResponse = client.get(url) @@ -1863,14 +1954,6 @@ class TestImageObjectStructuredData: "name": org.name, "url": f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}", } - assert ( - img["license"] - == f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}" - ) - assert ( - img["acquireLicensePage"] - == f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}" - ) def test_campaign_schema_no_image_when_no_image_url( self, @@ -1947,8 +2030,6 @@ class TestImageObjectStructuredData: "name": "Twitch", "url": "https://www.twitch.tv/", } - assert schema["image"]["license"] == "https://www.twitch.tv/" - assert schema["image"]["acquireLicensePage"] == "https://www.twitch.tv/" assert "organizer" not in schema # --- _pick_owner / Twitch Gaming skipping --- diff --git a/twitch/views.py b/twitch/views.py index e5db91f..f13ab43 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -79,7 +79,7 @@ def _build_image_object( *, copyright_notice: str | None = None, ) -> dict[str, Any]: - """Build a Schema.org ImageObject with attribution and license metadata. + """Build a Schema.org ImageObject with attribution metadata. Args: request: The HTTP request used for absolute URL building. @@ -96,14 +96,13 @@ def _build_image_object( "name": creator_name, "url": creator_url, } + return { "@type": "ImageObject", "contentUrl": request.build_absolute_uri(image_url), "creditText": creator_name, "copyrightNotice": copyright_notice or creator_name, "creator": creator, - "license": creator_url, - "acquireLicensePage": creator_url, }