Defamation-hardening an auto-published blog with guardrails and disclaimers
I run a blog that auto-publishes daily at 5:30 AM Central with no human review. The content is generated from a document archive, and some posts name living people who have never been charged. I needed to harden the pipeline against defamation risk.
Generation guardrails
I rewrote the system prompt with mandatory accuracy and fairness rules. Presence in a document does not equal evidence of wrongdoing. No guilt framing or suggestive connection language for living uncharged people. Titles must be factual, not insinuating.
I also neutralized the profile and connection prompts. The profile prompt now asks for factual roles instead of implications. The connection prompt treats relationships as document co-occurrence only, and I removed the suggestive label it was using.
A dry run verified all the guardrails fired correctly.
Disclaimer backfill
I identified 57 existing posts that name individuals by checking which posts had person IDs populated or linked to person pages. I appended an inline disclaimer to the stored HTML body of each post. The operation was idempotent and left slugs unchanged. I verified three posts live and checked a thematic post as a negative control.
I also retitled three posts that used insinuating framing like "Shadow" and "Problem" and "Puzzle." The slugs stayed the same. When I checked, the summaries were worse than the titles, so I neutralized those too.
Insert-time safety net
The disclaimer backfill only caught posts that linked to person pages or had person IDs. A post could still name someone in plain prose and slip through.
I added a function that scans the post title and body against the people table at insert time. The table has 89 rows. If the text matches a known person, the disclaimer is appended automatically, even when the model adds no link and person IDs is empty.
A deterministic test passed. A live dry run showed it firing on a real post naming an individual with no link.
Cache behavior
After I neutralized the summaries, the old phrases persisted on the live site. The app reads fresh per request with no cache decorator, and Cloudflare reported the response as dynamic. A cache-buster query parameter returned the new summary.
The issue was Cloudflare edge cache. The bare URLs were cached when I verified the title changes. I could not purge the cache because the available token lacks purge permission. The origin was correct and the edge cache expires in under an hour.
Architecture notes
Both the blog generator and the web server run from the same directory on the dev branch. The generator runs from a cron job. The web server runs under gunicorn. A separate clone on the master branch exists but is not served.
Committing on dev deploys the generator immediately because the cron job reads the file fresh each run. The web app does not import the generator script.
Both clones symlink their database paths to a shared database file.