On this page
Full-Stack Blog
Create a blog platform with server-rendered pages, a database, and auth. This project combines Next.js App Router, an ORM, and protected routes.
Requirements
Features
- Public blog listing and individual post pages
- Markdown content rendered to HTML
- Admin dashboard to create, edit, and delete posts
- User authentication for admin routes
- Responsive layout
Step 1: Scaffold the Project
npx create-next-app@latest my-blog --typescript --tailwind --app --src-dir
cd my-blog
npm install @prisma/client next-auth bcryptjs react-markdown
npm install -D prisma
npx prisma init
Step 2: Database Schema
prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
slug String @unique
content String
published Boolean @default(false)
createdAt DateTime @default(now())
authorId Int
author User @relation(fields: [authorId], references: [id])
}
Run migrations with npx prisma migrate dev --name init.
Step 3: Blog Listing Page
src/app/page.tsx
import Link from 'next/link';
import { prisma } from '@/lib/prisma';
export default async function Home() {
const posts = await prisma.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
select: { id: true, title: true, slug: true, createdAt: true },
});
return (
<main className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">My Blog</h1>
<ul className="space-y-4">
{posts.map(post => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
<p className="text-gray-500 text-sm">{post.createdAt.toLocaleDateString()}</p>
</li>
))}
</ul>
</main>
);
}
Step 4: Post Page
src/app/blog/[slug]/page.tsx — fetch by slug with prisma.post.findUnique, call notFound() if missing, and render content with <ReactMarkdown>.
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await prisma.post.findUnique({
where: { slug: params.slug, published: true },
include: { author: { select: { email: true } } },
});
if (!post) notFound();
return (
<article className="max-w-2xl mx-auto p-8 prose">
<h1>{post.title}</h1>
<p className="text-gray-500">By {post.author.email}</p>
<ReactMarkdown>{post.content}</ReactMarkdown>
</article>
);
}
Step 5: Admin API and Auth
src/app/api/posts/route.ts — protect writes with NextAuth:
export async function POST(request: Request) {
const session = await getServerSession();
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { title, content, slug } = await request.json();
const post = await prisma.post.create({
data: { title, content, slug, published: true, authorId: Number(session.user.id) },
});
return NextResponse.json(post, { status: 201 });
}
Configure NextAuth with a credentials provider in src/app/api/auth/[...nextauth]/route.ts. Hash passwords with bcryptjs on registration and protect /admin routes with getServerSession().
Step 6: Admin Dashboard
src/app/admin/page.tsx
'use client';
import { useState } from 'react';
export default function AdminPage() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const slug = title.toLowerCase().replace(/\s+/g, '-');
await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content, slug }),
});
setTitle(''); setContent('');
}
return (
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-8 space-y-4">
<input value={title} onChange={e => setTitle(e.target.value)} placeholder="Post title" className="w-full border p-2 rounded" />
<textarea value={content} onChange={e => setContent(e.target.value)} placeholder="Write in Markdown..." rows={12} className="w-full border p-2 rounded" />
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">Publish</button>
</form>
);
}
Extension Ideas
- Draft mode — save unpublished posts and preview before publishing
- Tags and categories — filter posts by topic
- Comments — add a
Commentmodel with moderation - SEO — dynamic
metadataand Open Graph images per post - Image uploads — store cover images in Cloudinary or S3
- RSS feed — generate
/feed.xmlfor subscribers