Back to DevLog

Defamation-hardening an auto-publishing blog with no human review

3 min read

I run a site that auto-publishes a blog post every day at 5:30 AM Central with no human review. The posts name people in a legal case, and some were framed in ways that suggested guilt or wrongdoing for living people who have not been charged. I spent today hardening the generation pipeline and fixing existing posts.

Generation guardrails

I rewrote the system prompt with mandatory accuracy rules: presence in a document is not evidence, no guilt or "connection" framing for living uncharged people, and document-focused angles instead of person-accusatory ones. I neutralized the profile prompt to describe a person's role factually, and rewrote the connection prompt to treat relationships as document co-occurrence only. I also added a disclaimer that gets appended automatically to any post that names an individual.

I tested it with a dry run on a post using the connection strategy for Lesley Groff. All guardrails fired.

Backfilling disclaimers to existing posts

I identified 57 posts that either had person_ids populated or linked to /person/ URLs in the body. I appended the disclaimer to all of them. The operation was idempotent and left slugs unchanged. I verified three posts live and checked that a thematic post with no individual names did not get the disclaimer.

Retitling and summary rewrites

I found three posts about Marko Nikolic with insinuating titles using words like "Shadow", "Problem", "Puzzle", and "Surprise". I neutralized the titles and preserved the slugs. The summaries were worse than the titles. One said "despite claiming limited contact. The paper trail tells a different story", which implies a living uncharged person lied. I neutralized all three summaries.

The new titles and summaries did not show up live at first. The app has no route cache and Cloudflare returned cf-cache-status DYNAMIC, but a cache-buster query parameter returned the new content. Cloudflare edge cache was holding copies with a one-hour TTL. I could not purge it because the available token lacks purge permissions. Origin is correct and the edge will self-heal.

Insert-time safety net

The disclaimer logic only fired when the post had person_ids populated or contained /person/ links. A post could mention someone in plain prose without either condition. I added a function that scans the title and body against the 89 names in the people table and appends the disclaimer if it finds a match. I tested it on a post mentioning Jeffrey Epstein with no link, and it fired. A negative control with no names stayed clean.

Architecture discovery

Both the blog generator cron job and the web server run from the app-dev directory on the dev branch. The app directory on master is a separate clone that is not served. Committing on dev is effectively deploying, because the cron job reads the file fresh each run and the web app does not import the auto-blog script. The two commits I made today are ahead of origin and not yet pushed.

Share this post