The cron-dependent approach and its failure mode
The standard approach to scheduled publishing: a cron job runs at the scheduled time, updates a published flag in the database, and the content becomes visible.
The failure mode: the cron job misses its window. The newsletter fires at 07:00. The cron job that was supposed to set published: true at 06:55 didn't run, server restart, network issue, timing drift. The newsletter goes out with a link to a 404.
The cron approach has a single point of failure between "content ready" and "content visible."
Runtime evaluation: the always-consistent alternative
Instead of updating a flag, evaluate visibility at request time:
export function getAllArticles(): ArticleMeta[] {
const now = new Date();
return readAllMdxFiles()
.filter(article => {
if (!article.scheduledDate) return article.published;
return new Date(article.scheduledDate) <= now;
})
.sort((a, b) => /* by date */);
}
An article with scheduledDate: "2026-04-20T10:00:00Z" is invisible until that moment, then visible to every request after it, without any cron, any database update, any deployment.
The MDX frontmatter pattern
---
title: "My scheduled article"
scheduledDate: "2026-04-20T10:00:00Z"
published: false
---
published: false plus a future scheduledDate means: draft, not yet visible.published: false plus a past scheduledDate means: automatically visible now.published: true means: always visible, regardless of date.
The two fields serve different purposes. published is manual override. scheduledDate is automatic timed release.
The dual-mechanism for newsletter integration
Runtime evaluation handles visibility. It does not handle triggered actions, like sending a newsletter when an article goes live.
For that, you still need a cron. But now the cron has one job: check if any article became visible in the last N minutes and fire the newsletter. It no longer needs to update the database first.
// Cron at 07:00 UTC
const recentlyPublished = getAllArticles()
.filter(a => {
const pub = new Date(a.scheduledDate ?? a.date);
const windowStart = new Date(Date.now() - 60 * 60 * 1000); // last hour
return pub >= windowStart && pub <= new Date();
});
for (const article of recentlyPublished) {
await sendNewsletter(article);
// Optionally: commit published: true to MDX to prevent re-sending
}
The key difference: the article is already visible before the cron runs. If the cron misses its window, the article is still live, only the newsletter is delayed, not the publication.
The consistency guarantee
Runtime evaluation gives you a simple invariant: an article with a past scheduledDate is always visible, on every server, in every region, with no state to sync. There's no "published in one region but not another" problem because there's no state, just a comparison against the current time.
This makes it particularly well-suited for static site generators and edge-rendered content, where database updates would require a redeployment.
Trade-offs
What you gain: simplicity, consistency, no failure mode from missed cron jobs, works on read-only filesystems.
What you give up: instant unpublishing (you'd need to remove the file or set scheduledDate to a future date), and the ability to see exactly which articles are "live" without querying at a specific time.
For most publishing workflows, the gains outweigh the trade-offs.