Loading...
Loading...
> Database-driven blog with full admin control and SEO optimization.
The blog system stores posts in your database using Prisma, giving you full control over content. No external CMS needed - manage everything from your admin dashboard. Features include categories, featured posts, view tracking, SEO metadata, and automatic read time calculation.
DESC: Blog models are included in your Prisma schema
1// prisma/schema.prisma2model BlogPost {3 id String @id @default(cuid())4 slug String @unique5 title String6 excerpt String?7 content String @db.Text8 featured Boolean @default(false)9 published Boolean @default(false)1011 authorId String12 author User @relation(fields: [authorId], references: [id])1314 categoryId String?15 category BlogCategory? @relation(fields: [categoryId], references: [id])1617 featuredImage String?18 readTime Int? // Minutes to read1920 seoTitle String?21 seoDescription String?2223 viewCount Int @default(0)24 createdAt DateTime @default(now())25 publishedAt DateTime?26 updatedAt DateTime @updatedAt27}2829model BlogCategory {30 id String @id @default(cuid())31 name String @unique32 slug String @unique33 description String?34 posts BlogPost[]35}
DESC: Push the schema to your database
1$npm run db:push
Tip: The blog models are included by default. Just run the migration.
Get all published posts for your blog page
1// src/lib/blog/queries.ts2import { prisma } from "@/lib/db";34export async function getPublishedPosts(options?: {5 limit?: number;6 categorySlug?: string;7 featured?: boolean;8}) {9 const { limit, categorySlug, featured } = options || {};1011 return prisma.blogPost.findMany({12 where: {13 published: true,14 ...(categorySlug && { category: { slug: categorySlug } }),15 ...(featured !== undefined && { featured }),16 },17 include: {18 author: { select: { name: true, image: true } },19 category: { select: { name: true, slug: true } },20 },21 orderBy: { publishedAt: "desc" },22 take: limit,23 });24}2526// Get single post by slug27export async function getPostBySlug(slug: string) {28 const post = await prisma.blogPost.findUnique({29 where: { slug },30 include: {31 author: { select: { name: true, image: true } },32 category: { select: { name: true, slug: true } },33 },34 });3536 if (post) {37 // Increment view count38 await prisma.blogPost.update({39 where: { id: post.id },40 data: { viewCount: { increment: 1 } },41 });42 }4344 return post;45}
Display posts on your public blog
1// src/app/blog/page.tsx2import { getPublishedPosts } from "@/lib/blog/queries";3import Link from "next/link";45export default async function BlogPage() {6 const posts = await getPublishedPosts();78 return (9 <div className="container py-12">10 <h1 className="font-mono text-2xl font-semibold mb-8">11 [ BLOG ]12 </h1>1314 <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">15 {posts.map((post) => (16 <Link key={post.id} href={`/blog/${post.slug}`}>17 <article className="border border-border p-6 transition-colors">18 {post.category && (19 <span className="text-xs text-primary font-mono">20 {post.category.name}21 </span>22 )}23 <h2 className="font-mono text-xs font-semibold mt-2">24 {post.title}25 </h2>26 {post.excerpt && (27 <p className="text-muted-foreground text-sm mt-2">28 {post.excerpt}29 </p>30 )}31 <div className="flex items-center gap-4 mt-4 text-xs text-muted-foreground">32 <span>{post.readTime} min read</span>33 <span>{post.viewCount} views</span>34 </div>35 </article>36 </Link>37 ))}38 </div>39 </div>40 );41}
Admin API to create new blog posts
1// src/app/api/blog/posts/route.ts2import { NextResponse } from "next/server";3import { auth } from "@/lib/auth";4import { createPost } from "@/lib/blog/queries";5import { generateSlug, calculateReadTime } from "@/lib/blog/utils";67export async function POST(req: Request) {8 const session = await auth();910 // Only admins can create posts11 if (!session?.user || session.user.role !== "ADMIN") {12 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });13 }1415 const body = await req.json();16 const { title, content, excerpt, categoryId, featured, published } = body;1718 try {19 const post = await createPost({20 title,21 slug: generateSlug(title),22 content,23 excerpt: excerpt || content.slice(0, 160) + "...",24 categoryId,25 featured: featured || false,26 published: published || false,27 authorId: session.user.id,28 readTime: calculateReadTime(content),29 publishedAt: published ? new Date() : null,30 });3132 return NextResponse.json({ post });33 } catch (error) {34 console.error("Create post error:", error);35 return NextResponse.json(36 { error: "Failed to create post" },37 { status: 500 }38 );39 }40}
Helper functions for blog management
1// src/lib/blog/utils.ts23// Generate URL-friendly slug from title4export function generateSlug(title: string): string {5 return title6 .toLowerCase()7 .replace(/[^a-z0-9]+/g, "-")8 .replace(/^-+|-+$/g, "");9}1011// Calculate read time (average 200 words per minute)12export function calculateReadTime(content: string): number {13 const words = content.trim().split(/\s+/).length;14 return Math.ceil(words / 200);15}1617// Format date for display18export function formatDate(date: Date | string): string {19 return new Date(date).toLocaleDateString("en-US", {20 year: "numeric",21 month: "long",22 day: "numeric",23 });24}
React in Your Content
Callout - Info, warning, error, successTerminal - Command displayCodeBlock - Syntax highlightingSteps - Step-by-step guidesCardGrid - Feature gridsComparisonTable - Feature tablesYouTube - Video embedsKbd - Keyboard shortcuts<Callout type="warning" title="Important">
This is a warning callout in your blog post.
</Callout>
<Terminal command="npm install" output="added 42 packages" />
<Steps>
<Step number={1} title="Install">Run the install command</Step>
<Step number={2} title="Configure">Update your settings</Step>
</Steps>Full Control
Each blog post can have custom SEO metadata:
seoTitleCustom page title (defaults to post title)seoDescriptionMeta description (defaults to excerpt)featuredImageOpenGraph image URLslugURL path (/blog/your-slug)