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:

  1. Manual rebuild: Push any commit to trigger rebuild
  2. 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:

Cons:

Method 2: Draft Branches with Scheduled Merge

How It Works

  1. Create post in a feature branch
  2. Open PR for review
  3. Schedule the PR merge for the publish date
  4. 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)

  1. Open the PR in GitHub
  2. Get approvals as needed
  3. In the merge button dropdown, select “Enable auto-merge”
  4. 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:

Cons:

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:

Cons:

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:

Cons:

For this content system, we recommend Method 2 (Draft Branches) + Method 3 (Published Flag):

Workflow

  1. Create Draft in Branch
    git checkout -b post/2026-01-15-topic
    
  2. Add Post with published: false
    ---
    layout: post
    title: "Post Title"
    date: 2026-01-15
    published: false
    ---
    
  3. Open PR for Review
    • Get feedback
    • Make edits
    • Approve when ready
  4. Merge PR (Post Still Hidden)
    • Post is in _posts/ but published: false
  5. Scheduled Publish Action
    • Daily cron checks for posts with today’s date
    • Changes published: false to published: true
    • Commits and pushes

Benefits

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"