feat: add seo_meta service for AI-generated SEO metadata with CLI management and database layer

Implement a new `SeoMetaService` subservice that generates clean SEO title/description/keywords for published content items, distinct from the existing SEO diagnostics auditor. Add `seo_metadata` polymorphic table with soft-delete support, batch query methods, and usage tracking. Extend the CLI with `seo-meta prune` and `seo-meta clear` commands for job row lifecycle management. Wire `schedule_seo_meta_for_table` into content creation and editing flows in `content.py`. Document the new service in `AGENTS.md` and `README.md`, including the `extra_head` site setting for custom `<head>` injection.
This commit is contained in:
2026-06-19 20:15:22 +00:00
parent 426d3639c6
commit d10f1af118
51 changed files with 2262 additions and 93 deletions
+74
View File
@@ -5,6 +5,14 @@ import devplacepy.seo as seo
import devplacepy.attachments as attach
def _seo_request():
return types.SimpleNamespace(
base_url="https://x.test/",
url=types.SimpleNamespace(path="/posts/abc"),
query_params={},
)
def test_truncate_no_overflow():
assert len(seo.truncate("x" * 200)) <= 160
assert seo.truncate("short text") == "short text"
@@ -76,3 +84,69 @@ def test_detect_mime_neutralizes_dangerous_types():
assert attach._detect_mime(b"", "x.svg") == "application/octet-stream"
assert attach._detect_mime(b"", "x.html") == "application/octet-stream"
assert attach._detect_mime(b"", "x.png") == "image/png"
def test_base_seo_context_emits_keywords_key():
ctx = seo.base_seo_context(
_seo_request(),
title="A Post",
description="Body text",
keywords="python, sqlite",
)
assert "meta_keywords" in ctx
assert ctx["meta_keywords"] == "python, sqlite"
def test_base_seo_context_default_keywords_empty_without_target():
ctx = seo.base_seo_context(
_seo_request(), title="A Post", description="Body text"
)
assert ctx["meta_keywords"] == ""
def test_base_seo_context_description_strips_markdown(monkeypatch):
monkeypatch.setattr(seo, "_ready_seo_metadata", lambda target: None)
ctx = seo.base_seo_context(
_seo_request(),
title="A Post",
description="## Heading\n\nThis is **bold** body with `code`.",
)
assert "##" not in ctx["meta_description"]
assert "**" not in ctx["meta_description"]
assert "`" not in ctx["meta_description"]
assert "bold" in ctx["meta_description"]
def test_base_seo_context_plain_default_fills_fields_for_target(monkeypatch):
monkeypatch.setattr(seo, "_ready_seo_metadata", lambda target: None)
ctx = seo.base_seo_context(
_seo_request(),
title="Async Python Database Tooling",
description="A **guide** to async python database tooling with sqlite.",
seo_target=("post", "abc"),
)
assert ctx["meta_keywords"]
assert "python" in ctx["meta_keywords"]
assert ctx["meta_description"]
assert "**" not in ctx["meta_description"]
def test_base_seo_context_consumes_ready_metadata(monkeypatch):
monkeypatch.setattr(
seo,
"_ready_seo_metadata",
lambda target: {
"seo_title": "Generated Title",
"seo_description": "Generated clean description",
"seo_keywords": "generated, metadata",
},
)
ctx = seo.base_seo_context(
_seo_request(),
title="Raw **markdown** title",
description="raw markdown body",
seo_target=("post", "abc"),
)
assert "Generated Title" in ctx["page_title"]
assert ctx["meta_description"] == "Generated clean description"
assert ctx["meta_keywords"] == "generated, metadata"