This blog is a plain static site — built with Eleventy, served from Vercel. There is no code running at request time. And yet we wanted to write posts in advance and have them appear on a set date.
On a dynamic site this isn't a question. The server compares the current time against a publish date on every request. A static site has nobody to make that comparison. Every page is generated at build time, shipped to a CDN, and after that nothing ever checks the clock again.
So we split the problem in two.
Without the first, future posts ship immediately. Without the second, the publish date arrives and the site stays exactly as it was yesterday. You need both. Here's each one.
1. Where Does the Decision Live? There Are Only Four Options
Before writing any code we counted the places the decision could live. Sort them by who evaluates "has the publish date passed?" and when, and there are exactly four.
The third one isn't scheduled publishing at all. The full text of an unpublished post ships inside the HTML and JavaScript merely hides it. One curl reveals it, and search engines read the raw markup before running any script. "Not visible" and "does not exist" are different things, and scheduling has to deliver the second.
The fourth is oversized for a blog. Bolt a CMS onto forty posts and running the CMS becomes bigger than writing them. The second contradicts the premise — we wanted the site to stay static. So we took the first, which means accepting its weakness whole: no trigger, no post, ever. The rebuild section below is the story of paying that price.
2. Leaving Future Posts Out of the Build
Eleventy has directory data files. Drop a posts.11tydata.js next to the posts/ folder and every template in that folder inherits it. Add eleventyComputed and you can derive values per post.
The key fact is that setting permalink to false means the file is never written at all.
Those two lines do different jobs.
permalink: false— no HTML is generated. Typing the URL directly gives a 404.eleventyExcludeFromCollections: true— the post leaves the collection, so it appears in neither the index page norsitemap.xml.
Exclude it from collections only, and the post is hidden from listings while its URL stays live. Skip the file only, and listings and the sitemap keep linking to a 404. The second is the worse one — you're submitting dead URLs straight to search engines.
Writing out all four combinations makes the choice obvious.
The middle two are the states you reach by accident. The third row is the deceptive one: nothing links to the post, so it feels unpublished — but an RSS reader, a social preview, a cached earlier deploy, or a guessed URL all reach it. Hiding something from a listing is not access control.
3. Future Posts Should Still Be Visible Locally
One condition in that snippet deserves attention.
Eleventy exposes its run mode through this variable: eleventy sets build, eleventy --serve sets serve. So future posts render normally under npm run dev and disappear only under npm run build.
That's deliberate. The biggest risk with scheduled publishing is discovering a typo on the morning it goes live. If the post is hidden locally too, there is no way to review it before the world sees it.
The asymmetry does have to be written down. Undocumented, "it renders in dev but isn't in the build output" looks exactly like a bug — even though it's the feature working. A rule like this belongs in the repository's own rules file, which is one reason where you put a document is itself a rule. Human or agent, whoever opens the build output without knowing this will file a bug report.
dev, judge from build.
Unpublished drafts get reviewed with npm run dev; the question "what is public today?" is answered only by the npm run build output. The two commands are supposed to disagree, so mixing them while checking is a reliable way to confuse yourself.
4. Dates Parse as UTC — Which Means 9 AM in Korea
A bare date in YAML front matter parses as midnight UTC.
Since most of our readers are in Korea, a date-only value publishes at 9 AM their time — a decent hour, as it happens, so we never bothered adding an explicit time. But if you don't know this, you'll spend a while wondering why the post scheduled for the 22nd didn't appear at midnight.
The same single line lands at a different moment in every region. Here is what date: 2026-08-22 actually means:
West of UTC, the post appears the previous evening. If you read the date as "the day this goes out," you are off by one. For most blogs that doesn't matter; for a post tied to a specific day — an event announcement, a year-end review — it's worth writing an explicit time.
An explicit offset makes the parsing exact. It does not make the publication exact — when the post actually appears is decided by the rebuild schedule in the next section. Pin the date to midnight in Korea and the post still shows up at 9:05 AM if that's when the rebuild runs.
5. The Real Problem — Nothing Ever Happens to a Static Site
Everything above is the part most articles cover. And if you stop there, scheduled publishing does not work.
Builds run when you push something. The fact that it is now August 22 creates no commit. No commit, no build; no build, and the HTML on the CDN is still whatever the last deploy produced. The post never appears.
Something has to tell the site that time has passed. We do it with a GitHub Actions cron that pings a Vercel deploy hook once a day.
A deploy hook is a URL you create in the Vercel project settings; one POST deploys the branch you bound it to. We picked 00:05 UTC to line up with the midnight-UTC parsing above — the publish condition flips at midnight, and five minutes later a build reflects it.
Scheduled workflows are best-effort and can slip by tens of minutes when runners are busy. For publishing a blog post that's irrelevant — nobody notices. Just don't reach for this cron when you need minute-level accuracy.
The real publish time is a window, not a moment
We say "it goes live at nine," but that number is a stack of delays. Counted separately:
Which means this design cannot promise a specific minute. The promise it can keep is "sometime that morning." If you need minute-level precision, the rebuild trigger has to be something other than a best-effort cron — and at that point you've already given up the simplicity that made a static site attractive.
When the rebuild fails, nothing makes a sound
This is the quietest failure in the whole design. If the deploy hook URL has expired, the secret is empty, or the build breaks on the hosting side, the site keeps serving yesterday perfectly. No 500, no 404. Just no new post. And the only person who notices a missing post is the person who wrote it.
That's why the curl carries -f. Without it, the hook can answer 404 and curl still exits zero, leaving a green workflow behind. A green cron that does nothing every day is the worst state of all — it hands you the illusion of monitoring on top of the failure.
On a week with something scheduled, it's still worth checking the live site directly. A green workflow only proves the hook was pinged; whether the build finished lives in the deployment log.
6. Traps We Found by Stepping in Them
Two locales drift apart
Every post here exists as a Korean and an English file. Publishing is decided per file from its own date, so if the two dates differ, one locale goes live alone.
The live one then advertises an hreflang pointing at a URL that doesn't exist yet. Canonical and hreflang break silently: the page looks perfect in a browser, and only search engines see the problem.
So the build now fails if the two files disagree on date. Having the build enforce it beats relying on remembering it every time.
Links between posts can point into the future
This one is subtler. Link from the August 22 post to the August 30 post and that link 404s for eight days. Nothing looks wrong when you write it — the target file is right there in the repo — and it heals itself later, so it rarely leaves a trace.
The rule is simple: links between posts must always point backward in time. The build checks this too. It scrapes every post link out of every body, confirms the target exists, and confirms the target's date is not later than the linking post's. Since dates are YYYY-MM-DD strings, a lexicographic comparison is a date comparison.
Moving a date forward unpublishes a live post
Change the date of an already-published post to a future value and the next build removes it from the site. Fix a typo and casually adjust the date at the same time, and you've killed a live URL. Treat publish dates as something you edit carefully.
To record a revision, use sitemapLastmod instead. The publish date stays put while only the sitemap's lastmod moves, so the post stays up and search engines still get the "this page changed" signal. That's how this very post was updated.
Preview deploys apply the filter too
A PR preview runs eleventy in build mode, so future posts are missing from previews as well. "Review it on the preview URL, then merge" simply doesn't work for scheduled posts. Reviewing an unpublished draft happens locally, in dev, or nowhere.
7. When a Post Doesn't Appear — A Diagnostic Order
"It should have gone live today and it didn't" always comes down to the same handful of places. Work down the list.
In practice it was almost always 1 or 2 — an empty secret in the first case, and in the second, doing the date math in local time instead of UTC. Start asking "why isn't it up?" at 8 AM in Korea and the answer is to wait an hour.
8. Common Questions
Is one rebuild a day enough?
For post-level scheduling, yes. Publish precision equals the cron interval, so a daily job can be up to a day late — except publish dates are date-granular, so in practice the post lands within its day. You can run the cron hourly if you want hour-level scheduling, but that's twenty-three deploys a day where nothing changed. We kept it at one.
Doesn't a daily rebuild clutter the deployment history?
It does. A new deployment appears even when the source is identical. The upside is that "did a build run today?" is now answerable at a glance — step 1 of the diagnostic order above leans on exactly that record. As the price of catching a silent failure, it's cheap.
The unpublished post is in the repository — isn't it public already?
In a public repository, yes. This design keeps a post off the site; it does not hide the source. If drafts being readable is a problem, keep the repository private or commit right before publishing. Overkill for blog posts, a real concern for announcements.
Can it leak through RSS or a social preview?
No. Future posts leave the collection, so they're absent from RSS, listings, and the sitemap alike, and no page file exists for a crawler to fetch. The one leaking state is the combination above where only permalink was left in place.
Does this work in Next.js or Astro?
The principle is identical: compare build time against the publish date and drop future posts from the static route list, wherever your framework generates those routes. The only thing that differs is where that hook lives — the need to push a rebuild from outside is exactly the same. With server rendering or ISR the problem disappears entirely, because the decision happens per request.
What happens if I edit a post after it's live?
It just ships. Publishing is decided from date alone, and a past date stays past. The single thing to watch is not touching date while editing. To record the revision, use sitemapLastmod.
9. Wrapping Up
All told this is about forty lines of application code. No database, no serverless function — scheduled publishing on a genuinely static site.
The shape of it:
- Build time is the only clock you have. There is no request time, so there is no other basis for the decision.
- Exclude future posts from both the file output and the collections. Doing one leaves either dead links or a live URL.
- Push the rebuild from outside. Miss this and everything else can be correct while the post never appears.
- Keep future posts visible locally. You need some way to review a post before it publishes.
- Make a failed rebuild audible. Every failure mode here is silent — the site keeps serving yesterday, perfectly.
If you run a static blog and have ever wanted to write in bursts and publish on a steady cadence, it takes far less code than you'd expect. Just don't forget the piece that triggers the rebuild — that's the one nearly everyone leaves out the first time.