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()
|
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>"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user