BTST logoBTST
BlogGitHubDocs
BTST logoBTST

Full-stack features as npm packages. Install a plugin, get routes, APIs, schemas, and UI.

Plugins

  • AI Chat
  • Auth UI
  • Blog
  • CMS
  • Form Builder
  • Kanban
  • OpenAPI
  • Route Docs
  • UI Builder

Resources

  • Documentation
  • All Plugins
  • Blog
  • GitHub↗

Get Started

Ready to ship faster? Install the package and add your first plugin in under 5 minutes.

npm i @btst/stack
Read the docs

© 2026 BTST. Open source under MIT License.

Built by @olliethedev
January 14, 2026NextJSWeb Development

Quick Ways to Add a Blog to Your Next.js Full-Stack App in 2026

A technical guide to the fastest open-source ways to add a blog to a modern Next.js app in 2026. Featuring MDX, Content Collections, Nextra, TinaCMS, Decap CMS and Better Stack’s blog plugin.

Quick Ways to Add a Blog to Your Next.js Full-Stack App in 2026

In 2026, adding a blog to a modern Next.js app is no longer synonymous with adopting a full CMS. If you’re building a product and you want content (marketing, changelog, docs-ish posts) without dragging in an entire platform, the ecosystem has matured into a few clear “dev-first” lanes:

  • Plugin-based full-stack blog (routes + DB + editor + SEO + RSS included)

  • File-based MD/MDX (simple, fast, in-repo)

  • Type-safe content pipelines (schema validation + generated types)

  • Turnkey blog frameworks (instant theme + routing)

  • Git-based editors (visual editing, content stays in Git)

This post focuses on free + open-source options and skips traditional CMS / hosted headless platforms (WordPress, Contentful, Strapi, etc.). You’ll get setup snippets, tradeoffs, and quick “pick this if…” guidance.


The 2026 Blog Options Landscape#

Here are the most common approaches devs actually ship:

  1. Better Stack Blog Plugin (top contender)
    A plugin that drops a full blog feature into your app: DB schema + API endpoints + SSR pages + editor UI + SEO + sitemap/RSS, with lifecycle hooks and adapters for Prisma/Drizzle/Kysely/MongoDB.
  2. Native MDX pages
    Put .mdx directly in your app and render it as routes. Great for dev-owned content in Git.
  3. next-mdx-remote
    More flexible MDX loading (files outside routes, remote sources, etc.), with more wiring.
  4. Content Collections (type-safe content as data)
    Parse Markdown/MDX at build-time into typed objects; best when you care about schemas and long-term maintainability.
  5. Nextra (turnkey)
    “Blog in minutes” if you’re okay with a theme + conventions.
  6. TinaCMS / Decap CMS (Git-based editors)
    Add a UI for editing file-based content while keeping Markdown in your repo.

Let’s go through them in the order most teams should evaluate them.


1) MDX Files as Pages (Built-in Next.js Support)#

If your blog is mostly dev-authored and you’re happy keeping posts in Git, the simplest solution is still: MDX in your repo.

Setup#

TYPESCRIPT
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
// next.config.mjs
import withMDX from "@next/mdx"({ extension: /\.mdx?$/ })

export default withMDX({
  pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
})

Example post:

TYPESCRIPT
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
export const metadata = { title: "Hello World", description: "First post" }

# Hello World

This is my **first blog post**.

<NewsletterSignup />

Pros

  • Minimal moving parts

  • Great SEO (SSG/SSR output HTML)

  • Easy MDX components

Cons

  • You build indexing/tags/feeds yourself

  • Frontmatter is untyped unless you add validation

  • Builds can slow down with huge content libraries

Pick this if: “It’s mostly a dev blog, content lives in Git, and I want maximum control.”


2) Dynamic MDX with next-mdx-remote (Flexible Fetching)#

If you want MDX but don’t want .mdx files to be actual route modules, next-mdx-remote lets you load from wherever and render.

Example (Pages Router SSG)#

TYPESCRIPT
  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
// pages/blog/[slug].tsx
import { MDXRemote, type MDXRemoteSerializeResult } from "next-mdx-remote"
import { serialize } from "next-mdx-remote/serialize"
import fs from "fs"
import path from "path"

export async function getStaticPaths() {
  const dir = path.join(process.cwd(), "posts")
  const files = fs.readdirSync(dir).filter((f) => f.endsWith(".mdx"))
  return {
    paths: files.map((f) => ({ params: { slug: f.replace(/\.mdx$/, "") } })),
    fallback: false,
  }
}

export async function getStaticProps({ params }: { params: { slug: string } }) {
  const filePath = path.join(process.cwd(), "posts", `${params.slug}.mdx`)
  const source = fs.readFileSync(filePath, "utf8")
  const mdxSource = await serialize(source, { parseFrontmatter: true })
  return { props: { source: mdxSource } }
}

