Build a Blog CMS with Next.js + shadcn/ui in 30 Minutes
From git clone to a working shadcn CMS-backed Next.js site, in 30 minutes
You can have a working blog CMS — Next.js front-end, shadcn/ui admin, posts, categories, media library — running in about 30 minutes. No cloud accounts. No Docker. One MySQL database, two git clone commands, and a single .env file.
This guide walks the full path: spin up UnfoldCMS as the content backend, point a Next.js front-end at its public API, and ship. By the end you'll have a real CMS admin built on shadcn/ui (the same 51 components you'd npx shadcn add) and a Next.js site rendering posts from it.
Why this stack: Next.js for the public site means SSR, edge caching, and the React ecosystem you already know. UnfoldCMS for the admin means you get a fully built shadcn/ui admin with 205 pages — posts, media, users, settings, SEO — without writing any admin UI yourself.
What you'll build
A two-service setup:
- Front-end — Next.js 15 app on
localhost:3000rendering a blog index and post pages, styled with shadcn/ui. - Backend — UnfoldCMS on
localhost:8000serving the admin (built on shadcn) and a public JSON API at/api/blog/posts.
The front-end fetches posts via fetch('http://localhost:8000/api/blog/posts'). The admin runs at localhost:8000/admin. Same shadcn design system across both — admin and public site visually match because they share the same component library.
Prerequisites
You need PHP 8.3+, Composer, Node 20+, pnpm, and MySQL or SQLite. If you're on macOS with Homebrew, brew install php composer node pnpm mysql covers the lot. On Linux, the equivalent apt/dnf packages.
This guide assumes you can run terminal commands and have a code editor. No prior Laravel or Next.js experience required.
Step 1 — Spin up UnfoldCMS (about 10 minutes)
Clone the CMS and install dependencies:
git clone https://github.com/hpakdaman/unfoldcms.git cms
cd cms
composer install
pnpm install
cp .env.example .env
php artisan key:generate
Set up the database. Easiest path: use SQLite for local development. Edit .env:
DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/cms/database/database.sqlite
Create the SQLite file and run migrations + seeds:
touch database/database.sqlite
php artisan migrate --seed
Build the admin front-end assets:
pnpm run build
Start the dev server:
php artisan serve
UnfoldCMS now runs at http://localhost:8000. Visit http://localhost:8000/admin and log in with the seeded admin account (the seeder prints the email and password to your terminal). You're staring at the shadcn/ui admin — 51 components across 205 pages, all rendered with Tailwind v4.
Create a test post: /admin/posts/create. Add a title, body, mark it published. Save.
Step 2 — Spin up Next.js (about 5 minutes)
In a separate terminal, scaffold a Next.js app:
pnpm create next-app@latest frontend --typescript --tailwind --app --no-src-dir --no-import-alias
cd frontend
Add shadcn/ui to the Next.js app — same component library the CMS uses:
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add card button badge
This pulls the shadcn card, button, and badge components into frontend/components/ui/. Same files you'd find in cms/resources/js/components/ui/ — that's the point. One design system, two apps.
Start the Next.js dev server:
pnpm dev
The site is now at http://localhost:3000.
Step 3 — Fetch posts from the CMS API
UnfoldCMS exposes a public read-only JSON endpoint at GET /api/blog/posts. No auth required for public reads. Open frontend/app/page.tsx and replace it with:
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import Link from "next/link";
type Post = {
id: number;
title: string;
slug: string;
short_description: string;
posted_at: string;
};
async function getPosts(): Promise<Post[]> {
const res = await fetch("http://localhost:8000/api/blog/posts?per_page=10", {
next: { revalidate: 60 },
});
const json = await res.json();
return json.data?.data ?? [];
}
export default async function HomePage() {
const posts = await getPosts();
return (
<main className="container mx-auto py-12">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
<div className="grid gap-6 md:grid-cols-2">
{posts.map((post) => (
<Link key={post.id} href={`/blog/${post.slug}`}>
<Card className="hover:shadow-lg transition-shadow">
<CardHeader>
<CardTitle>{post.title}</CardTitle>
<CardDescription>{post.short_description}</CardDescription>
</CardHeader>
<CardContent>
<Badge variant="secondary">
{new Date(post.posted_at).toLocaleDateString()}
</Badge>
</CardContent>
</Card>
</Link>
))}
</div>
</main>
);
}
Reload localhost:3000. The test post you created in Step 1 should now show up as a card, styled with shadcn.
Step 4 — Render a single post page
Create frontend/app/blog/[slug]/page.tsx:
import { notFound } from "next/navigation";
type Post = {
id: number;
title: string;
body: string;
posted_at: string;
};
async function getPost(slug: string): Promise<Post | null> {
const res = await fetch(`http://localhost:8000/api/blog/posts/${slug}`, {
next: { revalidate: 60 },
});
if (!res.ok) return null;
const json = await res.json();
return json.data ?? null;
}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return (
<article className="container mx-auto py-12 max-w-3xl">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<p className="text-muted-foreground mb-8">
{new Date(post.posted_at).toLocaleDateString()}
</p>
<div className="prose dark:prose-invert" dangerouslySetInnerHTML={{ __html: post.body }} />
</article>
);
}
Click a post card on the home page — you're on the detail page, content from the CMS, rendered through your Next.js app with shadcn styling.
Step 5 — Add categories
UnfoldCMS posts have categories. Use them to filter the feed. The API accepts a category query param:
const res = await fetch(`http://localhost:8000/api/blog/posts?category=tutorials&per_page=10`);
Build a category nav with shadcn Badge components, link each one to /?category=slug, and read the search param in your page component. That's the whole thing.
What you just shipped
In about 30 minutes you've built a real CMS-backed Next.js site:
- An admin at
localhost:8000/adminbuilt on 51 shadcn/ui components and 205 admin pages — posts, categories, media library, users, roles, SEO, settings. - A Next.js front-end at
localhost:3000consuming the CMS public API. - One design system shared by both, because both use shadcn/ui.
- One database, two services. No Docker. No cloud account.
This is the configuration most "best CMS for shadcn/ui" searches actually want: a working admin built on shadcn, plus a Next.js front-end you control. See The CMS Built on shadcn/ui: Why It Matters for why this matters more than headless-with-bring-your-own-admin.
Deploying to production
For production, the Next.js app deploys to Vercel, Netlify, or any Node host. The UnfoldCMS backend deploys to any PHP host — a $5 VPS, shared hosting, Hetzner, DigitalOcean, your own server. See Deploy a UnfoldCMS-Powered Site on Vercel for the front-end and Deploy a UnfoldCMS-Powered Site on Cloudflare Pages for the Cloudflare option. The CMS host and the front-end host can be different — they communicate via the public API.
Set NEXT_PUBLIC_CMS_URL=https://cms.yoursite.com in the Next.js project env, and replace the hardcoded http://localhost:8000 in your fetch calls. That's the whole production swap.
People Also Ask
Can I use shadcn/ui with a headless CMS?
Yes. shadcn lives on your front-end app — the Next.js / Astro / SvelteKit project that consumes the CMS API. The bigger question is whether the CMS admin is also built on shadcn. UnfoldCMS is the only option today where it is. With Payload, Sanity, Strapi, Directus, you get shadcn on the front-end but the admin runs on the vendor's own React library.
Does UnfoldCMS need Node.js?
No. UnfoldCMS is a Laravel monolith — PHP 8.3 backend, React admin rendered via Inertia. You only need Node to build the admin assets (one-time pnpm run build). At runtime, there's no Node server. Your Next.js front-end is a separate Node app — those two services don't share a runtime.
What's the CMS public API like?
GET /api/blog/posts returns a paginated list of published posts with per_page and category filters. GET /api/blog/posts/{slug} returns a single post. There's also /api/v1/* with broader endpoints (pages, categories, search, menus, settings). Auth is Sanctum for admin write operations; public read endpoints don't require auth.
Why Next.js and not Astro / SvelteKit / Nuxt?
This tutorial uses Next.js because it's the most popular pick among ICP-A devs. The same approach works with Astro, SvelteKit, Nuxt, or Remix — fetch the CMS API, render the result, ship. The CMS doesn't care which front-end framework calls it.
Where does the admin's shadcn code live?
In the UnfoldCMS source tree: cms/resources/js/components/ui/ holds the 51 shadcn components. cms/resources/js/pages/admin/ holds the 205 admin pages. Fork them like you'd fork any shadcn file — copy, edit, ship.
Bottom line
Thirty minutes from git clone to a real Next.js + shadcn CMS stack. The admin is built on shadcn, the front-end is built on shadcn, they talk over a clean JSON API. No Docker, no cloud account, no headless complexity for a project that doesn't need it.
Want to skip the local setup and see it live? Try the demo or see pricing.
Sources and methodology
- shadcn/ui component count verified:
find cms/resources/js/components/ui -name "*.tsx" \| wc -l= 51. - Admin page count verified:
find cms/resources/js/pages/admin -name "*.tsx" \| wc -l= 205. - Public API endpoints confirmed in
cms/routes/web.php(/api/blog/posts) andcms/routes/api.php(/api/v1/*). - Next.js 15 + shadcn/ui scaffolding confirmed against current stable docs as of June 2026.
- Tutorial steps tested end-to-end on macOS and Ubuntu 22.04 in June 2026.
Share this post:
Leave a Comment
Please log in to leave a comment.
Don't have an account? Register here