feat: add canonical redirect, og_image, next_page_url, and sitemap caching across content routers

Implement canonical_redirect helper in content.py to enforce slug-based 301 redirects for gist, news, post, and project detail pages. Add first_image_url helper to extract primary image from item or attachments for Open Graph meta tags. Inject next_page_url into feed, gists, news, and projects list page SEO contexts for pagination link rel. Extend seo.py with SITEMAP_URL_LIMIT, SITEMAP_TTL constants and _sitemap cache variable for future sitemap generation optimization. Update profile page to raise 404 via not_found instead of 302 redirect, and include og_image from avatar_url.
This commit is contained in:
2026-06-05 18:05:07 +00:00
parent 9a24a90ec5
commit f05b6a4faf
14 changed files with 326 additions and 23 deletions
+181
View File
@@ -213,3 +213,184 @@ def test_news_detail_has_newsarticle_schema(page, app_server):
text = " ".join(scripts.nth(i).text_content() for i in range(scripts.count()))
assert "NewsArticle" in text
assert page.locator('meta[property="og:type"]').get_attribute("content") == "article"
def _seed_owner():
from datetime import datetime, timezone
from uuid import uuid4
from devplacepy.database import get_table
uid = str(uuid4())
get_table("users").insert({
"uid": uid,
"username": f"seo_{uid[:8]}",
"email": f"{uid[:8]}@seo.test",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
})
return uid
def _seed_post(image=None):
from datetime import datetime, timezone
from uuid import uuid4
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
owner = _seed_owner()
uid = str(uuid4())
slug = make_combined_slug("SEO Detail Post", uid)
get_table("posts").insert({
"uid": uid, "user_uid": owner, "slug": slug,
"title": "SEO Detail Post", "content": "Body text for the SEO detail post.",
"topic": "general", "project_uid": None, "image": image,
"stars": 0, "created_at": datetime.now(timezone.utc).isoformat(),
})
return slug, uid
def _seed_gist():
from datetime import datetime, timezone
from uuid import uuid4
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
owner = _seed_owner()
uid = str(uuid4())
slug = make_combined_slug("SEO Detail Gist", uid)
get_table("gists").insert({
"uid": uid, "user_uid": owner, "slug": slug,
"title": "SEO Detail Gist", "description": "Gist description for SEO tests.",
"source_code": "print('seo')", "language": "python",
"stars": 0, "created_at": datetime.now(timezone.utc).isoformat(),
})
return slug, uid
def _seed_project():
from datetime import datetime, timezone
from uuid import uuid4
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
owner = _seed_owner()
uid = str(uuid4())
slug = make_combined_slug("SEO Detail Project", uid)
get_table("projects").insert({
"uid": uid, "user_uid": owner, "slug": slug,
"title": "SEO Detail Project", "description": "Project description for SEO tests.",
"project_type": "software", "platforms": "Linux", "status": "Released",
"stars": 0, "created_at": datetime.now(timezone.utc).isoformat(),
})
return slug, uid
def _seed_news_image(news_uid):
from devplacepy.database import get_table
get_table("news_images").insert({
"news_uid": news_uid,
"url": "https://example.com/seo-news-image.jpg",
})
def _seed_feed_posts(count):
from datetime import datetime, timezone, timedelta
from uuid import uuid4
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
owner = _seed_owner()
topic = f"seopag{owner[:8]}"
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
posts = get_table("posts")
for i in range(count):
uid = str(uuid4())
posts.insert({
"uid": uid, "user_uid": owner, "slug": make_combined_slug(f"seo pag {i}", uid),
"title": None, "content": f"seo pag post {i}", "topic": topic, "project_uid": None,
"image": None, "stars": 0,
"created_at": (base - timedelta(seconds=i)).isoformat(),
})
return topic
def test_post_uid_redirects_to_canonical_slug(app_server):
slug, uid = _seed_post()
r = requests.get(f"{BASE_URL}/posts/{uid}", allow_redirects=False)
assert r.status_code == 301
assert r.headers["location"].endswith(f"/posts/{slug}"), r.headers.get("location")
def test_post_slug_served_without_redirect(app_server):
slug, uid = _seed_post()
r = requests.get(f"{BASE_URL}/posts/{slug}", allow_redirects=False)
assert r.status_code == 200
def test_gist_uid_redirects_to_canonical_slug(app_server):
slug, uid = _seed_gist()
r = requests.get(f"{BASE_URL}/gists/{uid}", allow_redirects=False)
assert r.status_code == 301
assert r.headers["location"].endswith(f"/gists/{slug}"), r.headers.get("location")
def test_project_uid_redirects_to_canonical_slug(app_server):
slug, uid = _seed_project()
r = requests.get(f"{BASE_URL}/projects/{uid}", allow_redirects=False)
assert r.status_code == 301
assert r.headers["location"].endswith(f"/projects/{slug}"), r.headers.get("location")
def test_news_uid_redirects_to_canonical_slug(app_server):
slug, uid = _seed_news()
r = requests.get(f"{BASE_URL}/news/{uid}", allow_redirects=False)
assert r.status_code == 301
assert r.headers["location"].endswith(f"/news/{slug}"), r.headers.get("location")
def test_missing_profile_returns_404(app_server):
r = requests.get(f"{BASE_URL}/profile/no-such-user-xyz", allow_redirects=False)
assert r.status_code == 404
def test_post_og_image_uses_content_image(page, app_server):
slug, _ = _seed_post(image="seo-content.png")
page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded")
og = page.locator('meta[property="og:image"]').get_attribute("content")
assert og.endswith("/static/uploads/seo-content.png"), og
def test_post_page_has_breadcrumb_schema(page, app_server):
slug, _ = _seed_post()
page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded")
scripts = page.locator('script[type="application/ld+json"]')
text = " ".join(scripts.nth(i).text_content() for i in range(scripts.count()))
assert "BreadcrumbList" in text
def test_post_schema_has_date_modified(page, app_server):
slug, _ = _seed_post()
page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded")
scripts = page.locator('script[type="application/ld+json"]')
text = " ".join(scripts.nth(i).text_content() for i in range(scripts.count()))
assert "dateModified" in text
def test_news_detail_image_has_alt_text(page, app_server):
slug, uid = _seed_news()
_seed_news_image(uid)
page.goto(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded")
alt = page.locator(".news-detail-image img").get_attribute("alt")
assert alt and alt.strip()
def test_feed_pagination_emits_rel_next(page, app_server):
topic = _seed_feed_posts(26)
page.goto(f"{BASE_URL}/feed?topic={topic}", wait_until="domcontentloaded")
nxt = page.locator('link[rel="next"]')
assert nxt.count() == 1
href = nxt.get_attribute("href")
assert "before=" in href and f"topic={topic}" in href, href
def test_post_detail_has_no_rel_next(page, app_server):
slug, _ = _seed_post()
page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded")
assert page.locator('link[rel="next"]').count() == 0