DEV Community

Cover image for How We Automatically Publish Scheduled Blog Posts on nixx.dev
NIXX/DEV
NIXX/DEV

Posted on • Originally published at nixx.dev

How We Automatically Publish Scheduled Blog Posts on nixx.dev

No CMS. Just GitHub Actions, Markdown, and a little Next.js magic.


At nixx.dev, we like to write ahead and publish later. But we don’t like logging in just to hit “Publish.”

So we automated it.

With GitHub Actions, Markdown, and a custom Next.js webhook, our posts now publish themselves—right on schedule.

Here’s how we set it up 👇


Step 1: Markdown Posts with Metadata

Every blog post lives in /content/blog as a Markdown file with front matter like this:

---
title: "How to Build a Website in 2025"
date: "2025-04-10T08:00:00Z"
status: draft # options: draft, scheduled, published
---
Enter fullscreen mode Exit fullscreen mode

This lets us:

  • Write and commit content ahead of time
  • Use status to control visibility
  • Set a publish date in the future

When a post is ready, we switch the status to scheduled.


Step 2: Admin UI (Optional but Nice)

We also built a small admin dashboard with login support, so we can:

  • Edit posts visually
  • Schedule them with a date picker
  • Preview before they go live

Totally optional—but great for workflow.


Step 3: Daily GitHub Action

We trigger a GitHub Action every day at 7 AM UTC:

name: Publish Scheduled Posts

on:
  schedule:
    - cron: '0 7 * * *'

jobs:
  publish:
    runs-on: ubuntu-latest

    steps:
      - name: Trigger Webhook
        uses: distributhor/workflow-webhook@v1
        with:
          url: https://nixx.dev/api/webhooks/check-scheduled-posts
          method: POST
Enter fullscreen mode Exit fullscreen mode

It hits an API route that checks all posts and updates any that are due.


Step 4: Webhook Logic in Next.js v15

With Next.js v15 and the App Router, we use Route Handlers.

// app/api/webhooks/check-scheduled-posts/route.ts

import { promises as fs } from 'fs';
import path from 'path';
import matter from 'gray-matter';

export async function POST() {
  const POSTS_DIR = path.join(process.cwd(), 'content/blog');
  const files = await fs.readdir(POSTS_DIR);

  for (const file of files) {
    const filePath = path.join(POSTS_DIR, file);
    const content = await fs.readFile(filePath, 'utf-8');
    const { data } = matter(content);
    const now = new Date();
    const publishDate = new Date(data.date);

    if (data.status === 'scheduled' && publishDate <= now) {
      const updated = matter.stringify(content, { ...data, status: 'published' });
      await fs.writeFile(filePath, updated);
      console.log(`Published: ${data.title}`);
    }
  }

  return new Response(
    JSON.stringify({ message: 'Checked and published scheduled posts.' }),
    {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Deploy and Rebuild

After a post is marked published, we rebuild the site:

  • Staging is on Vercel, which auto-builds on GitHub pushes
  • Production runs on a VPS, using a simple script:
cd /var/www/nixx.dev
git pull origin main
npm run build
pm2 restart next
Enter fullscreen mode Exit fullscreen mode

✅ TL;DR

  • Posts are Markdown files with status and date fields
  • GitHub Action runs daily and triggers a webhook
  • Webhook updates post status from scheduled to published
  • The site rebuilds, and new posts appear—hands-free

Final Thoughts

This setup keeps our blog running without a CMS, plugins, or dashboard overhead. Just content, Git, and automation.

It’s reliable, version-controlled, and totally code-driven.

If you’re building with Next.js v15, give this approach a shot—and feel free to fork the idea to fit your stack.


💬 Got questions or want help setting this up? Drop a comment—I’d love to hear how you’re building your blog!

Top comments (0)