Scheduling Blog Post Releases
Overview
This document describes methods for preparing blog posts in advance via pull requests and scheduling their release for a future date. GitHub Pages/Jekyll offers several approaches, from simple to sophisticated.
Method Comparison
| Method | Complexity | Automation | Best For |
|---|---|---|---|
| Future-dated posts | Low | Manual rebuild | Simple scheduling |
| Draft branch + scheduled merge | Medium | Built-in | PR-based workflow |
published: false flag |
Low | Manual | Holding posts |
| GitHub Actions scheduled publish | High | Fully automated | Production systems |
Method 1: Future-Dated Posts (Simplest)
How It Works
Jekyll excludes posts with future dates from builds by default. When the date arrives, the next build includes the post.
Implementation
---
layout: post
title: "My Scheduled Post"
date: 2026-01-15 # Future date - won't appear until this date
author: Robert Allen
---
Triggering Publication
The post won’t appear until a site rebuild occurs on or after the date. Options:
- Manual rebuild: Push any commit to trigger rebuild
- Scheduled rebuild: Use GitHub Actions cron job
Scheduled Rebuild Workflow
Create .github/workflows/scheduled-rebuild.yml:
name: Scheduled Site Rebuild
on:
schedule:
# Run daily at 00:05 UTC
- cron: '5 0 * * *'
workflow_dispatch:
jobs:
rebuild:
runs-on: ubuntu-latest
steps:
- name: Trigger Pages Build
run: |
curl -X POST \
-H "Authorization: token $" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/$/pages/builds
Pros and Cons
Pros:
- Simple, no extra tooling
- Posts visible in repo immediately
- Standard Jekyll feature
Cons:
- Requires rebuild to publish
- Exact publish time depends on rebuild schedule
- Posts visible in source before live
Method 2: Draft Branches with Scheduled Merge
How It Works
- Create post in a feature branch
- Open PR for review
- Schedule the PR merge for the publish date
- Merge triggers site rebuild
Implementation
Step 1: Create Post Branch
# Create branch for the post
git checkout -b post/2026-01-15-ai-coding-assistants
# Create the post file
cat > _posts/2026-01-15-ai-coding-assistants.md << 'EOF'
---
layout: post
title: "The State of AI Coding Assistants in 2026"
date: 2026-01-15
author: Robert Allen
---
Content here...
EOF
git add .
git commit -m "Add: AI Coding Assistants blog post"
git push -u origin post/2026-01-15-ai-coding-assistants
Step 2: Create Pull Request
gh pr create \
--title "[Blog] The State of AI Coding Assistants in 2026" \
--body "Scheduled for publication: 2026-01-15" \
--label "content,scheduled"
Step 3: Schedule Merge (GitHub UI)
- Open the PR in GitHub
- Get approvals as needed
- In the merge button dropdown, select “Enable auto-merge”
- Or use the “Schedule merge” feature (GitHub Enterprise)
Alternative: Merge Queue with Schedule
For GitHub Enterprise users, use merge queues:
# In repo settings, configure merge queue
merge_queue:
enabled: true
merge_method: squash
Automated Scheduled Merge (GitHub Actions)
Create .github/workflows/scheduled-merge.yml:
name: Scheduled PR Merge
on:
schedule:
# Check every hour
- cron: '0 * * * *'
workflow_dispatch:
jobs:
merge-scheduled-prs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Find and Merge Scheduled PRs
env:
GH_TOKEN: $
run: |
# Get current date
TODAY=$(date +%Y-%m-%d)
# Find PRs with "scheduled" label
prs=$(gh pr list --label scheduled --json number,title,body)
echo "$prs" | jq -r '.[] | @base64' | while read pr; do
_jq() {
echo "$pr" | base64 --decode | jq -r "$1"
}
PR_NUM=$(_jq '.number')
PR_BODY=$(_jq '.body')
# Extract scheduled date from PR body
# Expected format: "Scheduled for publication: YYYY-MM-DD"
SCHEDULED_DATE=$(echo "$PR_BODY" | grep -oP 'Scheduled for publication: \K\d{4}-\d{2}-\d{2}')
if [[ "$SCHEDULED_DATE" == "$TODAY" ]]; then
echo "Merging PR #$PR_NUM (scheduled for $SCHEDULED_DATE)"
gh pr merge $PR_NUM --squash --delete-branch
fi
done
Pros and Cons
Pros:
- Full PR review workflow
- Clear audit trail
- Posts hidden until merge
- Works with existing CI/CD
Cons:
- Requires PR for each post
- Manual scheduling or custom automation
- More complex workflow
Method 3: Published Flag
How It Works
Jekyll supports a published: false frontmatter flag that excludes posts from the build.
Implementation
---
layout: post
title: "My Hidden Post"
date: 2026-01-15
published: false # Post won't appear on site
---
Publishing the Post
When ready to publish, change to published: true or remove the line:
# Find all unpublished posts
grep -r "published: false" _posts/
# Update and commit
sed -i 's/published: false/published: true/' _posts/2026-01-15-ai-coding-assistants.md
git add .
git commit -m "Publish: AI Coding Assistants post"
git push
Automated Publishing
Create .github/workflows/publish-scheduled.yml:
name: Publish Scheduled Posts
on:
schedule:
# Run daily at 00:05 UTC
- cron: '5 0 * * *'
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Find Posts to Publish
id: find
run: |
TODAY=$(date +%Y-%m-%d)
# Find posts with published: false and today's date
for file in _posts/*.md; do
POST_DATE=$(grep -oP '^date: \K\d{4}-\d{2}-\d{2}' "$file" || echo "")
PUBLISHED=$(grep -oP '^published: \K\w+' "$file" || echo "true")
if [[ "$POST_DATE" == "$TODAY" && "$PUBLISHED" == "false" ]]; then
echo "Publishing: $file"
sed -i 's/published: false/published: true/' "$file"
fi
done
- name: Commit Changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "No posts to publish"
else
git add .
git commit -m "Auto-publish scheduled posts for $(date +%Y-%m-%d)"
git push
fi
Pros and Cons
Pros:
- Simple flag to toggle
- Posts in main branch (easy to track)
- Works with any build system
Cons:
- Posts visible in source repo
- Requires rebuild after flag change
- Manual or automated process needed
Method 4: Full Automation with GitHub Actions
How It Works
Combine calendar data with GitHub Actions for fully automated publishing.
Implementation
Create .github/workflows/auto-publish.yml:
name: Auto-Publish from Calendar
on:
schedule:
# Run daily at 00:05 UTC
- cron: '5 0 * * *'
workflow_dispatch:
jobs:
check-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: npm install js-yaml
- name: Check Calendar for Today's Posts
run: |
node << 'EOF'
const fs = require('fs');
const yaml = require('js-yaml');
const path = require('path');
const today = new Date().toISOString().split('T')[0];
const calendarDir = 'calendar';
const postsToPublish = [];
function scanCalendar(filepath) {
const content = fs.readFileSync(filepath, 'utf8');
const cal = yaml.load(content);
if (!cal.months) return;
for (const [month, data] of Object.entries(cal.months)) {
if (!data.posts) continue;
for (const post of data.posts) {
if (post.publish_date === today && post.status === 'in-progress') {
postsToPublish.push(post);
}
}
}
}
// Scan all calendar files
const files = fs.readdirSync(calendarDir, { recursive: true });
for (const file of files) {
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
scanCalendar(path.join(calendarDir, file));
}
}
console.log(`Found ${postsToPublish.length} posts to publish today`);
fs.writeFileSync('publish-today.json', JSON.stringify(postsToPublish, null, 2));
EOF
- name: Publish Posts
run: |
node << 'EOF'
const fs = require('fs');
const posts = JSON.parse(fs.readFileSync('publish-today.json', 'utf8'));
for (const post of posts) {
// Find matching draft
const slug = post.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
const draftPath = `content/blog/drafts/${post.due_date}-${slug}.md`;
const publishPath = `_posts/${post.publish_date}-${slug}.md`;
if (fs.existsSync(draftPath)) {
// Read draft content
let content = fs.readFileSync(draftPath, 'utf8');
// Update frontmatter
content = content.replace(/^published: false/m, 'published: true');
content = content.replace(/^date: .+$/m, `date: ${post.publish_date}`);
// Write to _posts
fs.writeFileSync(publishPath, content);
console.log(`Published: ${publishPath}`);
// Remove from drafts
fs.unlinkSync(draftPath);
}
}
EOF
- name: Commit and Push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "No changes to commit"
else
git add .
git commit -m "Auto-publish: $(date +%Y-%m-%d)"
git push
fi
Pros and Cons
Pros:
- Fully automated
- Calendar-driven
- No manual intervention
- Works with existing calendar system
Cons:
- Complex setup
- Requires careful testing
- Debugging can be challenging
Recommended Approach
For this content system, we recommend Method 2 (Draft Branches) + Method 3 (Published Flag):
Workflow
- Create Draft in Branch
git checkout -b post/2026-01-15-topic - Add Post with
published: false--- layout: post title: "Post Title" date: 2026-01-15 published: false --- - Open PR for Review
- Get feedback
- Make edits
- Approve when ready
- Merge PR (Post Still Hidden)
- Post is in
_posts/butpublished: false
- Post is in
- Scheduled Publish Action
- Daily cron checks for posts with today’s date
- Changes
published: falsetopublished: true - Commits and pushes
Benefits
- Full review workflow
- Posts hidden until publish date
- Automated publishing
- Clear audit trail
- Separation of merge and publish
Timezone Considerations
GitHub Actions cron runs in UTC. Plan accordingly:
| Target Timezone | UTC Equivalent |
|---|---|
| 9am EST | 14:00 UTC (winter) / 13:00 UTC (summer) |
| 9am PST | 17:00 UTC (winter) / 16:00 UTC (summer) |
| 9am CET | 08:00 UTC (winter) / 07:00 UTC (summer) |
Configure cron for your target publish time:
# Publish at 9am EST (14:00 UTC winter)
- cron: '0 14 * * *'
Debugging Scheduled Posts
Check Post Status
# List all posts and their published status
for f in _posts/*.md; do
DATE=$(grep -oP '^date: \K.+' "$f")
PUBLISHED=$(grep -oP '^published: \K\w+' "$f" || echo "true")
echo "$f: date=$DATE, published=$PUBLISHED"
done
Force Rebuild
# Trigger manual rebuild
gh workflow run pages-build-deployment
Test Locally with Future Posts
# Jekyll serve with future posts visible
bundle exec jekyll serve --future
Calendar Integration
Update calendar status when publishing:
# Before
- title: "My Post"
status: in-progress
publish_date: 2026-01-15
# After (automated or manual)
- title: "My Post"
status: published
publish_date: 2026-01-15
published_url: "https://www.zircote.com/blog/2026/01/15/my-post"