# How I auto-publish scheduled posts with a Cloudflare Worker cron

> How I publish future-dated posts on a static Astro site: a Cloudflare Worker cron triggers a daily Pages rebuild via a deploy hook, with a DST-proof schedule.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-04 | Topics: [Cloudflare](https://flaviocopes.com/tags/cloudflare/) | Canonical: https://flaviocopes.com/scheduled-cloudflare-pages-rebuilds/

I schedule blog posts by giving them a future date in the frontmatter.

The build filters them out: a post dated tomorrow is not included in today's build. The first build that runs after that date picks it up, and the post goes live.

There is one catch. This site is a static Astro site on Cloudflare Pages, and Pages only builds when I push to the repository.

If I don't push anything tomorrow, tomorrow's post never publishes.

The fix is to trigger one build every morning. Here is how I do it with a deploy hook and a scheduled Cloudflare Worker.

## What is a deploy hook?

A deploy hook is a URL that triggers a new build of your Pages project.

You create one in the Cloudflare dashboard: open your Pages project, then **Settings → Builds → Deploy hooks**. Give it a name, pick the branch, and Cloudflare gives you back a URL.

Anyone with that URL can trigger builds on your project, so treat it like a secret.

Triggering a build is a single POST request:

```bash
curl -X POST "https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/<hook_id>"
```

Now I just need something to send that request every morning.

## Why not GitHub Actions?

My first version was a GitHub Actions workflow with a cron schedule that called the deploy hook.

It worked, but GitHub cron is best effort. My job was scheduled at 05:15 UTC. Looking at when it actually ran over the past days: 07:40, 08:34, 08:42, 10:07.

Hours late, every day. GitHub runs scheduled workflows when it has spare capacity, and popular times of day are busy.

For "publish this post in the morning", that's not great.

Cloudflare Workers have cron triggers too, and in my experience they fire on time. The free plan includes them.

## The Worker

The Worker is tiny. One scheduled handler that calls the deploy hook:

```js
export default {
  async scheduled(event, env) {
    const rome = new Intl.DateTimeFormat('en-GB', {
      timeZone: 'Europe/Rome',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false,
    }).format(new Date(event.scheduledTime))

    if (rome !== '09:30') return

    const res = await fetch(env.DEPLOY_HOOK_URL, { method: 'POST' })
    if (!res.ok) {
      throw new Error(`Deploy hook failed with status ${res.status}`)
    }
  },
}
```

Notice the time check at the top. That's the DST trick, and it deserves an explanation.

## Handling daylight saving time

Cron triggers run in UTC.

I want the rebuild at 9:30 in Italy. That's 07:30 UTC in summer and 08:30 UTC in winter.

Instead of editing the cron expression twice a year, I register both times in `wrangler.jsonc`:

```jsonc
{
  "name": "daily-redeploy",
  "main": "index.js",
  "compatibility_date": "2026-08-01",
  "triggers": {
    "crons": ["30 7 * * *", "30 8 * * *"]
  }
}
```

Both crons fire every day. The handler formats the current time in the `Europe/Rome` timezone and only proceeds when the wall clock says 09:30.

One of the two invocations does nothing. The other one triggers the build. No maintenance needed when the clocks change.

## Deploy it

Two commands:

```bash
npx wrangler deploy
npx wrangler secret put DEPLOY_HOOK_URL
```

The second one prompts for a value: paste the deploy hook URL. Storing it as a secret keeps it out of the code and out of the repository.

That's the whole system. Every morning at 9:30 the Worker POSTs the hook, Pages rebuilds the site, and any post whose date has passed goes live.

## One thing to know

If builds for the same branch are already queued, the deploy hook responds with HTTP `304` and does not create a new build.

That's normally fine — the queued build will include your latest commit anyway. But it's a useful signal to recognize when you're debugging why a hook "didn't work".

I kept the old GitHub Actions job around as a backup. But the Worker is the one I rely on now: it has fired at 9:30 sharp every day since I deployed it.