export default function PostPage({ source }: { source: MDXRemoteSerializeResult }) {
  return <MDXRemote {...source} />
}

Pros

  • Content can live outside your route tree

  • Can support remote sources

  • Still MDX

Cons

  • More wiring (slugs, metadata, indexing)

  • SSR parsing can impact TTFB unless cached

Pick this if: “I need flexible sourcing (or a massive repo) but I still want MDX.”


3) Content Collections (Type-Safe Markdown as Data)#

If you want schema validation + generated types for posts, a content pipeline is usually the best long-term “file-based” solution.

Conceptual example#

TYPESCRIPT
  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
// content-collections.ts
import { defineCollection, defineConfig } from "@content-collections/core"

const BlogPost = defineCollection({
  name: "BlogPost",
  directory: "content/blog",
  include: "*.mdx",
  schema: (z) => ({
    title: z.string(),
    date: z.string().datetime(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
})

export default defineConfig({
  collections: [BlogPost],
})

Pros

  • Best TypeScript ergonomics

  • Enforces frontmatter rules

  • Easy sorting/filtering without filesystem scanning in every route

Cons

  • Additional build step + config

  • Still file-based (publishing implies deploy/rebuild unless you add infra)

Pick this if: “My blog will grow and I care about strict content shape and DX.”


4) Nextra (Blog Theme for Instant Setup)#

Nextra is still one of the fastest “I want a blog right now” options.

Setup#

SHELL
  1. 1
  2. 2
npm install nextra nextra-theme-blog

TYPESCRIPT
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
// next.config.mjs
import nextra from "nextra"

const withNextra = nextra({
  theme: "nextra-theme-blog",
  themeConfig: "./theme.config.ts",
})

export default withNextra({})

Pros

  • Super fast setup

  • Nice defaults (routing/layout/docs-ish ergonomics)

Cons

  • Conventions can be constraining for app-style blogs

  • Deep customization can get tricky

Pick this if: “I’m okay adopting a theme-driven blog stack to ship immediately.”


5) Git-Based Editors: TinaCMS and Decap CMS#

If non-devs need to publish but you still want content in Git:

Decap CMS (classic)#

TYPESCRIPT
  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
# public/admin/config.yml
backend:
  name: git-gateway
  branch: main

media_folder: "public/uploads"
public_folder: "/uploads"

collections:
  - name: "posts"
    label: "Posts"
    folder: "content/posts"
    create: true
    slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
    fields:
      - { name: "title", label: "Title", widget: "string" }
      - { name: "date", label: "Date", widget: "datetime" }
      - { name: "tags", label: "Tags", widget: "list", default: [] }
      - { name: "body", label: "Body", widget: "markdown" }

TinaCMS (more React-native)#

Better for component-based editing and richer workflows, but integration/auth can be more involved.

Pick these if: “Publishing needs a UI, but we’re committed to Git-stored content.”



6) Better Stack Blog Plugin (Plugin-Based Full-Stack Blog)#

If you want the fastest path to a real blog feature inside a full-stack app—without bolting together routing + content storage + editor UI + SEO + RSS yourself—Better Stack’s Blog plugin is built for that exact use case.

Why it’s a top contender#

Better Stack treats “Blog” as a full-stack plugin, not a folder of markdown:

  • Backend plugin adds DB schema + typed API endpoints + hooks (auth/logging/policies)

  • Client plugin adds routes + SSR loaders + SEO metadata + React Query hydration

  • UI included: editor, drafts, post pages, tags, etc.

  • Adapters: Prisma, Drizzle, Kysely, MongoDB (and memory for dev/tests)

If your blog needs drafts, tags, CRUD, multi-author workflows, protected admin routes, image uploads, or you want content in a DB (not just Git), this usually beats rolling your own.

Minimal setup (Next.js)#

1) Install core + adapter + blog plugin#

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

2) Create your backend instance#

TYPESCRIPT
  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
// lib/better-stack.ts
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({
      onBeforeCreatePost: (_data, ctx) => isBlogAdmin(ctx.headers as Headers),
      onBeforeUpdatePost: (_id, _data, ctx) => isBlogAdmin(ctx.headers as Headers),
      onBeforeDeletePost: (_id, ctx) => isBlogAdmin(ctx.headers as Headers),
      onBeforeListPosts: (filter, ctx) => {
        // Allow public listing of published posts; restrict drafts
        if (filter.published === false) return isBlogAdmin(ctx.headers as Headers)
        return true
      },
    }),
  },
  adapter: (db) =>
    createPrismaAdapter(prisma, db, {
      provider: "postgresql",
    }),
})

export { handler, dbSchema }

// Your auth helper
function isBlogAdmin(_headers: Headers) {
  return true // replace with real auth check
}

