DEV Community

the bomber
the bomber

Posted on

Building a Pragmatic Blog CMS in Next.js 14 and Prisma with Drafts and ISR

Why Build Another CMS?

I'm Kaleb, a web developer here in Pompano Beach, Florida, running Dream Futures Tech. For my own business site, I wanted a blog workflow that didn't rely on bloated third-party SaaS tools or expensive managed databases. I already run an automation server at home handling internal workflows, so hosting my own PostgreSQL database alongside my Next.js site was a natural fit.

Instead of installing a heavy WordPress instance or paying monthly fees for a headless CMS, I built a lightweight blogging pipeline directly into my Next.js 14 App Router codebase. It handles drafts, public publishing, and Incremental Static Regeneration (ISR) using Prisma and Server Actions. Here is how I set it up for my own site.

The Prisma Schema

Everything starts with a clean database schema. I keep my Prisma schema simple. A single Post model handles titles, slugs, markdown or HTML content, and a boolean flag for publication status.

model Post {
  id          String   @id @default(cuid())
  title       String
  slug        String   @unique
  content     String   @db.Text
  published   Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}
Enter fullscreen mode Exit fullscreen mode

Keeping the schema minimal means I don't have to migrate complex taxonomy tables or tags until I actually need them. For my business blog, simple markdown storage and a unique slug are plenty.

Managing Drafts vs. Published Content

The core requirement for any CMS is keeping unfinished posts out of the public eye while allowing me to preview them. In my admin panel, I fetch all posts regardless of their status. But for the public-facing routes, the database query strictly filters for published: true.

Here is how I structure the public blog fetching function:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export async function getPublishedPosts() {
  return await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
  });
}

export async function getPostBySlug(slug: string) {
  return await prisma.post.findUnique({
    where: { slug, published: true },
  });
}
Enter fullscreen mode Exit fullscreen mode

For the admin routes, I use a separate query that bypasses the published: true restriction, protected by a simple cookie-based authentication check. This separation keeps the public queries safe and performant.

ISR and revalidatePath After Publishing

Performance matters, even for a low-traffic business site. I want my blog pages to load instantly from the cache, but I also want them to update immediately when I hit 'Publish' on a new draft. This is where Next.js 14 App Router and ISR shine.

By default, dynamic pages can be statically generated and cached. When I update a post or flip a draft to published, I trigger a Next.js Server Action. Inside that action, I update the database and then call revalidatePath to purge the cache for that specific slug and the main blog listing page.

Here is what the publishing Server Action looks like:

'use server';

import { PrismaClient } from '@prisma/client';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

const prisma = new PrismaClient();

export async function publishPost(formData: FormData) {
  const id = formData.get('id') as string;
  const title = formData.get('title') as string;
  const slug = formData.get('slug') as string;
  const content = formData.get('content') as string;
  const published = formData.get('published') === 'true';

  await prisma.post.upsert({
    where: { id: id || '' },
    update: { title, slug, content, published },
    create: { title, slug, content, published },
  });

  // Clear the cache for the blog index and the specific post
  revalidatePath('/blog');
  revalidatePath(`/blog/${slug}`);

  redirect('/admin/posts');
}
Enter fullscreen mode Exit fullscreen mode

This approach gives me the best of both worlds. The server at home doesn't get hammered by database queries on every page view because Next.js serves the cached static page. Yet, the moment I publish an update, the cache invalidates instantly without requiring a full site rebuild.

A Pragmatic Judgement

Is building a custom CMS overkill when off-the-shelf options exist? That is a fair judgement. If you need complex multi-author workflows, role-based permissions, or rich media management, building it yourself is a waste of time.

However, for a solo developer or small business owner who writes their own content, a custom Next.js and Prisma setup removes external dependencies, keeps hosting costs at zero on my home server, and gives me absolute control over the code. It fits my workflow precisely, and maintaining it takes almost zero effort.

I build sites and automations for small businesses at dreamfuturestech.com.

Top comments (0)