Compare commits

...

2 commits

Author SHA1 Message Date
02ea6314c3
Use several sitemaps
All checks were successful
Deploy to Server / deploy (push) Successful in 11s
2026-03-17 18:14:27 +01:00
f04d88e8fd
Refactor ImageObject schema to remove license metadata 2026-03-17 16:24:28 +01:00
4 changed files with 476 additions and 151 deletions

View file

@ -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

View file

@ -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 <urlset> sitemap XML string from URL entries.
Args:
url_entries: List of dictionaries containing URL entry data.
Returns:
A string containing the rendered XML.
"""
xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
for url_entry in url_entries:
xml += " <url>\n"
xml += f" <loc>{url_entry['url']}</loc>\n"
if url_entry.get("lastmod"):
xml += f" <lastmod>{url_entry['lastmod']}</lastmod>\n"
xml += " </url>\n"
xml += "</urlset>"
return xml
def _render_sitemap_index_xml(sitemap_entries: list[dict[str, str]]) -> str:
"""Render a <sitemapindex> XML string listing sitemap URLs.
Args:
sitemap_entries: List of dictionaries with "loc" and optional "lastmod".
Returns:
A string containing the rendered XML.
"""
xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
xml += '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
for entry in sitemap_entries:
xml += " <sitemap>\n"
xml += f" <loc>{entry['loc']}</loc>\n"
if entry.get("lastmod"):
xml += f" <lastmod>{entry['lastmod']}</lastmod>\n"
xml += " </sitemap>\n"
xml += "</sitemapindex>"
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 = '<?xml version="1.0" encoding="UTF-8"?>\n'
xml_content += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\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 += " <url>\n"
xml_content += f" <loc>{url_entry['url']}</loc>\n"
if url_entry.get("lastmod"):
xml_content += f" <lastmod>{url_entry['lastmod']}</lastmod>\n"
xml_content += (
f" <changefreq>{url_entry.get('changefreq', 'monthly')}</changefreq>\n"
)
xml_content += f" <priority>{url_entry.get('priority', '0.5')}</priority>\n"
xml_content += " </url>\n"
xml_content += "</urlset>"
# 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")

View file

@ -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('<?xml version="1.0" encoding="UTF-8"?>')
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 "<urlset" in content
assert "</urlset>" in content
assert "<sitemapindex" in content
assert "</sitemapindex>" 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 "<lastmod>" 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 (
"<loc>http://testserver/</loc>" in content
or "<loc>http://localhost:8000/</loc>" 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"<loc>http://testserver/twitch/campaigns/{active_campaign.twitch_id}/</loc>"
active_index: int = content.find(active_loc)
assert active_index != -1
active_end: int = content.find("</url>", active_index)
assert active_end != -1
inactive_loc: str = f"<loc>http://testserver/twitch/campaigns/{inactive_campaign.twitch_id}/</loc>"
inactive_index: int = content.find(inactive_loc)
assert inactive_index != -1
inactive_end: int = content.find("</url>", 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"<loc>http://testserver/kick/campaigns/{active_campaign.kick_id}/</loc>"
)
active_index: int = content.find(active_loc)
assert active_index != -1
active_end: int = content.find("</url>", active_index)
assert active_end != -1
inactive_loc: str = (
f"<loc>http://testserver/kick/campaigns/{inactive_campaign.kick_id}/</loc>"
)
inactive_index: int = content.find(inactive_loc)
assert inactive_index != -1
inactive_end: int = content.find("</url>", 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 "<priority>" in content
assert "</priority>" 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 "<changefreq>" in content
assert "</changefreq>" 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 "<lastmod>" 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 ---

View file

@ -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,
}