Scheduled posts are a query filter, not a redeploy
Schedule a post by giving it a future publishedAt. Every public query — listing, detail, sitemap, static params — drops anything still in the future, and ISR re-checks on a short interval. You can queue a month of notes without a publish worker.
One field, every surface
The mistake we used to make was treating schedule as a workflow: draft → scheduled → published, with a job that flips a flag at the right time. That job becomes another thing to monitor, and the flag becomes another thing that can disagree with the date.
The simpler model: publishedAt is both the public date and the gate. If it is in the future, the document may exist in the CMS and still be invisible to the site. When the clock passes, the next revalidation includes it. No state machine.
The filter has to live in more than one place
GROQ's publishedAt <= now() covers the Sanity path. The local seed fallback has to re-check in JavaScript, because a baked JSON file does not know what 'now' means at request time. Sitemap generation and generateStaticParams must use the same live set, or you ship URLs that 404 and omit posts that should be indexed.
That duplication is intentional. Scheduling only works if every public entrance agrees. One forgotten list endpoint that returns future posts is enough to spoil the trick.
ISR is the release mechanism
We revalidate the notes routes every few minutes. That is coarse enough to be cheap and fine enough that a post scheduled for 8:00 is not still missing at noon. You are not aiming for second-accurate launches. You are aiming for 'wrote in January, appeared in February without a deploy.'
If you need exact-minute launches for a press embargo, this is the wrong tool — use a flag or an edge config. For a practice notes feed, clock-plus-ISR is proportional.
What editors need to see
In the Studio preview, show whether a post is live or scheduled. The subtitle '◷ scheduled 2026-08-19' prevents the 'I published it, why isn't it on the site?' call. The document is not broken. It is waiting.
Optionally keep drafts separate from scheduled. A draft is unfinished. A scheduled post is finished and dated. Collapsing those two states teaches editors the wrong lesson about the publish date field.
Questions
- How do you schedule a note without a cron job?
- Set publishedAt in the future. Public queries filter with publishedAt <= now(), and ISR re-renders on a short interval. When the date passes, the post appears without a deploy or a worker.
- Why filter in both GROQ and JavaScript?
- Sanity queries can use now(). A committed seed JSON cannot. Any fallback path that reads the seed must apply the same date gate, or scheduled posts leak into local and degraded modes.
- Will this hit the sitemap at the exact minute?
- No — and that is fine for a notes feed. Sitemap and pages update on the next revalidation after the publish time. For embargo-grade timing, use an explicit publish flag instead.
Sources
- Sanity — GROQ now() — Use publishedAt <= now() on every public note query.
- Next.js — Incremental Static Regeneration
- Next.js — Dynamic Segments generateStaticParams