feat: add seo_meta service for AI-generated SEO metadata with CLI management and database layer
DevPlace CI / test (push) Failing after 2m13s

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
+41
View File
@@ -50,3 +50,44 @@ def test_keyword_scores_rank_match_higher():
assert scores
best = max(scores, key=scores.get)
assert "silicon" in next(c.text for c in chunks if c.uid == best).lower()
def test_dims_reflects_embedding_width():
store = VectorStore("ds_store_test_dims")
try:
chunks = _chunks()
vectors = local_embed([c.text for c in chunks]).vectors
store.add(chunks, vectors)
assert store.dims == len(vectors[0])
finally:
store.drop()
def test_coverage_analytics_empty_collection():
store = VectorStore("ds_store_test_cov_empty")
try:
analytics = store.coverage_analytics()
assert analytics == {
"chunks": 0,
"domains": 0,
"sources": 0,
"avg_chunk_chars": 0,
}
finally:
store.drop()
def test_coverage_analytics_counts_chunks_and_sources():
store = VectorStore("ds_store_test_cov")
try:
chunks = _chunks()
for index, chunk in enumerate(chunks):
chunk.source = "httpx" if index == 0 else "playwright"
vectors = local_embed([c.text for c in chunks]).vectors
store.add(chunks, vectors)
analytics = store.coverage_analytics()
assert analytics["chunks"] == 3
assert analytics["sources"] == 2
assert analytics["avg_chunk_chars"] > 0
finally:
store.drop()