Corrective implementation of implementation.md (containerized Django/Wagtail) #3
35
apps/blog/tests/test_seo.py
Normal file
35
apps/blog/tests/test_seo.py
Normal 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
|
||||
@@ -11,16 +11,32 @@ from apps.core.models import SiteSettings
|
||||
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)
|
||||
def article_json_ld(context, article):
|
||||
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 = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
@@ -30,7 +46,7 @@ def article_json_ld(context, article):
|
||||
"dateModified": article.last_published_at.isoformat() if article.last_published_at else "",
|
||||
"description": article.search_description or article.summary,
|
||||
"url": article.get_full_url(request),
|
||||
"image": image_url,
|
||||
"image": _article_image_url(request, article),
|
||||
}
|
||||
return mark_safe(
|
||||
'<script type="application/ld+json">' + json.dumps(data, ensure_ascii=True) + "</script>"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{% block title %}No Hype AI{% endblock %}</title>
|
||||
{% block head_meta %}{% endblock %}
|
||||
<script nonce="{{ request.csp_nonce|default:'' }}">
|
||||
(function(){try{if(localStorage.getItem('theme')==='light'){document.documentElement.classList.remove('dark');}}catch(e){}})();
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load core_tags %}
|
||||
{% load core_tags seo_tags %}
|
||||
{% 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 %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
{% for article in articles %}
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load wagtailcore_tags wagtailimages_tags seo_tags %}
|
||||
{% 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 %}
|
||||
<article>
|
||||
<h1>{{ page.title }}</h1>
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load seo_tags %}
|
||||
{% 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 %}
|
||||
<section>
|
||||
{% if featured_article %}
|
||||
|
||||
Reference in New Issue
Block a user