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. 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.
2. 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.
3. 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.
4. 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.
5. 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.
6. 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.
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.