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
}
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 },
});
}
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');
}
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)