Better Stack logoBetter Stack
BlogGitHubDocs
November 28, 2025NextJSWeb Development

The Easiest Way to Add a Blog to Next.js in 2026

Skip the CMS setup, custom APIs, and database headaches. Learn how to add a production-ready blog to your Next.js app in under 15 minutes using Better Stack's modular plugin system.

The Easiest Way to Add a Blog to Next.js in 2026

Every Next.js project eventually needs a blog. Marketing wants content. SEO demands fresh pages. Your users expect updates. And yet, adding a blog to an existing Next.js app remains surprisingly painful.

The traditional options? Wire up a headless CMS with webhooks, API routes, and ISR caching strategies. Or build a custom solution from scratch — database schemas, CRUD endpoints, markdown parsing, SEO meta tags, draft previews, and an editor that doesn't feel like it's from 2015.

Neither path is fast. Neither is fun.

There's a better way.

The Modular Approach to Blog Features#

What if adding a blog was as simple as installing a package and wiring up a few lines of configuration? Not a black-box SaaS integration. Not a starter template you'll spend days customizing. A real, production-grade blog system that drops into your existing architecture — typed, extensible, and yours to own.

This is what modular full-stack features look like. And in 2026, this isn't theoretical. It's here.

Better Stack provides a blog plugin that bundles everything: database schemas, API routes, React components, SEO metadata, and server-side rendering — all in a single installable unit that respects your Next.js patterns.

Let's build it.

Prerequisites#

Before we start, make sure your Next.js project has:

  • shadcn/ui installed with CSS variables enabled

  • Sonner <Toaster /> configured for notifications

  • TailwindCSS v4 set up

  • @tanstack/react-query installed

If you're starting fresh, the shadcn CLI handles most of this:

BASH
  1. 1
  2. 2
npx shadcn@latest init
npx shadcn@latest add sonner

Step 1: Install Better Stack#

Add the core package and a database adapter. We'll use Prisma in this guide, but Better Stack supports multiple adapters:

  • @btst/adapter-prisma — For Prisma ORM (PostgreSQL, MySQL, SQLite, CockroachDB)

  • @btst/adapter-drizzle — For Drizzle ORM

  • @btst/adapter-kysely — Any Kysely suported dialect

  • @btst/adapter-mongodb — For MongoDB

  • @btst/adapter-memory — For development and testing

BASH
  1. 1
npm install @btst/stack @tanstack/react-query @btst/adapter-prisma

Step 2: Create the Backend Instance#

Create lib/better-stack.ts to configure your API handler and register the blog plugin:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
import { betterStack } from "@btst/stack"
import { blogBackendPlugin } from "@btst/stack/plugins/blog/api"
import { createPrismaAdapter } from "@btst/adapter-prisma"
import { PrismaClient } from "@prisma/client"

const prisma = new PrismaClient()

const { handler, dbSchema } = betterStack({
  basePath: "/api/data",
  plugins: {
    blog: blogBackendPlugin()
  },
  adapter: (db) => createPrismaAdapter(prisma, db, { 
    provider: "postgresql"
  })
})

export { handler, dbSchema }

This single configuration gives you:

  • Full CRUD API for posts and tags

  • Database schema definitions

  • Type-safe data layer abstraction

Step 3: Create the API Route#

Next.js App Router needs a catch-all route to handle Better Stack requests:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
import { handler } from "@/lib/better-stack"

export const GET = handler
export const POST = handler
export const PUT = handler
export const DELETE = handler

That's your entire API layer. No manual endpoint definitions. No request parsing boilerplate.

Step 4: Generate the Database Schema#

Run the CLI to generate your Prisma schema:

BASH
  1. 1
npx @btst/cli generate --config=lib/better-stack.ts --orm=prisma --output=prisma/schema.prisma

Then run your migration:

BASH
  1. 1
npx prisma migrate dev --name add-blog

Step 5: Set Up the Client#

Create lib/better-stack-client.tsx to configure client-side routing and data fetching:

TSX
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
import { createStackClient } from "@btst/stack/client"
import { blogClientPlugin } from "@btst/stack/plugins/blog/client"
import { QueryClient } from "@tanstack/react-query"