3) Mount the API route#

TYPESCRIPT
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
// app/api/data/[[...all]]/route.ts
import { handler } from "@/lib/better-stack"

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

4) Import plugin styles#

TYPESCRIPT
  1. 1
  2. 2
  3. 3
/* app/globals.css */
@import "@btst/stack/plugins/blog/css";

5) Register the client plugin#

TYPESCRIPT
  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
// lib/better-stack-client.tsx
import { createStackClient } from "@btst/stack/client"
import { blogClientPlugin } from "@btst/stack/plugins/blog/client"
import { QueryClient } from "@tanstack/react-query"

const getBaseURL = () =>
  process.env.NEXT_PUBLIC_BASE_URL || 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,
        seo: {
          siteName: "My Blog",
          author: "Your Name",
          twitterHandle: "@yourhandle",
          locale: "en_US",
          defaultImage: `${baseURL}/og-image.png`,
        },
      }),
    },
  })
}

6) Add the page handler (routes come from plugins)#

TYPESCRIPT
  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
// app/pages/[[...all]]/page.tsx
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 { normalizePath } from "@btst/stack/client"

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?.PageComponent ? <route.PageComponent /> : notFound()}
    </HydrationBoundary>
  )
}

What you get (out of the box)#

  • Pages:

    • /blog (home)

    • /blog/drafts

    • /blog/new

    • /blog/:slug

    • /blog/:slug/edit

    • /blog/tag/:tagSlug

  • API:

    • GET /posts, POST /posts, PUT /posts/:id, DELETE /posts/:id

    • GET /tags, GET /posts/next-previous

  • SEO helpers, SSR loaders, React Query hydration, hooks for auth/policies

  • Optional sitemap aggregation (if you enable sitemap generation)

When to pick Better Stack Blog#

Pick it if you want a real product-grade blog inside your app:

  • content in DB (not only Git)

  • drafts + CRUD + auth rules

  • editor UI (not “edit markdown in VS Code”)

  • consistent SSR + caching + SEO + sitemap/RSS patterns

  • minimal glue code



Comparison: Which Blog Approach Should You Choose?#

OptionStorageEditing UXSEO/SSRType Safety“Blog features” out of boxBest for
Better Stack Blog pluginDB (via adapter)✅ Editor + drafts✅ SSR + meta + sitemap/RSS hooks✅ (plugin contracts)✅✅✅Product apps that want a real blog feature
Native MDX pagesGit❌ (dev-only)✅❌ (unless custom)❌Simple dev blogs
next-mdx-remoteAnywhere❌/⚠️✅❌❌Flexible sourcing / large content
Content CollectionsGit❌ (dev-only)✅✅✅✅❌Typed content pipelines
NextraGit❌ (dev-only)✅⚠️✅✅“Blog now” with conventions
Tina/DecapGit✅ UI✅⚠️❌Non-dev authoring on file content

Quick Recommendations#

  • If you want a top-tier blog feature inside a full-stack app: ✅ Better Stack Blog plugin

  • If content is dev-only and you want absolute control: Native MDX pages

  • If you need remote/flexible content sources: next-mdx-remote (+ caching/ISR)

  • If you want schema validation + TS types: Content Collections

  • If you want “done in 10 minutes” with a theme: Nextra

  • If non-devs must edit but content stays in Git: TinaCMS / Decap CMS


Closing Thoughts#

In 2026, the “best blog” for a Next.js app depends less on rendering (they can all be fast + SEO-friendly) and more on workflow and feature surface.

If you’re building a product and the blog is a real feature (drafts, editing UI, auth, image uploads, tags, RSS, SEO, stable routes), a plugin-based approach is often the shortest path to “production-grade” without weeks of glue code—making Better Stack’s Blog plugin one of the strongest OSS choices for devs who want to ship fast and keep full-stack ownership.

In This Post

The 2026 Blog Options Landscape1) MDX Files as Pages (Built-in Next.js Support)SetupHello World2) Dynamic MDX with `next-mdx-remote` (Flexible Fetching)Example (Pages Router SSG)3) Content Collections (Type-Safe Markdown as Data)Conceptual example4) Nextra (Blog Theme for Instant Setup)Setup5) Git-Based Editors: TinaCMS and Decap CMSDecap CMS (classic)public/admin/config.ymlTinaCMS (more React-native)6) Better Stack Blog Plugin (Plugin-Based Full-Stack Blog)Why it’s a top contenderMinimal setup (Next.js)1) Install core + adapter + blog plugin2) Create your backend instance3) Mount the API route4) Import plugin styles5) Register the client plugin6) Add the page handler (routes come from plugins)What you get (out of the box)When to pick Better Stack BlogComparison: Which Blog Approach Should You Choose?Quick RecommendationsClosing Thoughts