Improve SEO?
This commit is contained in:
parent
efed2c2f69
commit
725df27b47
8 changed files with 353 additions and 128 deletions
17
core/seo.py
Normal file
17
core/seo.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from typing import Any
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class SeoMeta(TypedDict, total=False):
|
||||||
|
"""Shared typed optional SEO metadata for template context generation."""
|
||||||
|
|
||||||
|
page_image: str | None
|
||||||
|
page_image_width: int | None
|
||||||
|
page_image_height: int | None
|
||||||
|
og_type: str
|
||||||
|
schema_data: dict[str, Any] | None
|
||||||
|
breadcrumb_schema: dict[str, Any] | None
|
||||||
|
pagination_info: list[dict[str, str]] | None
|
||||||
|
published_date: str | None
|
||||||
|
modified_date: str | None
|
||||||
|
robots_directive: str
|
||||||
126
kick/views.py
126
kick/views.py
|
|
@ -1,6 +1,9 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import Any
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from django.core.paginator import EmptyPage
|
from django.core.paginator import EmptyPage
|
||||||
|
|
@ -24,6 +27,7 @@ if TYPE_CHECKING:
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
from core.seo import SeoMeta
|
||||||
from kick.models import KickChannel
|
from kick.models import KickChannel
|
||||||
from kick.models import KickReward
|
from kick.models import KickReward
|
||||||
|
|
||||||
|
|
@ -33,26 +37,36 @@ logger: logging.Logger = logging.getLogger("ttvdrops.kick.views")
|
||||||
def _build_seo_context(
|
def _build_seo_context(
|
||||||
page_title: str = "Kick Drops",
|
page_title: str = "Kick Drops",
|
||||||
page_description: str | None = None,
|
page_description: str | None = None,
|
||||||
og_type: str = "website",
|
seo_meta: SeoMeta | None = None,
|
||||||
robots_directive: str = "index, follow",
|
) -> dict[str, Any]:
|
||||||
) -> dict[str, str]:
|
"""Build SEO context for template rendering.
|
||||||
"""Build minimal SEO context for template rendering.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
page_title: The title of the page for <title> and OG tags.
|
page_title: The title of the page for <title> and OG tags.
|
||||||
page_description: Optional description for meta and OG tags.
|
page_description: Optional description for meta and OG tags.
|
||||||
og_type: Open Graph type (default "website").
|
seo_meta: Optional typed SEO metadata.
|
||||||
robots_directive: Value for meta robots tag (default "index, follow").
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A dictionary with SEO-related context variables.
|
A dictionary with SEO-related context variables.
|
||||||
"""
|
"""
|
||||||
return {
|
context: dict[str, Any] = {
|
||||||
"page_title": page_title,
|
"page_title": page_title,
|
||||||
"page_description": page_description or "Archive of Kick drops.",
|
"page_description": page_description or "Archive of Kick drops.",
|
||||||
"og_type": og_type,
|
"og_type": "website",
|
||||||
"robots_directive": robots_directive,
|
"robots_directive": "index, follow",
|
||||||
}
|
}
|
||||||
|
if seo_meta:
|
||||||
|
if seo_meta.get("og_type"):
|
||||||
|
context["og_type"] = seo_meta["og_type"]
|
||||||
|
if seo_meta.get("robots_directive"):
|
||||||
|
context["robots_directive"] = seo_meta["robots_directive"]
|
||||||
|
if seo_meta.get("schema_data"):
|
||||||
|
context["schema_data"] = json.dumps(seo_meta["schema_data"])
|
||||||
|
if seo_meta.get("published_date"):
|
||||||
|
context["published_date"] = seo_meta["published_date"]
|
||||||
|
if seo_meta.get("modified_date"):
|
||||||
|
context["modified_date"] = seo_meta["modified_date"]
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
def _build_breadcrumb_schema(items: list[dict[str, str]]) -> str:
|
def _build_breadcrumb_schema(items: list[dict[str, str]]) -> str:
|
||||||
|
|
@ -278,9 +292,51 @@ def campaign_detail_view(request: HttpRequest, kick_id: str) -> HttpResponse:
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
seo_context: dict[str, str] = _build_seo_context(
|
campaign_url: str = request.build_absolute_uri(
|
||||||
|
reverse("kick:campaign_detail", args=[campaign.kick_id]),
|
||||||
|
)
|
||||||
|
campaign_event: dict[str, Any] = {
|
||||||
|
"@type": "Event",
|
||||||
|
"name": campaign.name,
|
||||||
|
"description": f"Kick drop campaign: {campaign.name}.",
|
||||||
|
"url": campaign_url,
|
||||||
|
"eventStatus": "https://schema.org/EventScheduled",
|
||||||
|
"eventAttendanceMode": "https://schema.org/OnlineEventAttendanceMode",
|
||||||
|
"location": {"@type": "VirtualLocation", "url": "https://kick.com"},
|
||||||
|
}
|
||||||
|
if campaign.starts_at:
|
||||||
|
campaign_event["startDate"] = campaign.starts_at.isoformat()
|
||||||
|
if campaign.ends_at:
|
||||||
|
campaign_event["endDate"] = campaign.ends_at.isoformat()
|
||||||
|
if campaign.organization:
|
||||||
|
campaign_event["organizer"] = {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": campaign.organization.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
webpage_node: dict[str, Any] = {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"url": campaign_url,
|
||||||
|
"datePublished": campaign.added_at.isoformat(),
|
||||||
|
"dateModified": campaign.updated_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
campaign_schema: dict[str, Any] = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [
|
||||||
|
campaign_event,
|
||||||
|
webpage_node,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=campaign.name,
|
page_title=campaign.name,
|
||||||
page_description=f"Kick drop campaign: {campaign.name}.",
|
page_description=f"Kick drop campaign: {campaign.name}.",
|
||||||
|
seo_meta={
|
||||||
|
"schema_data": campaign_schema,
|
||||||
|
"published_date": campaign.added_at.isoformat(),
|
||||||
|
"modified_date": campaign.updated_at.isoformat(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|
@ -379,9 +435,27 @@ def category_detail_view(request: HttpRequest, kick_id: int) -> HttpResponse:
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
seo_context: dict[str, str] = _build_seo_context(
|
category_url: str = request.build_absolute_uri(
|
||||||
|
reverse("kick:game_detail", args=[category.kick_id]),
|
||||||
|
)
|
||||||
|
category_schema: dict[str, Any] = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "CollectionPage",
|
||||||
|
"name": category.name,
|
||||||
|
"description": f"Kick drop campaigns for {category.name}.",
|
||||||
|
"url": category_url,
|
||||||
|
"datePublished": category.added_at.isoformat(),
|
||||||
|
"dateModified": category.updated_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=category.name,
|
page_title=category.name,
|
||||||
page_description=f"Kick drop campaigns for {category.name}.",
|
page_description=f"Kick drop campaigns for {category.name}.",
|
||||||
|
seo_meta={
|
||||||
|
"schema_data": category_schema,
|
||||||
|
"published_date": category.added_at.isoformat(),
|
||||||
|
"modified_date": category.updated_at.isoformat(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|
@ -466,9 +540,37 @@ def organization_detail_view(request: HttpRequest, kick_id: str) -> HttpResponse
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
seo_context: dict[str, str] = _build_seo_context(
|
org_url: str = request.build_absolute_uri(
|
||||||
|
reverse("kick:organization_detail", args=[org.kick_id]),
|
||||||
|
)
|
||||||
|
organization_node: dict[str, Any] = {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": org.name,
|
||||||
|
"url": org_url,
|
||||||
|
"description": f"Kick drop campaigns by {org.name}.",
|
||||||
|
}
|
||||||
|
webpage_node: dict[str, Any] = {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"url": org_url,
|
||||||
|
"datePublished": org.added_at.isoformat(),
|
||||||
|
"dateModified": org.updated_at.isoformat(),
|
||||||
|
}
|
||||||
|
org_schema: dict[str, Any] = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [
|
||||||
|
organization_node,
|
||||||
|
webpage_node,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=org.name,
|
page_title=org.name,
|
||||||
page_description=f"Kick drop campaigns by {org.name}.",
|
page_description=f"Kick drop campaigns by {org.name}.",
|
||||||
|
seo_meta={
|
||||||
|
"schema_data": org_schema,
|
||||||
|
"published_date": org.added_at.isoformat(),
|
||||||
|
"modified_date": org.updated_at.isoformat(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,14 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- Campaign description -->
|
<!-- Campaign description -->
|
||||||
<p>{{ campaign.description|linebreaksbr }}</p>
|
<p>{{ campaign.description|linebreaksbr }}</p>
|
||||||
|
<small>
|
||||||
|
Published:
|
||||||
|
<time datetime="{{ campaign.added_at|date:'c' }}"
|
||||||
|
title="{{ campaign.added_at|date:'DATETIME_FORMAT' }}">{{ campaign.added_at|date:"M d, Y H:i" }}</time>
|
||||||
|
· Last updated:
|
||||||
|
<time datetime="{{ campaign.updated_at|date:'c' }}"
|
||||||
|
title="{{ campaign.updated_at|date:'DATETIME_FORMAT' }}">{{ campaign.updated_at|date:"M d, Y H:i" }}</time>
|
||||||
|
</small>
|
||||||
<!-- Campaign end times -->
|
<!-- Campaign end times -->
|
||||||
<div>
|
<div>
|
||||||
{% if campaign.end_at < now %}
|
{% if campaign.end_at < now %}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,14 @@
|
||||||
</iframe>
|
</iframe>
|
||||||
<!-- Channel Info -->
|
<!-- Channel Info -->
|
||||||
<p>Channel ID: {{ channel.twitch_id }}</p>
|
<p>Channel ID: {{ channel.twitch_id }}</p>
|
||||||
|
<p>
|
||||||
|
Published:
|
||||||
|
<time datetime="{{ channel.added_at|date:'c' }}"
|
||||||
|
title="{{ channel.added_at|date:'DATETIME_FORMAT' }}">{{ channel.added_at|date:"M d, Y H:i" }}</time>
|
||||||
|
· Last updated:
|
||||||
|
<time datetime="{{ channel.updated_at|date:'c' }}"
|
||||||
|
title="{{ channel.updated_at|date:'DATETIME_FORMAT' }}">{{ channel.updated_at|date:"M d, Y H:i" }}</time>
|
||||||
|
</p>
|
||||||
{% if active_campaigns %}
|
{% if active_campaigns %}
|
||||||
<h5>Active Campaigns</h5>
|
<h5>Active Campaigns</h5>
|
||||||
<table>
|
<table>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,14 @@
|
||||||
Twitch ID: <a href="https://www.twitch.tv/directory/category/{{ game.slug|urlencode }}">{{ game.twitch_id }}</a>
|
Twitch ID: <a href="https://www.twitch.tv/directory/category/{{ game.slug|urlencode }}">{{ game.twitch_id }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div>Twitch slug: {{ game.slug }}</div>
|
<div>Twitch slug: {{ game.slug }}</div>
|
||||||
|
<small>
|
||||||
|
Published:
|
||||||
|
<time datetime="{{ game.added_at|date:'c' }}"
|
||||||
|
title="{{ game.added_at|date:'DATETIME_FORMAT' }}">{{ game.added_at|date:"M d, Y H:i" }}</time>
|
||||||
|
· Last updated:
|
||||||
|
<time datetime="{{ game.updated_at|date:'c' }}"
|
||||||
|
title="{{ game.updated_at|date:'DATETIME_FORMAT' }}">{{ game.updated_at|date:"M d, Y H:i" }}</time>
|
||||||
|
</small>
|
||||||
<!-- RSS Feeds -->
|
<!-- RSS Feeds -->
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'core:game_campaign_feed' game.twitch_id %}"
|
<a href="{% url 'core:game_campaign_feed' game.twitch_id %}"
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,22 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock extra_head %}
|
{% endblock extra_head %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 id="org-name">{{ organization.name }}</h1>
|
<h1>{{ organization.name }}</h1>
|
||||||
<theader>
|
<p>
|
||||||
<h2 id="games-header">Games by {{ organization.name }}</h2>
|
Published:
|
||||||
</theader>
|
<time datetime="{{ organization.added_at|date:'c' }}"
|
||||||
<table id="games-table">
|
title="{{ organization.added_at|date:'DATETIME_FORMAT' }}">{{ organization.added_at|date:"M d, Y H:i" }}</time>
|
||||||
|
· Last updated:
|
||||||
|
<time datetime="{{ organization.updated_at|date:'c' }}"
|
||||||
|
title="{{ organization.updated_at|date:'DATETIME_FORMAT' }}">{{ organization.updated_at|date:"M d, Y H:i" }}</time>
|
||||||
|
</p>
|
||||||
|
<header>
|
||||||
|
<h2>Games by {{ organization.name }}</h2>
|
||||||
|
</header>
|
||||||
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for game in games %}
|
{% for game in games %}
|
||||||
<tr id="game-row-{{ game.twitch_id }}">
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'twitch:game_detail' game.twitch_id %}">{{ game }}</a>
|
<a href="{% url 'twitch:game_detail' game.twitch_id %}">{{ game }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -1051,9 +1051,11 @@ class TestSEOHelperFunctions:
|
||||||
context: dict[str, Any] = _build_seo_context(
|
context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Test Title",
|
page_title="Test Title",
|
||||||
page_description="Test Description",
|
page_description="Test Description",
|
||||||
page_image="https://example.com/image.jpg",
|
seo_meta={
|
||||||
og_type="article",
|
"page_image": "https://example.com/image.jpg",
|
||||||
schema_data={"@context": "https://schema.org"},
|
"og_type": "article",
|
||||||
|
"schema_data": {"@context": "https://schema.org"},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert context["page_title"] == "Test Title"
|
assert context["page_title"] == "Test Title"
|
||||||
|
|
@ -1083,14 +1085,16 @@ class TestSEOHelperFunctions:
|
||||||
context: dict[str, Any] = _build_seo_context(
|
context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Test",
|
page_title="Test",
|
||||||
page_description="Desc",
|
page_description="Desc",
|
||||||
page_image="https://example.com/img.jpg",
|
seo_meta={
|
||||||
og_type="article",
|
"page_image": "https://example.com/img.jpg",
|
||||||
schema_data={},
|
"og_type": "article",
|
||||||
breadcrumb_schema=breadcrumb,
|
"schema_data": {},
|
||||||
pagination_info=[{"rel": "next", "url": "/page/2/"}],
|
"breadcrumb_schema": breadcrumb,
|
||||||
published_date=now.isoformat(),
|
"pagination_info": [{"rel": "next", "url": "/page/2/"}],
|
||||||
modified_date=now.isoformat(),
|
"published_date": now.isoformat(),
|
||||||
robots_directive="noindex, follow",
|
"modified_date": now.isoformat(),
|
||||||
|
"robots_directive": "noindex, follow",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# breadcrumb_schema is JSON-dumped, so parse it back
|
# breadcrumb_schema is JSON-dumped, so parse it back
|
||||||
|
|
|
||||||
264
twitch/views.py
264
twitch/views.py
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
|
@ -44,6 +46,8 @@ if TYPE_CHECKING:
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
|
from core.seo import SeoMeta
|
||||||
|
|
||||||
logger: logging.Logger = logging.getLogger("ttvdrops.views")
|
logger: logging.Logger = logging.getLogger("ttvdrops.views")
|
||||||
|
|
||||||
MIN_QUERY_LENGTH_FOR_FTS = 3
|
MIN_QUERY_LENGTH_FOR_FTS = 3
|
||||||
|
|
@ -89,66 +93,48 @@ def _truncate_description(text: str, max_length: int = 160) -> str:
|
||||||
return text[:max_length].rsplit(" ", 1)[0] + "…"
|
return text[:max_length].rsplit(" ", 1)[0] + "…"
|
||||||
|
|
||||||
|
|
||||||
def _build_seo_context( # noqa: PLR0913, PLR0917
|
def _build_seo_context(
|
||||||
page_title: str = "ttvdrops",
|
page_title: str = "ttvdrops",
|
||||||
page_description: str | None = None,
|
page_description: str | None = None,
|
||||||
page_image: str | None = None,
|
seo_meta: SeoMeta | None = None,
|
||||||
page_image_width: int | None = None,
|
|
||||||
page_image_height: int | None = None,
|
|
||||||
og_type: str = "website",
|
|
||||||
schema_data: dict[str, Any] | None = None,
|
|
||||||
breadcrumb_schema: dict[str, Any] | None = None,
|
|
||||||
pagination_info: list[dict[str, str]] | None = None,
|
|
||||||
published_date: str | None = None,
|
|
||||||
modified_date: str | None = None,
|
|
||||||
robots_directive: str = "index, follow",
|
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Build SEO context for template rendering.
|
"""Build SEO context for template rendering.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
page_title: Page title (shown in browser tab, og:title).
|
page_title: Page title (shown in browser tab, og:title).
|
||||||
page_description: Page description (meta description, og:description).
|
page_description: Page description (meta description, og:description).
|
||||||
page_image: Image URL for og:image meta tag.
|
seo_meta: Optional typed SEO metadata with image, schema, breadcrumb,
|
||||||
page_image_width: Width of the image in pixels.
|
pagination, OpenGraph, and date fields.
|
||||||
page_image_height: Height of the image in pixels.
|
|
||||||
og_type: OpenGraph type (e.g., "website", "article").
|
|
||||||
schema_data: Dict representation of Schema.org JSON-LD data.
|
|
||||||
breadcrumb_schema: Breadcrumb schema dict for navigation hierarchy.
|
|
||||||
pagination_info: List of dicts with "rel" (prev|next|first|last) and "url".
|
|
||||||
published_date: ISO 8601 published date (e.g., "2025-01-01T00:00:00Z").
|
|
||||||
modified_date: ISO 8601 modified date.
|
|
||||||
robots_directive: Robots meta content (e.g., "index, follow" or "noindex").
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with SEO context variables to pass to render().
|
Dict with SEO context variables to pass to render().
|
||||||
"""
|
"""
|
||||||
# TODO(TheLovinator): Instead of having so many parameters, # noqa: TD003
|
|
||||||
# consider having a single "seo_info" parameter that
|
|
||||||
# can contain all of these optional fields. This would make
|
|
||||||
# it easier to extend in the future without changing the
|
|
||||||
# function signature.
|
|
||||||
|
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"page_title": page_title,
|
"page_title": page_title,
|
||||||
"page_description": page_description or DEFAULT_SITE_DESCRIPTION,
|
"page_description": page_description or DEFAULT_SITE_DESCRIPTION,
|
||||||
"og_type": og_type,
|
"og_type": "website",
|
||||||
"robots_directive": robots_directive,
|
"robots_directive": "index, follow",
|
||||||
}
|
}
|
||||||
if page_image:
|
if seo_meta:
|
||||||
context["page_image"] = page_image
|
if seo_meta.get("og_type"):
|
||||||
if page_image_width and page_image_height:
|
context["og_type"] = seo_meta["og_type"]
|
||||||
context["page_image_width"] = page_image_width
|
if seo_meta.get("robots_directive"):
|
||||||
context["page_image_height"] = page_image_height
|
context["robots_directive"] = seo_meta["robots_directive"]
|
||||||
if schema_data:
|
if seo_meta.get("page_image"):
|
||||||
context["schema_data"] = json.dumps(schema_data)
|
context["page_image"] = seo_meta["page_image"]
|
||||||
if breadcrumb_schema:
|
if seo_meta.get("page_image_width") and seo_meta.get("page_image_height"):
|
||||||
context["breadcrumb_schema"] = json.dumps(breadcrumb_schema)
|
context["page_image_width"] = seo_meta["page_image_width"]
|
||||||
if pagination_info:
|
context["page_image_height"] = seo_meta["page_image_height"]
|
||||||
context["pagination_info"] = pagination_info
|
if seo_meta.get("schema_data"):
|
||||||
if published_date:
|
context["schema_data"] = json.dumps(seo_meta["schema_data"])
|
||||||
context["published_date"] = published_date
|
if seo_meta.get("breadcrumb_schema"):
|
||||||
if modified_date:
|
context["breadcrumb_schema"] = json.dumps(seo_meta["breadcrumb_schema"])
|
||||||
context["modified_date"] = modified_date
|
if seo_meta.get("pagination_info"):
|
||||||
|
context["pagination_info"] = seo_meta["pagination_info"]
|
||||||
|
if seo_meta.get("published_date"):
|
||||||
|
context["published_date"] = seo_meta["published_date"]
|
||||||
|
if seo_meta.get("modified_date"):
|
||||||
|
context["modified_date"] = seo_meta["modified_date"]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -296,7 +282,7 @@ def org_list_view(request: HttpRequest) -> HttpResponse:
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Twitch Organizations",
|
page_title="Twitch Organizations",
|
||||||
page_description="List of Twitch organizations.",
|
page_description="List of Twitch organizations.",
|
||||||
schema_data=collection_schema,
|
seo_meta={"schema_data": collection_schema},
|
||||||
)
|
)
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"orgs": orgs,
|
"orgs": orgs,
|
||||||
|
|
@ -361,13 +347,25 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon
|
||||||
url: str = request.build_absolute_uri(
|
url: str = request.build_absolute_uri(
|
||||||
reverse("twitch:organization_detail", args=[organization.twitch_id]),
|
reverse("twitch:organization_detail", args=[organization.twitch_id]),
|
||||||
)
|
)
|
||||||
org_schema: dict[str, str | dict[str, str]] = {
|
organization_node: dict[str, Any] = {
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": org_name,
|
"name": org_name,
|
||||||
"url": url,
|
"url": url,
|
||||||
"description": org_description,
|
"description": org_description,
|
||||||
}
|
}
|
||||||
|
webpage_node: dict[str, Any] = {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"url": url,
|
||||||
|
"datePublished": organization.added_at.isoformat(),
|
||||||
|
"dateModified": organization.updated_at.isoformat(),
|
||||||
|
}
|
||||||
|
org_schema: dict[str, Any] = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [
|
||||||
|
organization_node,
|
||||||
|
webpage_node,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
# Breadcrumb schema
|
# Breadcrumb schema
|
||||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
|
|
@ -384,9 +382,12 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=org_name,
|
page_title=org_name,
|
||||||
page_description=org_description,
|
page_description=org_description,
|
||||||
schema_data=org_schema,
|
seo_meta={
|
||||||
breadcrumb_schema=breadcrumb_schema,
|
"schema_data": org_schema,
|
||||||
modified_date=organization.updated_at.isoformat(),
|
"breadcrumb_schema": breadcrumb_schema,
|
||||||
|
"published_date": organization.added_at.isoformat(),
|
||||||
|
"modified_date": organization.updated_at.isoformat(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"organization": organization,
|
"organization": organization,
|
||||||
|
|
@ -481,8 +482,10 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0
|
||||||
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,
|
||||||
pagination_info=pagination_info,
|
seo_meta={
|
||||||
schema_data=collection_schema,
|
"pagination_info": pagination_info,
|
||||||
|
"schema_data": collection_schema,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"campaigns": campaigns,
|
"campaigns": campaigns,
|
||||||
|
|
@ -724,7 +727,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO(TheLovinator): If the campaign has specific allowed channels, we could list those as potential locations instead of just linking to Twitch homepage. # noqa: TD003
|
# TODO(TheLovinator): If the campaign has specific allowed channels, we could list those as potential locations instead of just linking to Twitch homepage. # noqa: TD003
|
||||||
campaign_schema: dict[str, str | dict[str, str]] = {
|
campaign_event: dict[str, Any] = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Event",
|
"@type": "Event",
|
||||||
"name": campaign_name,
|
"name": campaign_name,
|
||||||
|
|
@ -738,9 +741,9 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if campaign.start_at:
|
if campaign.start_at:
|
||||||
campaign_schema["startDate"] = campaign.start_at.isoformat()
|
campaign_event["startDate"] = campaign.start_at.isoformat()
|
||||||
if campaign.end_at:
|
if campaign.end_at:
|
||||||
campaign_schema["endDate"] = campaign.end_at.isoformat()
|
campaign_event["endDate"] = campaign.end_at.isoformat()
|
||||||
campaign_owner: Organization | None = (
|
campaign_owner: Organization | None = (
|
||||||
_pick_owner(list(campaign.game.owners.all())) if campaign.game else None
|
_pick_owner(list(campaign.game.owners.all())) if campaign.game else None
|
||||||
)
|
)
|
||||||
|
|
@ -750,17 +753,25 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
||||||
else "Twitch"
|
else "Twitch"
|
||||||
)
|
)
|
||||||
if campaign_image:
|
if campaign_image:
|
||||||
campaign_schema["image"] = {
|
campaign_event["image"] = {
|
||||||
"@type": "ImageObject",
|
"@type": "ImageObject",
|
||||||
"contentUrl": request.build_absolute_uri(campaign_image),
|
"contentUrl": request.build_absolute_uri(campaign_image),
|
||||||
"creditText": campaign_owner_name,
|
"creditText": campaign_owner_name,
|
||||||
"copyrightNotice": campaign_owner_name,
|
"copyrightNotice": campaign_owner_name,
|
||||||
}
|
}
|
||||||
if campaign_owner:
|
if campaign_owner:
|
||||||
campaign_schema["organizer"] = {
|
campaign_event["organizer"] = {
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": campaign_owner_name,
|
"name": campaign_owner_name,
|
||||||
}
|
}
|
||||||
|
webpage_node: dict[str, Any] = {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"url": url,
|
||||||
|
"datePublished": campaign.added_at.isoformat(),
|
||||||
|
"dateModified": campaign.updated_at.isoformat(),
|
||||||
|
}
|
||||||
|
campaign_event["mainEntityOfPage"] = webpage_node
|
||||||
|
campaign_schema: dict[str, Any] = campaign_event
|
||||||
|
|
||||||
# Breadcrumb schema for navigation
|
# Breadcrumb schema for navigation
|
||||||
# TODO(TheLovinator): We should have a game.get_display_name() method that encapsulates the logic of choosing between display_name, name, and twitch_id. # noqa: TD003
|
# TODO(TheLovinator): We should have a game.get_display_name() method that encapsulates the logic of choosing between display_name, name, and twitch_id. # noqa: TD003
|
||||||
|
|
@ -787,12 +798,19 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=campaign_name,
|
page_title=campaign_name,
|
||||||
page_description=campaign_description,
|
page_description=campaign_description,
|
||||||
page_image=campaign_image,
|
seo_meta={
|
||||||
page_image_width=campaign_image_width,
|
"page_image": campaign_image,
|
||||||
page_image_height=campaign_image_height,
|
"page_image_width": campaign_image_width,
|
||||||
schema_data=campaign_schema,
|
"page_image_height": campaign_image_height,
|
||||||
breadcrumb_schema=breadcrumb_schema,
|
"schema_data": campaign_schema,
|
||||||
modified_date=campaign.updated_at.isoformat() if campaign.updated_at else None,
|
"breadcrumb_schema": breadcrumb_schema,
|
||||||
|
"published_date": campaign.added_at.isoformat()
|
||||||
|
if campaign.added_at
|
||||||
|
else None,
|
||||||
|
"modified_date": campaign.updated_at.isoformat()
|
||||||
|
if campaign.updated_at
|
||||||
|
else None,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
context.update(seo_context)
|
context.update(seo_context)
|
||||||
|
|
||||||
|
|
@ -887,7 +905,7 @@ class GamesGridView(ListView):
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Twitch Games",
|
page_title="Twitch Games",
|
||||||
page_description="Twitch games that had or have Twitch drops.",
|
page_description="Twitch games that had or have Twitch drops.",
|
||||||
schema_data=collection_schema,
|
seo_meta={"schema_data": collection_schema},
|
||||||
)
|
)
|
||||||
context.update(seo_context)
|
context.update(seo_context)
|
||||||
|
|
||||||
|
|
@ -1080,6 +1098,10 @@ class GameDetailView(DetailView):
|
||||||
reverse("twitch:game_detail", args=[game.twitch_id]),
|
reverse("twitch:game_detail", args=[game.twitch_id]),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
if game.added_at:
|
||||||
|
game_schema["datePublished"] = game.added_at.isoformat()
|
||||||
|
if game.updated_at:
|
||||||
|
game_schema["dateModified"] = game.updated_at.isoformat()
|
||||||
preferred_owner: Organization | None = _pick_owner(owners)
|
preferred_owner: Organization | None = _pick_owner(owners)
|
||||||
owner_name: str = (
|
owner_name: str = (
|
||||||
(preferred_owner.name or preferred_owner.twitch_id)
|
(preferred_owner.name or preferred_owner.twitch_id)
|
||||||
|
|
@ -1114,12 +1136,17 @@ class GameDetailView(DetailView):
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=game_name,
|
page_title=game_name,
|
||||||
page_description=game_description,
|
page_description=game_description,
|
||||||
page_image=game_image,
|
seo_meta={
|
||||||
page_image_width=game_image_width,
|
"page_image": game_image,
|
||||||
page_image_height=game_image_height,
|
"page_image_width": game_image_width,
|
||||||
schema_data=game_schema,
|
"page_image_height": game_image_height,
|
||||||
breadcrumb_schema=breadcrumb_schema,
|
"schema_data": game_schema,
|
||||||
modified_date=game.updated_at.isoformat() if game.updated_at else None,
|
"breadcrumb_schema": breadcrumb_schema,
|
||||||
|
"published_date": game.added_at.isoformat() if game.added_at else None,
|
||||||
|
"modified_date": game.updated_at.isoformat()
|
||||||
|
if game.updated_at
|
||||||
|
else None,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
context.update({
|
context.update({
|
||||||
"active_campaigns": active_campaigns,
|
"active_campaigns": active_campaigns,
|
||||||
|
|
@ -1213,8 +1240,10 @@ def dashboard(request: HttpRequest) -> HttpResponse:
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Twitch Drops",
|
page_title="Twitch Drops",
|
||||||
page_description="Overview of active Twitch drop campaigns and rewards.",
|
page_description="Overview of active Twitch drop campaigns and rewards.",
|
||||||
og_type="website",
|
seo_meta={
|
||||||
schema_data=website_schema,
|
"og_type": "website",
|
||||||
|
"schema_data": website_schema,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|
@ -1306,8 +1335,10 @@ def reward_campaign_list_view(request: HttpRequest) -> HttpResponse:
|
||||||
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,
|
||||||
pagination_info=pagination_info,
|
seo_meta={
|
||||||
schema_data=collection_schema,
|
"pagination_info": pagination_info,
|
||||||
|
"schema_data": collection_schema,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"reward_campaigns": reward_campaigns,
|
"reward_campaigns": reward_campaigns,
|
||||||
|
|
@ -1375,29 +1406,45 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes
|
||||||
else f"{campaign_name}"
|
else f"{campaign_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
campaign_schema: dict[str, str | dict[str, str]] = {
|
reward_url: str = request.build_absolute_uri(
|
||||||
"@context": "https://schema.org",
|
reverse("twitch:reward_campaign_detail", args=[reward_campaign.twitch_id]),
|
||||||
|
)
|
||||||
|
|
||||||
|
campaign_event: dict[str, Any] = {
|
||||||
"@type": "Event",
|
"@type": "Event",
|
||||||
"name": campaign_name,
|
"name": campaign_name,
|
||||||
"description": campaign_description,
|
"description": campaign_description,
|
||||||
"url": request.build_absolute_uri(
|
"url": reward_url,
|
||||||
reverse("twitch:reward_campaign_detail", args=[reward_campaign.twitch_id]),
|
|
||||||
),
|
|
||||||
"eventStatus": "https://schema.org/EventScheduled",
|
"eventStatus": "https://schema.org/EventScheduled",
|
||||||
"eventAttendanceMode": "https://schema.org/OnlineEventAttendanceMode",
|
"eventAttendanceMode": "https://schema.org/OnlineEventAttendanceMode",
|
||||||
"location": {"@type": "VirtualLocation", "url": "https://www.twitch.tv"},
|
"location": {"@type": "VirtualLocation", "url": "https://www.twitch.tv"},
|
||||||
}
|
}
|
||||||
if reward_campaign.starts_at:
|
if reward_campaign.starts_at:
|
||||||
campaign_schema["startDate"] = reward_campaign.starts_at.isoformat()
|
campaign_event["startDate"] = reward_campaign.starts_at.isoformat()
|
||||||
if reward_campaign.ends_at:
|
if reward_campaign.ends_at:
|
||||||
campaign_schema["endDate"] = reward_campaign.ends_at.isoformat()
|
campaign_event["endDate"] = reward_campaign.ends_at.isoformat()
|
||||||
if reward_campaign.game and reward_campaign.game.owners.exists():
|
if reward_campaign.game and reward_campaign.game.owners.exists():
|
||||||
owner = reward_campaign.game.owners.first()
|
owner = reward_campaign.game.owners.first()
|
||||||
campaign_schema["organizer"] = {
|
campaign_event["organizer"] = {
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": owner.name or owner.twitch_id,
|
"name": owner.name or owner.twitch_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webpage_node: dict[str, Any] = {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"url": reward_url,
|
||||||
|
"datePublished": reward_campaign.added_at.isoformat(),
|
||||||
|
"dateModified": reward_campaign.updated_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
campaign_schema = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [
|
||||||
|
campaign_event,
|
||||||
|
webpage_node,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
# 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": request.build_absolute_uri("/")},
|
||||||
|
|
@ -1419,9 +1466,12 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=campaign_name,
|
page_title=campaign_name,
|
||||||
page_description=campaign_description,
|
page_description=campaign_description,
|
||||||
schema_data=campaign_schema,
|
seo_meta={
|
||||||
breadcrumb_schema=breadcrumb_schema,
|
"schema_data": campaign_schema,
|
||||||
modified_date=reward_campaign.updated_at.isoformat(),
|
"breadcrumb_schema": breadcrumb_schema,
|
||||||
|
"published_date": reward_campaign.added_at.isoformat(),
|
||||||
|
"modified_date": reward_campaign.updated_at.isoformat(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"reward_campaign": reward_campaign,
|
"reward_campaign": reward_campaign,
|
||||||
|
|
@ -1506,8 +1556,10 @@ class ChannelListView(ListView):
|
||||||
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.",
|
||||||
pagination_info=pagination_info,
|
seo_meta={
|
||||||
schema_data=collection_schema,
|
"pagination_info": pagination_info,
|
||||||
|
"schema_data": collection_schema,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
context.update(seo_context)
|
context.update(seo_context)
|
||||||
context["search_query"] = search_query
|
context["search_query"] = search_query
|
||||||
|
|
@ -1648,17 +1700,30 @@ class ChannelDetailView(DetailView):
|
||||||
if total_campaigns > 1:
|
if total_campaigns > 1:
|
||||||
description += "s"
|
description += "s"
|
||||||
|
|
||||||
channel_schema: dict[str, Any] = {
|
channel_url: str = self.request.build_absolute_uri(
|
||||||
"@context": "https://schema.org",
|
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
||||||
|
)
|
||||||
|
channel_node: dict[str, Any] = {
|
||||||
"@type": "BroadcastChannel",
|
"@type": "BroadcastChannel",
|
||||||
"name": name,
|
"name": name,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": self.request.build_absolute_uri(
|
"url": channel_url,
|
||||||
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
|
||||||
),
|
|
||||||
"broadcastChannelId": channel.twitch_id,
|
"broadcastChannelId": channel.twitch_id,
|
||||||
"providerName": "Twitch",
|
"providerName": "Twitch",
|
||||||
}
|
}
|
||||||
|
webpage_node: dict[str, Any] = {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"url": channel_url,
|
||||||
|
"datePublished": channel.added_at.isoformat(),
|
||||||
|
"dateModified": channel.updated_at.isoformat(),
|
||||||
|
}
|
||||||
|
channel_schema: dict[str, Any] = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [
|
||||||
|
channel_node,
|
||||||
|
webpage_node,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
# Breadcrumb schema
|
# Breadcrumb schema
|
||||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
|
|
@ -1675,11 +1740,16 @@ class ChannelDetailView(DetailView):
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=name,
|
page_title=name,
|
||||||
page_description=description,
|
page_description=description,
|
||||||
schema_data=channel_schema,
|
seo_meta={
|
||||||
breadcrumb_schema=breadcrumb_schema,
|
"schema_data": channel_schema,
|
||||||
modified_date=channel.updated_at.isoformat()
|
"breadcrumb_schema": breadcrumb_schema,
|
||||||
|
"published_date": channel.added_at.isoformat()
|
||||||
|
if channel.added_at
|
||||||
|
else None,
|
||||||
|
"modified_date": channel.updated_at.isoformat()
|
||||||
if channel.updated_at
|
if channel.updated_at
|
||||||
else None,
|
else None,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
context.update({
|
context.update({
|
||||||
"active_campaigns": active_campaigns,
|
"active_campaigns": active_campaigns,
|
||||||
|
|
@ -1733,7 +1803,7 @@ def badge_list_view(request: HttpRequest) -> HttpResponse:
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Twitch Chat Badges",
|
page_title="Twitch Chat Badges",
|
||||||
page_description="List of Twitch chat badges awarded through drop campaigns.",
|
page_description="List of Twitch chat badges awarded through drop campaigns.",
|
||||||
schema_data=collection_schema,
|
seo_meta={"schema_data": collection_schema},
|
||||||
)
|
)
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"badge_sets": badge_sets,
|
"badge_sets": badge_sets,
|
||||||
|
|
@ -1847,7 +1917,7 @@ def badge_set_detail_view(request: HttpRequest, set_id: str) -> HttpResponse:
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=f"Badge Set: {badge_set_name}",
|
page_title=f"Badge Set: {badge_set_name}",
|
||||||
page_description=badge_set_description,
|
page_description=badge_set_description,
|
||||||
schema_data=badge_schema,
|
seo_meta={"schema_data": badge_schema},
|
||||||
)
|
)
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"badge_set": badge_set,
|
"badge_set": badge_set,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue