> Short answer: AIKit can keep a high publishing cadence without producing thin posts by treating content QA as an automated pipeline, not a final human proofreading pass. The practical system is a checklist-driven LLM reviewer that scores each draft for answer-first clarity, llms.txt discoverability, code usefulness, internal links, and funnel intent before the post reaches D1.
The Problem
Marketing automation fails when it only optimizes for volume. A queue can publish three posts per day, but if those posts repeat the same angle, miss the reader intent, or lack structured examples, they do not help search engines, customers, or AI agents. The bigger risk is that a fast blog becomes a library of shallow pages: good titles, acceptable excerpts, and very little operational value.
For AIKit, the bar is higher because the content is not just for human readers. The site exposes dynamic llms.txt and llms-full.txt routes, so every published article can become input for assistants, search copilots, and agentic research tools. That means each post needs an answer-first opening, clear sections, implementation detail, and enough concrete context that an LLM can cite or reuse it without guessing.
The old workflow relied on the publisher script to validate JSON shape and word count. That is necessary, but it is not enough. A 1,000-word post can still be weak if it has no code, no operational checklist, no measurable outcome, and no next step. The QA layer needs to evaluate usefulness before publication.
The Solution
Build a lightweight LLM content QA gate between draft generation and blog-publisher.py. The gate reads each queue JSON file, extracts the title, excerpt, category, tags, and body_text, then returns a structured verdict. The verdict should be machine-readable so the cron job can decide whether to publish, request a rewrite, or archive a bad draft for manual review.
A useful first version has five checks. First, confirm the post answers its core question in the first 100 words. Second, ensure the article has at least four H2 sections and one practical artifact such as a command, SQL query, config snippet, or table. Third, check that the excerpt matches the article instead of sounding like generic marketing copy. Fourth, score LLM discoverability by looking for direct definitions, nouns, product names, and plain-English summaries. Fifth, verify funnel intent: every post should move the reader toward a next action such as reading a related guide, downloading a checklist, booking a demo, or trying an implementation pattern.
Architecture Overview
The system fits into the existing queue-first publishing model. Queue files remain the source of truth. The QA script runs before publish, writes a sidecar report, and only allows the direct D1 publish when the post passes the minimum score. Nothing about the EmDash database schema needs to change.
```text
queue/*.json
-> content-qa.py
-> qa-reports/653-slug.json
-> pass: blog-publisher.py
-> fail: rewrite queue file or hold for review
-> D1 ec_posts
-> dynamic /blog, /llms.txt, /llms-full.txt
```
This is intentionally boring infrastructure. The point is not to create a complex editorial platform. The point is to give the cron job a reliable quality signal using the same file-based queue it already understands. Sidecar reports also create an audit trail, which is helpful when a topic later underperforms and the team wants to know whether the problem was title, structure, depth, or targeting.
Step 1: Validate the Queue File
Start with deterministic checks before asking an LLM for judgment. These checks catch the errors that break publishing: invalid JSON, unexpected keys, wrong body_text type, unsafe word counts, and missing metadata.
```python
import json, os
EXPECTED = {"title", "body_text", "excerpt", "category", "tags"}
def validate_queue_file(path):
data = json.load(open(path))
extra = set(data) - EXPECTED
missing = EXPECTED - set(data)
if extra or missing:
raise ValueError({"extra": sorted(extra), "missing": sorted(missing)})
words = len(data["body_text"].split())
if not 800 <= words <= 1500:
raise ValueError(f"word count out of range: {words}")
if not isinstance(data["body_text"], str):
raise TypeError("body_text must be a string")
return data
```
This first step should run even when no LLM provider is available. It prevents malformed files from reaching the publisher and keeps operational failures separate from editorial failures. If the deterministic validator fails, the cron job should repair obvious field issues or stop and report the exact filename.
Step 2: Score the Draft for Agent Readability
After the file is structurally valid, score it for usefulness. A simple rubric works better than a vague prompt. Ask for JSON with scores from 0 to 5 and require short evidence for each score.
```json
{
"answer_first": 5,
"technical_depth": 4,
"llm_discoverability": 5,
"funnel_fit": 3,
"rewrite_required": false,
"notes": ["Opening answers the question", "Add one internal CTA"]
}
```
The pass rule should be conservative: require an average score of at least 4, no category below 3, and rewrite_required set to false. This prevents one strong section from hiding a weak article. It also gives the generation step precise feedback. Instead of saying the draft is bad, the system can say the article lacks implementation detail or has no explicit next step.
Step 3: Add a Rewrite Loop That Preserves the Slug
When a draft fails QA, the rewrite step should improve the same file instead of creating a new topic. This prevents queue sprawl and numbering collisions. The rewrite prompt should include the report, the original title, and the exact constraints: keep the title unless the slug collides, preserve category and tags, keep body_text between 800 and 1500 words, and add the missing substance.
A useful rewrite loop is limited to two attempts. If the second attempt still fails, move the file into a review folder and generate a new queue item. This avoids burning a full cron window on one stubborn draft while still maintaining quality.
Results
A QA gate changes the metric from posts shipped to useful posts shipped. The expected operational result is fewer publish-time failures, fewer generic articles, and better coverage in llms-full.txt because every post follows an agent-readable structure. The marketing result is also measurable: more posts with clear CTAs, more reusable snippets, and better internal consistency across AIKit, EmDash, and related product pages.
The first version can be implemented in under 100 lines of Python because it does not need a database migration or a new admin screen. It only needs to read queue files, call an LLM reviewer, write a report, and block publish when the score is too low. That makes it safe to add to an existing cron job without changing the D1 publishing path.
Key Takeaways
- Automating publication is not enough; AIKit needs automated editorial QA before D1 insert.
- Deterministic validation should catch JSON shape, field names, type errors, and word count before any LLM call.
- LLM scoring should focus on answer-first clarity, technical depth, llms.txt discoverability, and funnel fit.
- Failed drafts should be rewritten in place, not duplicated into extra queue files.
- The best content pipeline is small, auditable, and strict enough to prevent thin posts from becoming permanent site assets.