Add canonical and social SEO meta tags for core page templates
Some checks failed
CI / typecheck (pull_request) Failing after 2m20s
CI / lint (pull_request) Failing after 3m3s
CI / tests (pull_request) Failing after 3m7s

This commit is contained in:
Codex_B
2026-02-28 12:39:12 +00:00
parent 6fc28f9d9a
commit e279e15c9c
6 changed files with 94 additions and 9 deletions

View File

@@ -0,0 +1,35 @@
import pytest
from apps.blog.models import ArticleIndexPage, ArticlePage
from apps.blog.tests.factories import AuthorFactory
@pytest.mark.django_db
def test_article_page_renders_core_seo_meta(client, home_page):
index = ArticleIndexPage(title="Articles", slug="articles")
home_page.add_child(instance=index)
author = AuthorFactory()
article = ArticlePage(
title="SEO Article",
slug="seo-article",
author=author,
summary="Summary content",
body=[("rich_text", "<p>Body</p>")],
)
index.add_child(instance=article)
article.save_revision().publish()
resp = client.get("/articles/seo-article/")
html = resp.content.decode()
assert resp.status_code == 200
assert '<link rel="canonical" href="http' in html
assert 'property="og:type" content="article"' in html
assert 'name="twitter:card" content="summary_large_image"' in html
@pytest.mark.django_db
def test_homepage_renders_website_og_type(client, home_page):
resp = client.get("/")
html = resp.content.decode()
assert resp.status_code == 200
assert 'property="og:type" content="website"' in html

View File

@@ -11,16 +11,32 @@ from apps.core.models import SiteSettings
register = template.Library() register = template.Library()
def _article_image_url(request, article) -> str:
site_settings = SiteSettings.for_request(request)
image = article.hero_image or site_settings.default_og_image
if isinstance(image, Image):
rendition = image.get_rendition("fill-1200x630")
return request.build_absolute_uri(rendition.url)
return ""
@register.simple_tag(takes_context=True)
def canonical_url(context, page=None) -> str:
request = context["request"]
target = page or context.get("page")
if target and hasattr(target, "get_full_url"):
return target.get_full_url(request)
return request.build_absolute_uri()
@register.simple_tag(takes_context=True)
def article_og_image_url(context, article) -> str:
return _article_image_url(context["request"], article)
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def article_json_ld(context, article): def article_json_ld(context, article):
request = context["request"] request = context["request"]
site_settings = SiteSettings.for_request(request)
image = article.hero_image or site_settings.default_og_image
image_url = ""
if isinstance(image, Image):
rendition = image.get_rendition("fill-1200x630")
image_url = request.build_absolute_uri(rendition.url)
data = { data = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Article", "@type": "Article",
@@ -30,7 +46,7 @@ def article_json_ld(context, article):
"dateModified": article.last_published_at.isoformat() if article.last_published_at else "", "dateModified": article.last_published_at.isoformat() if article.last_published_at else "",
"description": article.search_description or article.summary, "description": article.search_description or article.summary,
"url": article.get_full_url(request), "url": article.get_full_url(request),
"image": image_url, "image": _article_image_url(request, article),
} }
return mark_safe( return mark_safe(
'<script type="application/ld+json">' + json.dumps(data, ensure_ascii=True) + "</script>" '<script type="application/ld+json">' + json.dumps(data, ensure_ascii=True) + "</script>"

View File

@@ -5,6 +5,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}No Hype AI{% endblock %}</title> <title>{% block title %}No Hype AI{% endblock %}</title>
{% block head_meta %}{% endblock %}
<script nonce="{{ request.csp_nonce|default:'' }}"> <script nonce="{{ request.csp_nonce|default:'' }}">
(function(){try{if(localStorage.getItem('theme')==='light'){document.documentElement.classList.remove('dark');}}catch(e){}})(); (function(){try{if(localStorage.getItem('theme')==='light'){document.documentElement.classList.remove('dark');}}catch(e){}})();
</script> </script>

View File

@@ -1,6 +1,14 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load core_tags %} {% load core_tags seo_tags %}
{% block title %}Articles | No Hype AI{% endblock %} {% block title %}Articles | No Hype AI{% endblock %}
{% block head_meta %}
{% canonical_url page as canonical %}
<link rel="canonical" href="{{ canonical }}" />
<meta name="description" content="Latest No Hype AI articles and benchmark-driven reviews." />
<meta property="og:type" content="website" />
<meta property="og:title" content="Articles | No Hype AI" />
<meta property="og:url" content="{{ canonical }}" />
{% endblock %}
{% block content %} {% block content %}
<h1>{{ page.title }}</h1> <h1>{{ page.title }}</h1>
{% for article in articles %} {% for article in articles %}

View File

@@ -1,6 +1,21 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load wagtailcore_tags wagtailimages_tags seo_tags %} {% load wagtailcore_tags wagtailimages_tags seo_tags %}
{% block title %}{{ page.title }} | No Hype AI{% endblock %} {% block title %}{{ page.title }} | No Hype AI{% endblock %}
{% block head_meta %}
{% canonical_url page as canonical %}
{% article_og_image_url page as og_image %}
<link rel="canonical" href="{{ canonical }}" />
<meta name="description" content="{{ page.search_description|default:page.summary }}" />
<meta property="og:type" content="article" />
<meta property="og:title" content="{{ page.title }} | No Hype AI" />
<meta property="og:description" content="{{ page.search_description|default:page.summary }}" />
<meta property="og:url" content="{{ canonical }}" />
{% if og_image %}<meta property="og:image" content="{{ og_image }}" />{% endif %}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ page.title }} | No Hype AI" />
<meta name="twitter:description" content="{{ page.search_description|default:page.summary }}" />
{% if og_image %}<meta name="twitter:image" content="{{ og_image }}" />{% endif %}
{% endblock %}
{% block content %} {% block content %}
<article> <article>
<h1>{{ page.title }}</h1> <h1>{{ page.title }}</h1>

View File

@@ -1,5 +1,15 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load seo_tags %}
{% block title %}No Hype AI{% endblock %} {% block title %}No Hype AI{% endblock %}
{% block head_meta %}
{% canonical_url page as canonical %}
<link rel="canonical" href="{{ canonical }}" />
<meta name="description" content="Honest AI coding tool reviews for developers." />
<meta property="og:type" content="website" />
<meta property="og:title" content="No Hype AI" />
<meta property="og:description" content="Honest AI coding tool reviews for developers." />
<meta property="og:url" content="{{ canonical }}" />
{% endblock %}
{% block content %} {% block content %}
<section> <section>
{% if featured_article %} {% if featured_article %}