My CMS Was Publishing My Posts Early
My CMS was publishing my scheduled posts early. On two sites. And nobody noticed, including me.
Schedule a post for next Tuesday and it showed up the second you hit save: at its URL, in the post list, in the RSS feed, in the sitemap. It even tooted to Mastodon. Then next Tuesday rolled around and... nothing happened, because nothing was ever watching the clock. Drafts were worse. Saving one sent webmentions for a post nobody could even see yet.
The CMS is Webstead, which runs this blog. I wrote it. So yeah, this one's on me.
The whole bug, basically
Here's how a post page decided whether you could see a post:
Post.objects.filter(slug=slug, deleted=False, published_on__isnull=False)
"Has a publish date" meant "is published." A scheduled post has a publish date. It's just in the future. Same idea was copy-pasted into the listing, the feed, the sitemap, and the widgets.
The fix is one boring little method that every public page now goes through:
def live(self):
"""Posts the public can see: not deleted, with a publish time that has passed."""
return self.filter(deleted=False, published_on__lte=timezone.now())
Plus the part that was missing entirely: a job that runs every minute, notices when a scheduled post's time has come, and only then sends the webmentions, Bridgy, and Mastodon stuff. Once. (#141)
How long was it broken? Honestly, I have no idea. Long enough to not be funny.
How I found it
I didn't, really. I was planning something bigger: making Webstead the best CMS for an AI to run. No admin UI required. You just say "draft a post about X" and it lands on your site as a draft, through a real API, with guardrails.
Before building any of that I had Claude review the spec against the actual code. Most of the review was the useful-but-boring kind ("you don't have revisions", "there's no service layer"). Then, almost as a side note: every public view treats "has a publish date" as "is published."
Oh. Cool. Cool cool cool.
Give the robot a draft, not a loaded gun
That bug reframed the whole project for me. Before an AI gets write access to my site, the site needs the stuff any careful human editor would want anyway:
- Drafts that are actually private. That's the fix above.
- One way to save a post. Micropub and the admin each had their own save path with their own side effects. That's how you get bugs like this one. Now everything goes through a single content service. (#142, #146)
- Undo. Every change saves a revision of what the post looked like before, and who changed it: me in the admin, Micropub, or an AI with its own token. Revert puts back the text, status, tags, and photos. (#144)
- Previews. A draft gets a signed link that expires in 7 days, isn't indexed, and doesn't count in analytics. So I can look at it on my phone before it's real. (#145)
None of that is AI stuff. It's just a CMS growing up. But it's exactly what makes handing the keys to an AI not terrifying. When it writes something dumb, it's a draft, I can see it, and I can undo it.
But Aaron, couldn't you have just... not had the bug?
Sure. And I could also floss every day.
(Bonus: the same review caught that removing a tag from one post through Micropub deleted that tag from every post. I wrote about that one in the post about letting Claude open pull requests.)
What's next
I already wrote about the other half: the MCP server that now lives inside Webstead. It's been running on this site for a few weeks. Claude can search my archive, write drafts, share preview links, and schedule posts, and anything that changes the site's settings or menus needs a second confirmation.
This post got scheduled by asking for it. If you're reading it, it went live on time. π
Comments
No comments yet.