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
+110
View File
@@ -259,6 +259,116 @@ def test_add_issue_usage_ignores_zero_call_batches(local_db):
assert get_issue_usage()["calls"] == 0
def test_seo_usage_accumulates_and_computes_averages(local_db):
from devplacepy.database import add_seo_usage, get_seo_usage
get_table("seo_usage").delete()
empty = get_seo_usage()
assert empty["calls"] == 0
assert empty["avg_tokens"] == 0.0
assert empty["avg_cost_usd"] == 0.0
totals = {
"calls": 2,
"prompt_tokens": 2000,
"completion_tokens": 400,
"total_tokens": 2400,
"cost_usd": 0.0005,
"upstream_latency_ms": 1600.0,
"total_latency_ms": 1800.0,
}
add_seo_usage(totals)
add_seo_usage(totals)
usage = get_seo_usage()
assert usage["calls"] == 4
assert usage["total_tokens"] == 4800
assert abs(usage["cost_usd"] - 0.001) < 1e-9
assert usage["avg_tokens"] == 1200.0
assert usage["avg_cost_usd"] == 0.00025
assert usage["avg_tokens_per_second"] == 250.0
def test_add_seo_usage_ignores_zero_call_batches(local_db):
from devplacepy.database import add_seo_usage, get_seo_usage
get_table("seo_usage").delete()
add_seo_usage({"calls": 0, "total_tokens": 999, "cost_usd": 1.0})
assert get_seo_usage()["calls"] == 0
def test_upsert_seo_metadata_inserts_then_updates_single_row(local_db):
from devplacepy.database import (
get_seo_metadata,
has_fresh_seo_metadata,
upsert_seo_metadata,
)
target_uid = generate_uid()
table = get_table("seo_metadata")
upsert_seo_metadata(
"post", target_uid, "Plain Title", "Plain description", "alpha, beta", "pending", "plain"
)
assert has_fresh_seo_metadata("post", target_uid) is False
assert get_seo_metadata("post", target_uid) is None
upsert_seo_metadata(
"post", target_uid, "AI Title", "AI description", "python, sqlite", "ready", "ai"
)
ready = get_seo_metadata("post", target_uid)
assert ready is not None
assert ready["seo_title"] == "AI Title"
assert ready["seo_keywords"] == "python, sqlite"
assert ready["generated_at"]
assert ready["deleted_at"] is None
assert has_fresh_seo_metadata("post", target_uid) is True
rows = list(table.find(target_type="post", target_uid=target_uid))
assert len(rows) == 1
def test_seo_metadata_batch_returns_only_ready_live_rows(local_db):
from devplacepy.database import (
get_seo_metadata_batch,
upsert_seo_metadata,
)
ready_uid = generate_uid()
pending_uid = generate_uid()
upsert_seo_metadata("gist", ready_uid, "T", "D", "k1, k2", "ready", "ai")
upsert_seo_metadata("gist", pending_uid, "T", "D", "k1, k2", "pending", "plain")
batch = get_seo_metadata_batch("gist", [ready_uid, pending_uid])
assert ready_uid in batch
assert pending_uid not in batch
assert batch[ready_uid]["status"] == "ready"
def test_mark_seo_metadata_stale_drops_ready_status(local_db):
from devplacepy.database import (
get_seo_metadata,
mark_seo_metadata_stale,
upsert_seo_metadata,
)
target_uid = generate_uid()
upsert_seo_metadata("project", target_uid, "T", "D", "k", "ready", "ai")
assert get_seo_metadata("project", target_uid) is not None
mark_seo_metadata_stale("project", target_uid)
assert get_seo_metadata("project", target_uid) is None
def test_seo_metadata_is_soft_deletable_and_indexed(local_db):
from devplacepy.database import SOFT_DELETE_TABLES
assert "seo_metadata" in SOFT_DELETE_TABLES
columns = get_table("seo_metadata").columns
assert "deleted_at" in columns
assert "deleted_by" in columns
def test_interleave_by_author_spreads_consecutive_runs():
from devplacepy.database import interleave_by_author