Scheduling Posts with AstroJS

#tutorial

Before going on a trip this weekend, I wrote multiple posts so I didn’t need to write on my trip. However, to keep my writing streak, I had to manually release posts by setting their draft property to false. That sucked. Today I released scheduling posts so I don’t need to go through this pain again. Here’s how I did it.

Implementing scheduled posts

A scheduled post on my site is any post that is NOT in draft, with a pubDate that is in the future. I want to display a scheduled post when the pubDate is in the past. With the previous sentence in mind, I created a file that exported a method hasPubDatePassed.

import type { CollectionEntry } from 'astro:content';
import { formatInTimeZone } from 'date-fns-tz'

const formatDate = (date: Date): string => {
	return formatInTimeZone(date, 'Etc/UTC', 'yyyy-MM-dd');
}

const hasPubDatePassed = (post: CollectionEntry<"blog">) => {
	let todaysDate = new Date()
	todaysDate.setHours(0, 0, 0, 0)
	return formatDate(todaysDate) >= formatDate(post.data.pubDate);
};

export default hasPubDatePassed

I used this method in my filterPublishPosts helper (source)

const filterPublishedPosts = (allPosts: CollectionEntry<"blog">[]): CollectionEntry<"blog">[] => {
  return allPosts.sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
  ).filter((post) => import.meta.env.DEV || !isDraftPost(post) && hasPubDatePassed(post));
}

Where this filterPublishedPosts method sorts the posts and then displays only those that are not in draft and who’s pubDate is in the past.

Note: import.meta.env.DEV is used to display all posts in local development.

Timezone Gotcha’s

After a bug with timezones sometime last year, I knew it wasn’t enough to do a date comparison like below. todaysDate would have a timestamp but pubDate doesn’t have a timestamp. If we applied timezones to these two dates we could publish a post earlier then expected.

// ❌: 🕛 Timezone issues
const hasPubDatePassed = (post: CollectionEntry<"blog">) => {
	let todaysDate = new Date()
	return todaysDate >= post.data.pubDate;
};

To avoid dealing with timezones, I set the time of today’s date to 00:00. This time matches the time of pubDate. After setting the time, I convert both dates into UTC timezone, and output strings in a format like 2024-05-30. Finally, I do a string comparison that will return all posts with pubDate today or in the past.

// ✅: 🕛 Sets both times to UTC
const hasPubDatePassed = (post: CollectionEntry<"blog">) => {
	let todaysDate = new Date()
	todaysDate.setHours(0, 0, 0, 0)
	return formatDate(todaysDate) >= formatDate(post.data.pubDate);
};

I have a scheduled post for tomorrow to test this feature 🤞 it all works. Note to self: automated tests would be nice.

Update: GitHub Action to trigger a build

After I finished writing this post, I realised I missed one big thing. When AstroJS builds, it’s building as a static site. Which means at the time of the build, only those posts will exist. So scheduling a post in the future will never work because it will never exist in the AstroJS build step 🤦🏻.

To resolve this issue, I introduced a GitHub Action that would trigger daily at 6am UTC (around 2am EST). The GitHub Action will call a Netlify build hook , which will build the site and pull in the scheduled post. It’s important to keep the build hook a secret because it’s not authed, meaning anyone can curl the URL and trigger a build.

name: "Scheduled Build"
on:
  schedule:
    - cron: "0 6 * * *"
jobs:
  trigger_netlify_build:
    name: trigger_netlify_build
    runs-on: ubuntu-latest
    steps:
      - name: Call netlify build
        run: |
          curl -X POST -d {} ${{ secrets.NETLIFY_BUILD_HOOK }}

Want to stay connected?

Subscribe to my newsletter

Weekly, bite-sized, practical tips and insights that have meaningfully improved how I code, design, and write.

No spam. Unsubscribe anytime.