Blog System
Complete guide to the powerful blog system with rich text editing, AI assistance, and SEO features
Blog System
The starter kit includes a comprehensive blog system with TipTap editor, AI writing assistant, post translation, categories, tags, and SEO optimization.
Features
- ✅ Rich Text Editor (TipTap) with formatting tools
- ✅ AI Writing Assistant with multiple models
- ✅ Post Translation using AI
- ✅ Categories & Tags for organization
- ✅ SEO Optimization with meta tags
- ✅ Featured Images with optimization
- ✅ Drafts & Publishing workflow
- ✅ View & Like tracking
- ✅ Author Attribution
- ✅ Admin Management interface
Architecture
graph LR
A[Blog Editor] --> B[TipTap]
A --> C[AI Assistant]
A --> D[Image Upload]
B --> E[Save Post]
C --> E
D --> E
E --> F[Database]
F --> G[Public Blog]
F --> H[Admin Dashboard]Creating a Blog Post
Admin Interface
Navigate to /dashboard/admin/blog and click "New Post".
Post Editor
The post editor includes:
- Title - Post title and URL slug
- Featured Image - Upload or select image
- Categories - Select or create categories
- Tags - Add relevant tags
- Content Editor - Rich text with formatting
- AI Assistant - Generate or improve content
- SEO Settings - Meta title and description
- Publishing - Draft or publish
Using the TipTap Editor
The rich text editor supports:
- Formatting: Bold, italic, underline, strikethrough
- Headings: H1, H2, H3, H4, H5, H6
- Lists: Bullet, numbered, task lists
- Links: Insert and edit hyperlinks
- Images: Embed images inline
- Code: Code blocks with syntax highlighting
- Quotes: Blockquotes
- Alignment: Left, center, right, justify
- Colors: Text and highlight colors
- YouTube: Embed YouTube videos
- Tables: Insert and edit tables (if enabled)
Toolbar
// Default toolbar options
[
"undo",
"redo",
"bold",
"italic",
"underline",
"strike",
"heading",
"bulletList",
"orderedList",
"blockquote",
"codeBlock",
"link",
"image",
"youtube",
"textAlign",
"color",
"highlight",
];AI Writing Assistant
The AI assistant can help with:
-
Generate Content
- Create blog post from topic
- Generate specific sections
- Expand on ideas
-
Refine Existing Content
- Improve clarity and flow
- Fix grammar and spelling
- Adjust tone (professional, casual, technical)
- Shorten or expand content
-
Translate Posts
- Translate to multiple languages
- Preserve HTML formatting
- Chunk large posts for better quality
Using the AI Assistant
- Click "AI Assistant" button in editor
- Choose mode:
- Generate: Create new content
- Refine: Improve existing content
- Translate: Convert to another language
- Select AI model (free or paid options)
- Provide instructions
- Review and insert generated content
AI Models Available
Free Models (no cost):
meta-llama/llama-3.2-3b-instruct:freegoogle/gemini-flash-1.5
Paid Models (via OpenRouter):
anthropic/claude-3.5-sonnet- Best qualityopenai/gpt-4-turbo- Excellent resultsgoogle/gemini-pro-1.5- Good balance
Note: Cost Savings: The starter kit uses FREE models by default. No API costs!
Translation Feature
Translate blog posts to other languages:
// Supported languages
const languages = [
"Spanish",
"French",
"German",
"Italian",
"Portuguese",
"Chinese",
"Japanese",
"Korean",
"Arabic",
"Hindi",
"Russian",
];
// Translation preserves:
// - HTML formatting
// - Links and images
// - Code blocks
// - Heading structureHow it works:
- AI chunks long posts into manageable sections
- Translates each chunk while preserving HTML
- Reassembles chunks into full translated post
- Creates new post with
translationGroupIdlinking
Categories
Organize posts into categories:
// Example categories
{
name: "Tutorials",
slug: "tutorials",
description: "Step-by-step guides"
}Managing Categories:
- Navigate to
/dashboard/admin/blog/categories - Create, edit, or delete categories
- Categories shown on blog listing pages
Usage:
- Posts can have multiple categories
- Filter posts by category on frontend
- Display category badges on post cards
Tags
Add tags for granular organization:
// Example tags
{ name: "Next.js", slug: "nextjs" }
{ name: "TypeScript", slug: "typescript" }
{ name: "Tutorial", slug: "tutorial" }Managing Tags:
- Navigate to
/dashboard/admin/blog/tags - Create tags on-the-fly or manage existing
- Tags auto-create when used in posts
Usage:
- Posts can have unlimited tags
- Filter posts by tag
- Tag cloud displays on sidebar
Featured Images
Upload featured images for posts:
- Click "Upload Featured Image" in editor
- Select image (drag & drop or browse)
- Image auto-optimizes and uploads
- Preview shows in editor
- Displayed on post cards and post page
Requirements:
- Max size: 4MB (configurable)
- Formats: JPG, PNG, WebP, GIF
- Auto-optimization with Sharp
Post Metadata & SEO
SEO Settings
Configure SEO for better search rankings:
{
title: "My Blog Post", // Post title
seoTitle: "My Blog Post - Complete Guide", // Override for <title>
excerpt: "Short summary...", // Meta description fallback
seoDescription: "Detailed SEO description", // Meta description
slug: "my-blog-post", // URL slug
featuredImage: "/uploads/image.jpg" // OG image
}Meta Tags Generated:
<title>My Blog Post - Complete Guide</title>
<meta name="description" content="Detailed SEO description" />
<meta property="og:title" content="My Blog Post - Complete Guide" />
<meta property="og:description" content="Detailed SEO description" />
<meta property="og:image" content="/uploads/image.jpg" />
<meta property="og:type" content="article" />
<meta name="twitter:card" content="summary_large_image" />Automatic SEO
Posts automatically get:
- Clean, SEO-friendly URLs (slug)
- Proper heading hierarchy (H1 for title)
- Alt text on images (if provided)
- Schema.org Article markup
- Canonical URLs
- Sitemap inclusion (if configured)
Publishing Workflow
Draft Mode
- Create post
- Save as draft (
published: false) - Preview at
/blog/preview/{id}(admin only) - Edit and refine
- Publish when ready
Publishing
- Set
published: true - Set
publishedAt: new Date() - Post appears on public blog
- Listed in blog index
- Visible to all users
Unpublishing
- Set
published: false - Post hidden from public
- Still accessible to admins
- Can re-publish later
Blog Frontend
Listing Page
/blog shows all published posts:
// Features:
- Grid or list view
- Filter by category
- Filter by tag
- Search posts
- Pagination
- Sort by: newest, oldest, most viewed, most likedPost Page
/blog/{slug} displays individual post:
// Includes:
- Full content (sanitized HTML)
- Author info with avatar
- Published date
- Categories and tags
- Featured image
- Like button
- View counter
- Related posts
- Social sharing buttonsSEO-Friendly URLs
/blog/getting-started-with-nextjs
/blog/category/tutorials
/blog/tag/typescript
/blog/author/john-doeView & Like Tracking
View Counter
Automatically tracks post views:
// Increments on page view
// Stored in database
// Displayed on post card and page
viewCount: integer (default: 0)Like System
Anonymous likes using browser fingerprinting:
// User clicks like button
// Generate unique fingerprint
// Check if already liked
// Increment like count
// Store fingerprint in blogPostLikes
// Prevents duplicate likes from same userImplementation:
import { getLikeStatus, toggleLike } from "@/lib/blog-likes";
// Check if user liked post
const liked = await getLikeStatus(postId, fingerprint);
// Toggle like
const result = await toggleLike(postId, fingerprint);
// Returns: { liked: boolean, likeCount: number }Code Examples
Fetching Blog Posts
// Server component
import { db } from "@/db";
import { blogPosts } from "@/db/schema";
import { eq, desc } from "drizzle-orm";
export async function getAllPosts() {
const posts = await db.query.blogPosts.findMany({
where: eq(blogPosts.published, true),
orderBy: [desc(blogPosts.publishedAt)],
with: {
author: {
columns: {
id: true,
name: true,
image: true,
},
},
postCategories: {
with: {
category: true,
},
},
postTags: {
with: {
tag: true,
},
},
},
});
return posts;
}Creating a Post
// API route: POST /api/blog
import { db } from "@/db";
import { blogPosts } from "@/db/schema";
export async function POST(request: Request) {
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session?.user || session.user.role !== "admin") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const data = await request.json();
const [post] = await db
.insert(blogPosts)
.values({
title: data.title,
slug: data.slug,
content: data.content,
excerpt: data.excerpt,
featuredImage: data.featuredImage,
published: data.published || false,
authorId: session.user.id,
publishedAt: data.published ? new Date() : null,
})
.returning();
// Add categories
if (data.categoryIds?.length) {
await db.insert(blogPostCategories).values(
data.categoryIds.map((categoryId: number) => ({
postId: post.id,
categoryId,
}))
);
}
// Add tags
if (data.tagIds?.length) {
await db.insert(blogPostTags).values(
data.tagIds.map((tagId: number) => ({
postId: post.id,
tagId,
}))
);
}
return NextResponse.json(post);
}Blog Post CardComponent
// components/blog/blog-card.tsx
"use client";
import Image from "next/image";
import Link from "next/link";
import { formatDate } from "@/lib/utils";
export function BlogCard({ post }) {
return (
<article className="blog-card">
{post.featuredImage && (
<Link href={`/blog/${post.slug}`}>
<Image
src={post.featuredImage}
alt={post.title}
width={400}
height={200}
className="rounded-lg"
/>
</Link>
)}
<div className="flex gap-2 mt-4">
{post.postCategories?.map((pc) => (
<span key={pc.category.id} className="badge">
{pc.category.name}
</span>
))}
</div>
<h2 className="text-2xl font-bold mt-2">
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</h2>
<p className="text-muted-foreground mt-2">{post.excerpt}</p>
<div className="flex items-center justify-between mt-4">
<div className="flex items-center gap-2">
<Image
src={post.author.image || "/default-avatar.png"}
alt={post.author.name}
width={32}
height={32}
className="rounded-full"
/>
<span>{post.author.name}</span>
</div>
<div className="flex gap-4 text-sm text-muted-foreground">
<span>{post.viewCount} views</span>
<span>{post.likeCount} likes</span>
<span>{formatDate(post.publishedAt)}</span>
</div>
</div>
</article>
);
}Security
Content Sanitization
All blog content is sanitized before rendering:
import DOMPurify from "isomorphic-dompurify";
// Sanitize HTML content
const cleanHTML = DOMPurify.sanitize(post.content, {
ALLOWED_TAGS: [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"p",
"br",
"strong",
"em",
"u",
"s",
"a",
"img",
"ul",
"ol",
"li",
"blockquote",
"pre",
"code",
"table",
"thead",
"tbody",
"tr",
"th",
"td",
],
ALLOWED_ATTR: ["href", "src", "alt", "title", "class", "target", "rel"],
});Protection Against:
- XSS attacks
- Script injection
- Malicious HTML
- Unsafe attributes
Admin-Only Management
All blog management routes are protected:
// app/dashboard/admin/blog/page.tsx
import { requireAuth } from "@/lib/requireAuth";
export default async function BlogManagementPage() {
await requireAuth("admin");
// Only admins can access
}Customization
Custom TipTap Extensions
Add custom TipTap extensions:
// components/blog/tiptap-editor.tsx
import { useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Image from "@tiptap/extension-image";
import Link from "@tiptap/extension-link";
import CustomExtension from "./custom-extension";
const editor = useEditor({
extensions: [
StarterKit,
Image,
Link,
CustomExtension, // Your custom extension
],
content: initialContent,
});Custom Post Types
Extend the blog system for custom post types:
- Add
typefield toblogPoststable - Update schema and push
- Filter by type in queries
- Create dedicated admin pages
Custom Taxonomies
Add custom taxonomies beyond categories and tags:
- Create new junction tables
- Update schema
- Add admin UI for management
- Update post editor
Best Practices
- Always use SEO fields for better discoverability
- Add featured images to improve engagement
- Use categories sparingly (3-5 main categories)
- Tag liberally for better filtering
- Preview before publishing to check formatting
- Optimize images before uploading (max 4MB)
- Write clear excerpts for post cards and SEO
- Use headings properly (H2, H3, not H1)
Troubleshooting
Editor not loading
- Check console for errors
- Verify TipTap packages installed
- Clear Next.js cache (
.nextfolder)
AI Assistant not working
- Verify
OPENROUTER_API_KEYis set - Check API key is valid
- Ensure free models are available
Images not uploading
- Check
UPLOADTHING_TOKENis configured - Verify file size is under limit
- Check user authentication
Post not appearing
- Verify
published: true - Check
publishedAtis set - Ensure correct permissions