const getBaseURL = () => 
  process.env.BASE_URL || "http://localhost:3000"

export const getStackClient = (queryClient: QueryClient) => {
  const baseURL = getBaseURL()
  return createStackClient({
    plugins: {
      blog: blogClientPlugin({
        apiBaseURL: baseURL,
        apiBasePath: "/api/data",
        siteBaseURL: baseURL,
        siteBasePath: "/pages",
        queryClient: queryClient,
        seo: {
          siteName: "My Blog",
          author: "Your Name",
          twitterHandle: "@yourhandle",
          locale: "en_US",
          defaultImage: `${baseURL}/og-image.png`,
        },
      })
    }
  })
}

Step 6: Configure React Query#

Create a query client utility for proper SSR hydration:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
import { QueryClient, isServer } from "@tanstack/react-query"

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: isServer ? 60 * 1000 : 0,
        refetchOnMount: false,
        refetchOnWindowFocus: false,
        retry: false
      },
      dehydrate: {
        shouldDehydrateQuery: () => true
      }
    }
  })
}

let browserQueryClient: QueryClient | undefined = undefined

export function getOrCreateQueryClient() {
  if (isServer) {
    return makeQueryClient()
  }
  if (!browserQueryClient) browserQueryClient = makeQueryClient()
  return browserQueryClient
}

Add the provider to your root layout:

TSX
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
import { QueryClientProvider } from "@tanstack/react-query"
import { getOrCreateQueryClient } from "@/lib/query-client"

export default function RootLayout({ children }) {
  const queryClient = getOrCreateQueryClient()
  
  return (
    <html>
      <body>
        <QueryClientProvider client={queryClient}>
          {children}
        </QueryClientProvider>
      </body>
    </html>
  )
}

Step 7: Import Plugin Styles#

Add the blog CSS to your global stylesheet:

CSS
  1. 1
@import "@btst/stack/plugins/blog/css";

Step 8: Create the Layout Provider#

Set up framework-specific overrides for the blog pages:

TSX
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
"use client"

import { BetterStackProvider } from "@btst/stack/context"
import type { BlogPluginOverrides } from "@btst/stack/plugins/blog/client"
import Link from "next/link"
import Image from "next/image"
import { useRouter } from "next/navigation"

const getBaseURL = () => 
  typeof window !== 'undefined' 
    ? (process.env.NEXT_PUBLIC_BASE_URL || window.location.origin)
    : (process.env.BASE_URL || "http://localhost:3000")

type PluginOverrides = {
  blog: BlogPluginOverrides
}

export default function Layout({ children }: { children: React.ReactNode }) {
  const router = useRouter()
  const baseURL = getBaseURL()
  
  return (
    <BetterStackProvider<PluginOverrides>
      basePath="/pages"
      overrides={{
        blog: {
          apiBaseURL: baseURL,
          apiBasePath: "/api/data",
          navigate: (path) => router.push(path),
          refresh: () => router.refresh(),
          uploadImage: async (file) => {
            // Implement your image upload logic
            // e.g., upload to Vercel Blob, S3, Cloudinary
            return "https://example.com/uploads/image.jpg"
          },
          Link: (props) => <Link {...props} />,
          Image: (props) => <Image {...props} />,
        }
      }}
    >
      {children}
    </BetterStackProvider>
  )
}

Step 9: Create the Page Handler#

This catch-all page handles routing, SSR, and metadata generation:

TSX
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
import { notFound } from "next/navigation"
import { getOrCreateQueryClient } from "@/lib/query-client"
import { getStackClient } from "@/lib/better-stack-client"
import { metaElementsToObject, normalizePath } from "@btst/stack/client"
import { Metadata } from "next"

export default async function Page({ params }: { params: Promise<{ all: string[] }> }) {
  const pathParams = await params
  const path = normalizePath(pathParams?.all)
  
  const queryClient = getOrCreateQueryClient()
  const stackClient = getStackClient(queryClient)
  const route = stackClient.router.getRoute(path)
  
  if (route?.loader) await route.loader()
  
  const dehydratedState = dehydrate(queryClient)
  
  return (
    <HydrationBoundary state={dehydratedState}>
      {route && route.PageComponent ? <route.PageComponent /> : notFound()}
    </HydrationBoundary>
  )
}

