Adding Scheduled Posts to My GitHub Discussions Powered Blog
3 min read
When I first built my GitHub Discussions–powered blog and later created the Astro Discussions Blog Loader, one critical feature was still missing: scheduled posts.
The original setup worked great for immediate posts: create a discussion, and a GitHub webhook would trigger a Netlify build. But what if I wanted to write a post today and publish it next week? That required something smarter.
The Solution: Netlify Functions
I solved this with two small but powerful Netlify Functions that work together:
- Webhook Handler – decides if a new or updated discussion should be published immediately or scheduled for later.
- Scheduled Publisher – runs every 5 minutes, checks scheduled posts, and publishes them once their time comes.
This way, all scheduling logic lives in GitHub Discussions itself—no external database or service required.
1. Webhook Handler (webhook.mjs
)
Instead of having the GitHub webhook trigger a site build directly, I now route it through a handler.
Here’s the decision flow:
- If the publish date ≤ now → publish immediately.
- If the publish date is in the future → add a
state/scheduled
label and exit without triggering a build.
This ensures that future-dated posts don’t go live until the correct time.
import { addScheduledLabel, removeScheduledLabel, verifySignature } from "./lib/github-utils.mjs";
import { isValidPost, parsePostPublishDate, isPostPublished } from "./lib/blog-utils.mjs";
import { triggerDeploy } from "./lib/netlify-utils.mjs";
export default async (req) => {
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
const bodyText = await req.text();
if (!verifySignature(req, bodyText)) return new Response("Invalid signature", { status: 401 });
if (req.headers.get("x-github-event") !== "discussion")
return new Response("Ignored", { status: 202 });
const payload = JSON.parse(bodyText);
const d = payload.discussion;
const { node_id:id, title } = d;
const now = new Date();
const valid = isValidPost(d);
if (!valid.valid) return new Response(valid.reason, { status: 200 });
const { publishDate } = parsePostPublishDate(d.body, now);
if (publishDate > now) {
await addScheduledLabel(d.node_id);
console.log(`Scheduled post '${title}' [${id}] for ${publishDate.toISOString()}`);
return new Response("Scheduled", { status: 201 });
}
await removeScheduledLabel(d.node_id);
await triggerDeploy();
console.log(`Publishing post '${title}' [${id}]`);
return new Response("Published", { status: 200 });
};
2. Scheduled Publisher (tick.mjs
)
Once posts are labeled as scheduled, the second piece of the puzzle is a scheduled function. This function runs every 5 minutes, finds all discussions with the state/scheduled
label, and checks if their publish date has passed.
If it has, the function removes the label, logs the publish event, and triggers a site build. If multiple posts are ready at the same time, they’re all published in one go.
import { removeScheduledLabel, getScheduledDiscussions } from "./lib/github-utils.mjs";
import { triggerDeploy } from "./lib/netlify-utils.mjs";
import { parsePostPublishDate } from "./lib/blog-utils.mjs";
export const config = { schedule: "*/5 * * * *" };
export default async () => {
const now = new Date();
const posts = await getScheduledDiscussions();
const publishedPosts = await Promise.all(
posts.map(async post => {
const { publishDate } = parsePostPublishDate(post.body, now);
if (publishDate <= now) {
await removeScheduledLabel(post.id);
console.log(`Publishing post '${post.title}' [${post.id}]`);
return post;
}
})
);
const publishCount = publishedPosts.filter(Boolean).length;
if (publishCount > 0) await triggerDeploy();
return new Response(`Published ${publishCount} posts`, { status: 200 });
};
Other Cool Features
Beyond the main scheduling flow, there are a few extra touches worth noting:
-
Dynamic time zone handling Posts publish according to my own time zone (with full DST support).
-
Webhook validation Ensures only GitHub can trigger the workflow.
-
Published state checking Uses my site’s RSS feed to confirm which posts are live.
-
GitHub Discussions Blog Loader update Now supports ignoring multiple labels (e.g.
state/draft
andstate/scheduled
).
In Summary
With just two small Netlify Functions and some clever label management, my GitHub Discussions blog now has full scheduled publishing.
I can write posts weeks in advance, schedule them for the perfect time, and trust they’ll go live automatically. And this is all kept within my existing infrastructure of GitHub and Netlify.
If you’d like to dive deeper, you can check out the full source code in my repository.
Until next time 👋