Back to blog
Aug 04, 2025
7 min read

Building Scheduled Posts for Astro SSR Sites with GitHub Actions

Learn how to add scheduled post functionality to your Astro SSR site using GitHub Actions for daily rebuilds. This approach keeps your site performant while enabling content to go live automatically on scheduled dates.

If you’re running an Astro blog in SSR mode and want to schedule posts for future publication, you might discover a challenge: date filtering happens at build time, not request time. This means posts with future dates won’t magically appear when their date arrives - they need a rebuild to go live. Here’s how I solved this problem using GitHub Actions, maintaining site performance while adding scheduling capabilities.

The Problem: Build-Time vs Runtime

When you filter posts by date in Astro:

const posts = (await getCollection("blog"))
  .filter((post) => !post.data.draft && new Date(post.data.date) <= new Date())

This filtering happens when your site builds, not when users visit. In SSR mode with services like Vercel, your site is built once and cached. Future-dated posts remain hidden until the next build triggers.

The Solution Architecture

Instead of making pages fully dynamic (which would hurt performance), we’ll use GitHub Actions to trigger daily rebuilds. This approach:

  • Maintains fast, cached pages between rebuilds
  • Automatically publishes scheduled content
  • Requires no manual intervention
  • Costs nothing (GitHub Actions free tier is generous)

Step-by-Step Implementation

Step 1: Add Date Filtering to Your Content

First, ensure all your content endpoints filter out future posts. Here’s what you need to update:

Blog Listing Page (src/pages/blog/index.astro)

const posts = (await getCollection("blog"))
  .filter((post) => !post.data.draft && new Date(post.data.date) <= new Date())
  .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());

Tag Pages (src/pages/tags/[tag].astro)

export async function getStaticPaths() {
  const posts = await getCollection("blog", ({ data }) => 
    !data.draft && new Date(data.date) <= new Date()
  );
  
  const uniqueTags = [...new Set(posts.flatMap(post => post.data.tags))];
  
  return uniqueTags.map(tag => ({
    params: { tag },
    props: { 
      posts: posts
        .filter(post => post.data.tags.includes(tag))
        .sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
    }
  }));
}

RSS/JSON Feeds (src/pages/rss.xml.js, src/pages/feed.json.js)

const posts = await getCollection("blog");
const nonDraftPosts = posts.filter(
  (post) => !post.data.draft && new Date(post.data.date) <= new Date()
);

Sitemap (src/pages/sitemap.xml.ts)

posts
  .filter(post => !post.data.draft && new Date(post.data.date) <= new Date())
  .forEach(post => {
    urls.push({
      loc: `${site}/blog/${post.slug}/`,
      changefreq: 'monthly',
      priority: 0.8
    });
  });

Step 2: Create the GitHub Action

Create .github/workflows/scheduled-rebuild.yml:

name: Scheduled Site Rebuild

on:
  schedule:
    # Run at 12:05 AM Central Time (5:05 AM UTC)
    - cron: '5 5 * * *'
  
  # Allow manual trigger from GitHub Actions tab
  workflow_dispatch:

jobs:
  trigger-rebuild:
    runs-on: ubuntu-latest
    
    steps:
      - name: Trigger Vercel Deployment
        run: |
          if [ -z "${{ secrets.VERCEL_DEPLOY_HOOK }}" ]; then
            echo "VERCEL_DEPLOY_HOOK secret is not set"
            exit 1
          fi
          
          curl -X POST "${{ secrets.VERCEL_DEPLOY_HOOK }}" \
            -H "Content-Type: application/json" \
            -d '{"triggered_by": "scheduled-rebuild"}' \
            --fail \
            --show-error
          
          echo "✅ Deployment triggered successfully"
      
      - name: Log Rebuild
        run: |
          echo "Scheduled rebuild triggered at $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
          echo "This rebuild ensures scheduled posts go live on their publication date"

Step 3: Create a Vercel Deploy Hook

  1. Go to your Vercel Dashboard
  2. Select your project
  3. Navigate to SettingsGit
  4. Scroll to Deploy Hooks
  5. Create a new hook:
    • Name: scheduled-rebuild
    • Git Branch: main (or your production branch)
  6. Copy the generated webhook URL

Step 4: Add the Hook to GitHub Secrets

  1. Go to your GitHub repository
  2. Navigate to SettingsSecrets and variablesActions
  3. Click New repository secret
  4. Create the secret:
    • Name: VERCEL_DEPLOY_HOOK
    • Secret: Paste the webhook URL from Vercel
  5. Click Add secret

Step 5: Test Your Setup

  1. Go to the Actions tab in your GitHub repository
  2. Find Scheduled Site Rebuild workflow
  3. Click Run workflowRun workflow
  4. Check your Vercel dashboard to confirm the deployment triggered
  5. Verify your scheduled posts appear after the build completes

