DEV Community

Léo Guillaume (Dibodev)
Léo Guillaume (Dibodev)

Posted on

My static Nuxt blog publishes itself: drafts, scheduled "drip", and auto-rebuilds

I run my freelance site as a fully static (SSG) Nuxt site. It's fast, cheap to host, and boring in the good way. But static sites are genuinely bad at one thing: publishing an article next Tuesday at 9am. There's no server rendering each request, so "schedule this post" doesn't exist — something has to rebuild the site at the right moment.

I also write in bursts. Some evenings I'll draft two or three articles at once, and I don't want them all going live the same day. So I bolted a small publishing engine onto my existing stack: drafts, a scheduled queue (a "drip"), and automatic rebuilds. Here's how the pieces fit.

The stack

  • Nuxt 4, prerendered (SSG) for every public page
  • Storyblok as the headless CMS — the source of truth for content
  • A Nitro server (the same Nuxt app) on a small VPS under PM2 — the always-on part
  • GitHub Actions for the deploy/rebuild

The whole thing rests on one realization: on an SSG site, "publish" is two actions, not one — (1) put the content in the CMS, and (2) rebuild the static site so it actually shows up. Scheduling is just doing both later, automatically.

1. Drafts: write now, decide later

The dashboard has a markdown editor. Drafts are saved server-side (Nitro's storage layer), each with a status (draft / scheduled / published) and a publish date. Nothing touches the live site yet — a draft is just a row waiting for its moment.

2. The queue and the "drip"

The fun part. A Nitro scheduled task runs every hour and asks one question: is anything due?

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    experimental: { tasks: true },
    scheduledTasks: {
      '5 * * * *': ['articles:process-queue'], // every hour, at :05
    },
  },
})
Enter fullscreen mode Exit fullscreen mode
// server/tasks/articles/process-queue.ts
export default defineTask({
  meta: { name: 'articles:process-queue' },
  async run() {
    const due = await getDueScheduledArticles() // publishDate <= now
    if (!due.length) return { result: 'nothing due' }

    for (const article of due) {
      await publishToStoryblok(article) // markdown -> richtext -> CMS
    }
    await triggerSiteRebuild() // ONE rebuild for the whole batch
    return { result: `published ${due.length}` }
  },
})
Enter fullscreen mode Exit fullscreen mode

That's the entire scheduler. No queue service, no cron box, no extra infra — just a task the Nitro server already knows how to run.

3. Markdown → CMS → rebuild

When a post is due, it's converted from markdown to Storyblok's richtext format, pushed to the CMS, and then — the non-negotiable step on a static site — the site is rebuilt. No rebuild, no article. I trigger it with a workflow_dispatch call to my deploy workflow:

async function triggerSiteRebuild() {
  await $fetch(
    'https://api.github.com/repos/<owner>/<repo>/actions/workflows/deploy.yml/dispatches',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.REPO_ACCESS_TOKEN}`, // needs `actions: write`
        Accept: 'application/vnd.github+json',
      },
      body: { ref: 'main' },
    },
  )
}
Enter fullscreen mode Exit fullscreen mode

Three gotchas I hit

  • There's still a server. The scheduled task only fires if something is actually running. It's easy to forget an SSG site has no always-on backend by default — the Nitro server under PM2 exists purely so the scheduler (and the admin) stay alive.
  • One rebuild per batch, not per article. If three posts are due in the same run, publish all three then rebuild once. Otherwise you kick off three deploys back to back for nothing.
  • Timezones. "9am" means nothing until you pin the timezone. Ask me how I know.

The payoff

Now I can draft on a Sunday, spread posts across the next two weeks, and close the laptop. They go live on their own, the site rebuilds itself, and I stop babysitting a "publish" button.

It powers the blog on my freelance site, dibodev.fr — building this kind of custom tooling (and business apps / SaaS) for small companies is basically my day job.

Happy to dig into the Nitro task or the rebuild trigger in the comments.

Top comments (0)