I’ve always liked static site generators because they’re fast and I don't have to worry about a database exploding at 3 AM. But for a long time, my blog felt like a "manual transmission" car. If I wanted to publish a post, I had to be at my desk, merge a PR, and wait for a build. If I wanted to schedule a post for next Tuesday? I had to remember to click "Merge" on Tuesday morning.
The Build-Time Filter
The heart of the system is Vite's import.meta.glob. Since I'm using MDsveX, my posts are basically Svelte components that get pulled in at build time. I added a filter in src/lib/posts.ts that checks the metadata date against new Date(). If the post is in the future, it just doesn't get included in the production array.
const now = new Date().getTime();
// ... inside the loop ...
const isVisible = !post.metadata.hidden && postDate <= now;
if (showDrafts || isVisible) {
// include the post
}...And this works fine, but it means the post technically exists in the repository, just not in the rendered HTML. Since I keep this repo private, it’s not a huge deal, but if I ever decided to open-source the code, I’d have to actually move the drafts to a separate branch or something to keep them truly hidden. Right now, I'm just relying on the fact that only I can see the source, which feels a bit like cheating.
GitHub Actions: The Cron Suggestion
To make the "scheduled" posts actually go live, I set up a GitHub Actions cron job to rebuild the site every night.
on:
schedule:
- cron: "0 0 * * *"Here’s the thing no one tells you: GitHub's schedule is more of a suggestion than a command. I’ve seen my "midnight" builds trigger at 1:45 AM or even 3 AM depending on how busy their runners are. If you’re trying to time a post for a specific launch event, don't rely on this. It’s fine for a personal blog, but it’s definitely one of those "you get what you pay for" situations.
Nix and the "Works on My Machine" Problem
I use Nix Flakes to keep my build environment sane. My deploy.yml doesn't just run npm install; it enters a nix develop shell to get the exact pnpm and Node versions I used locally.
I assumed this would solve all my CI issues, but I still ran into a weird bug where a specific version of a remark plugin worked on my macOS Nix shell but failed on the Linux GitHub runner because of some obscure glibc difference. Nix gets you 95% of the way to "identical environments," but that last 5% is where you spend your Saturday afternoons.
A Hacky Preview Mode
I implemented a "preview" mode using a shared secret in the query string. If SvelteKit sees ?preview=secret, it bypasses the date filter.
export const GET: RequestHandler = async ({ url }) => {
const previewToken = url.searchParams.get('preview');
const isPreview = previewToken === process.env.PREVIEW_TOKEN;
const posts = importPosts(isPreview);
return json(posts);
};It’s simple, it works, and it’s better than having to run pnpm dev every time I want to see if a layout tweak messed up a specific post. I’m not 100% sure if there's a more "idiomatic" SvelteKit way to do this—maybe using a separate Vercel/Netlify preview branch—but for a site hosted on Cloudflare Workers, this "token in the URL" approach is the least amount of friction.
By turning this into an automated pipeline, I've removed the "deployment anxiety." I just write the post, set a date, and trust that GitHub will (eventually) get around to building it.
