Next Starter Kit
Features

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:

  1. Title - Post title and URL slug
  2. Featured Image - Upload or select image
  3. Categories - Select or create categories
  4. Tags - Add relevant tags
  5. Content Editor - Rich text with formatting
  6. AI Assistant - Generate or improve content
  7. SEO Settings - Meta title and description
  8. 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:

  1. Generate Content

    • Create blog post from topic
    • Generate specific sections
    • Expand on ideas
  2. Refine Existing Content

    • Improve clarity and flow
    • Fix grammar and spelling
    • Adjust tone (professional, casual, technical)
    • Shorten or expand content
  3. Translate Posts

    • Translate to multiple languages
    • Preserve HTML formatting
    • Chunk large posts for better quality

Using the AI Assistant

  1. Click "AI Assistant" button in editor
  2. Choose mode:
    • Generate: Create new content
    • Refine: Improve existing content
    • Translate: Convert to another language
  3. Select AI model (free or paid options)
  4. Provide instructions
  5. Review and insert generated content

AI Models Available

Free Models (no cost):

  • meta-llama/llama-3.2-3b-instruct:free
  • google/gemini-flash-1.5

Paid Models (via OpenRouter):

  • anthropic/claude-3.5-sonnet - Best quality
  • openai/gpt-4-turbo - Excellent results
  • google/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 structure

How it works:

  1. AI chunks long posts into manageable sections
  2. Translates each chunk while preserving HTML
  3. Reassembles chunks into full translated post
  4. Creates new post with translationGroupId linking

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

Upload featured images for posts:

  1. Click "Upload Featured Image" in editor
  2. Select image (drag & drop or browse)
  3. Image auto-optimizes and uploads
  4. Preview shows in editor
  5. 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

  1. Create post
  2. Save as draft (published: false)
  3. Preview at /blog/preview/{id} (admin only)
  4. Edit and refine
  5. Publish when ready

Publishing

  1. Set published: true
  2. Set publishedAt: new Date()
  3. Post appears on public blog
  4. Listed in blog index
  5. Visible to all users

Unpublishing

  1. Set published: false
  2. Post hidden from public
  3. Still accessible to admins
  4. 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 liked

Post 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 buttons

SEO-Friendly URLs

/blog/getting-started-with-nextjs
/blog/category/tutorials
/blog/tag/typescript
/blog/author/john-doe

View & 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 user

Implementation:

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:

  1. Add type field to blogPosts table
  2. Update schema and push
  3. Filter by type in queries
  4. Create dedicated admin pages

Custom Taxonomies

Add custom taxonomies beyond categories and tags:

  1. Create new junction tables
  2. Update schema
  3. Add admin UI for management
  4. Update post editor

Best Practices

  1. Always use SEO fields for better discoverability
  2. Add featured images to improve engagement
  3. Use categories sparingly (3-5 main categories)
  4. Tag liberally for better filtering
  5. Preview before publishing to check formatting
  6. Optimize images before uploading (max 4MB)
  7. Write clear excerpts for post cards and SEO
  8. Use headings properly (H2, H3, not H1)

Troubleshooting

Editor not loading

  • Check console for errors
  • Verify TipTap packages installed
  • Clear Next.js cache (.next folder)

AI Assistant not working

  • Verify OPENROUTER_API_KEY is set
  • Check API key is valid
  • Ensure free models are available

Images not uploading

  • Check UPLOADTHING_TOKEN is configured
  • Verify file size is under limit
  • Check user authentication

Post not appearing

  • Verify published: true
  • Check publishedAt is set
  • Ensure correct permissions

Next Steps

On this page