export async function generateMetadata({ params }: { params: Promise<{ all: string[] }> }): Promise<Metadata> {
  const pathParams = await params
  const path = normalizePath(pathParams?.all)
  
  const queryClient = getOrCreateQueryClient()
  const stackClient = getStackClient(queryClient)
  const route = stackClient.router.getRoute(path)
  
  if (!route) return notFound()
  if (route?.loader) await route.loader()
  
  return route.meta ? metaElementsToObject(route.meta()) : { title: "Blog" }
}

That's It. You Have a Blog.#

Navigate to /pages/blog and you'll see your blog homepage. Here's what you get out of the box:

Pages:

  • /pages/blog — Blog homepage with published posts

  • /pages/blog/drafts — Draft management

  • /pages/blog/new — Rich markdown editor for new posts

  • /pages/blog/:slug — Individual post pages with SEO

  • /pages/blog/:slug/edit — Edit existing posts

  • /pages/blog/tag/:tagSlug — Tag-filtered views

Features:

  • Server-side rendering with data prefetching

  • Automatic SEO meta tags and Open Graph

  • Markdown editor with live preview

  • Draft and publish workflow

  • Tag management

  • Responsive, styled components

API Endpoints:

  • GET /api/data/posts — List posts with filtering

  • POST /api/data/posts — Create posts

  • PUT /api/data/posts/:id — Update posts

  • DELETE /api/data/posts/:id — Delete posts

  • GET /api/data/tags — List tags

Adding Sitemap Support#

Better Stack generates sitemaps from all registered plugins. Next.js has native sitemap support:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
import type { MetadataRoute } from "next"
import { QueryClient } from "@tanstack/react-query"
import { getStackClient } from "@/lib/better-stack-client"

export const dynamic = "force-dynamic"

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const queryClient = new QueryClient()
  const stackClient = getStackClient(queryClient)
  return stackClient.generateSitemap()
}

This automatically includes all blog posts with their URLs, last modified dates, and change frequencies — no manual configuration needed.

Adding Authorization#

The blog plugin accepts lifecycle hooks for authorization:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
import { blogBackendPlugin, type BlogBackendHooks } from "@btst/stack/plugins/blog/api"

const blogHooks: BlogBackendHooks = {
  onBeforeCreatePost(data, context) {
    return isAdmin(context.headers as Headers)
  },
  onBeforeUpdatePost(postId, data, context) {
    return isAdmin(context.headers as Headers)
  },
  onBeforeDeletePost(postId, context) {
    return isAdmin(context.headers as Headers)
  },
}

const { handler, dbSchema } = betterStack({
  plugins: {
    blog: blogBackendPlugin(blogHooks)
  },
  // ...
})

Why This Matters#

You just added a production-ready blog to Next.js without:

  • Setting up a headless CMS

  • Writing CRUD endpoints

  • Building a markdown editor

  • Configuring SEO meta tags

  • Creating database schemas manually

  • Wiring up client-state management

This is the power of modular full-stack features. Not scaffolding. Not boilerplate generation. Real, typed, ownable code that integrates with your architecture.

The future of web development isn't about AI writing your code. It's about assembling better building blocks. Better Stack is one piece of that future — a plugin system that treats features like components, not projects.

Build with better blocks.


Resources:

  • Full Installation Guide — Complete setup instructions for all frameworks and adapters

  • Better Stack Documentation

  • Blog Plugin Reference

  • Database Adapters — Detailed configuration for Prisma, Drizzle, Kysely, and MongoDB

  • Next.js Example Repository

In This Post

The Modular Approach to Blog FeaturesPrerequisitesStep 1: Install Better StackStep 2: Create the Backend InstanceStep 3: Create the API RouteStep 4: Generate the Database SchemaStep 5: Set Up the ClientStep 6: Configure React QueryStep 7: Import Plugin StylesStep 8: Create the Layout ProviderStep 9: Create the Page HandlerThat's It. You Have a Blog.Adding Sitemap SupportAdding AuthorizationWhy This Matters

Powered by Better-Stack