Usage Guide

Creating Scheduled Posts

Now you can create posts with future dates:

---
title: "Announcing Our New Feature"
description: "Exciting news about our latest release"
summary: "We're launching something amazing next week"
date: "2025-12-25"  # Will go live on December 25
tags: ["announcement", "features"]
draft: false  # Not a draft, just scheduled
---

Your content here...

Important Distinctions

  • draft: true = Never published (regardless of date)
  • draft: false + future date = Published when date arrives
  • draft: false + past date = Published immediately

Customizing Rebuild Frequency

The default schedule runs once daily at midnight Central Time. To adjust:

# Every 12 hours
- cron: '0 */12 * * *'

# Every 6 hours
- cron: '0 */6 * * *'

# Every hour (for time-sensitive content)
- cron: '0 * * * *'

# Specific times (8 AM and 5 PM Central)
- cron: '0 13 * * *'  # 8 AM CT (13:00 UTC)
- cron: '0 22 * * *'  # 5 PM CT (22:00 UTC)

Alternative Approaches We Considered

Making Pages Fully Dynamic

We could remove date filtering and check dates at request time:

// In your page component
const posts = posts.filter(post => {
  if (typeof window !== 'undefined') {
    return new Date(post.data.date) <= new Date();
  }
  return true;
});

Why we didn’t choose this: It would make every page request hit the server, eliminating Vercel’s edge caching and significantly impacting performance.

Using Incremental Static Regeneration (ISR)

Vercel’s ISR could revalidate pages on a schedule:

export const config = {
  isr: {
    expiration: 60 * 60 * 24, // 24 hours
  },
};

Why we didn’t choose this: ISR in Astro is complex and doesn’t guarantee posts appear exactly when scheduled. The GitHub Actions approach is simpler and more predictable.

Manual Rebuilds

Simply triggering deploys manually when posts should go live.

Why we didn’t choose this: Defeats the purpose of scheduling. We want automation!

Monitoring and Troubleshooting

Verify Scheduled Posts

Check which posts are scheduled but not yet live:

# In your local environment
npm run dev

# Check the blog listing - scheduled posts shouldn't appear
# Check individual post URLs - they should 404 until their date

Monitor GitHub Actions

  • Check the Actions tab for rebuild history
  • Failed rebuilds show as ❌ with error details
  • Successful rebuilds trigger Vercel deployments

Common Issues and Fixes

Posts not appearing after scheduled date?

  • Verify the GitHub Action ran successfully
  • Check date format: must be "YYYY-MM-DD"
  • Ensure draft: false in frontmatter
  • Manually trigger a rebuild to test

GitHub Action failing?

  • Verify VERCEL_DEPLOY_HOOK secret is set correctly
  • Check if the webhook URL is valid (test with curl locally)
  • Look for Vercel deployment quota issues

Time zone confusion?

  • Dates in frontmatter are interpreted as midnight local time
  • The rebuild runs at midnight Central Time
  • Posts may appear up to 24 hours after their date depending on timing

Cost Considerations

This solution is essentially free:

  • GitHub Actions: 2,000 minutes/month free (our daily rebuild uses ~1 minute/day = 30 minutes/month)
  • Vercel Builds: Hobby plan includes 6,000 build minutes/month
  • No additional infrastructure needed

Security Notes

The deploy hook is sensitive - anyone with it can trigger builds. GitHub Secrets are encrypted and safe, but:

  • Never commit the webhook URL to your repository
  • Rotate the hook periodically (delete and recreate in Vercel)
  • Monitor your Vercel dashboard for unexpected deployments

Conclusion

Scheduled posts in Astro SSR don’t have to be complicated or expensive. By combining GitHub Actions with Vercel’s deploy hooks, we get a robust scheduling system that:

  • Maintains optimal site performance
  • Requires zero manual intervention
  • Costs nothing to operate
  • Scales to any posting frequency

The beauty of this approach is its simplicity. No external services, no complex state management, just a daily rebuild that makes your scheduled content appear like magic.

Now I can write posts whenever inspiration strikes and schedule them for optimal publishing times. The robots handle the rest while I sleep!

Complete Code Reference

All the code from this tutorial is available in my blog’s repository. The key files are:

  • .github/workflows/scheduled-rebuild.yml - The GitHub Action
  • src/pages/blog/index.astro - Blog listing with date filtering
  • src/pages/tags/[tag].astro - Tag pages with date filtering
  • src/pages/sitemap.xml.ts - Dynamic sitemap with date filtering

Feel free to adapt this solution for your own Astro site. Happy scheduling!

Let's Build AI That Works

Ready to implement these ideas in your organization?