Add the BTST Blog plugin to a Next.js App Router app with the v3 generator, then configure persistent storage, editor authorization, image uploads, and public routes.

BTST's Blog plugin adds an editor, published posts, drafts, tags, and server-rendered article pages to an existing Next.js application. This guide uses the v3 integration to get a local blog running, then explains the storage, authorization, and upload work needed before deployment.
The examples target the App Router. They use @btst/codegen 3.1.2 and @btst/stack 3.1.2; the generated application was checked with Next.js 16.2.1 and Node.js 22.18.0. If your app already has BTST, review the v3 migration notes before replacing its integration files.
Start with an existing Next.js App Router project, Node.js 22, Tailwind CSS v4, and shadcn/ui configured with CSS variables. Keep a clean commit so you can review the generator's changes and handle file conflicts deliberately.
Install the UI components used by the generated setup:
npx shadcn@latest add button dropdown-menu sonner
Render the Sonner <Toaster /> once in your existing root layout. Retain your application's theme, providers, and layout markup. The generated Blog layout provides its own React Query and BTST context; you do not need to pass a QueryClient instance from the root Server Component into a client provider.
If you only want to evaluate the interface first, open the live Blog. This site's /p/blog prefix differs from the /pages/blog prefix generated below.
Run the released generator from your application root:
npx @btst/codegen@3.1.2 init \
--framework nextjs \
--adapter memory \
--plugins blog
Let it install the runtime dependencies and review any conflict prompts. The memory adapter is for a local, single-process evaluation: posts are lost when its process restarts. This command does not configure a production database or an administrator sign-in policy. Keep the evaluation private until you complete those steps.
For a reproducible v3 runtime matching this guide, retain @btst/stack 3.1.2 in your dependency lockfile. If the generator resolves a newer release, review that release before assuming the same behavior.
The generated files separate several jobs:
| File or location | Purpose |
|---|---|
lib/stack.ts | Backend plugins, database adapter, and API handler |
lib/stack-client.tsx | Browser-safe client configuration |
lib/stack-client.server.ts | Request headers and trusted origins for server rendering |
app/api/data/[[...all]]/route.ts | Next.js Route Handler for the Blog API |
app/(request)/pages/[[...all]]/page.tsx | Request-aware pages and metadata |
app/pages/client-layout.tsx | React Query, StackProvider, routing, and upload override |
app/sitemap.ts | Sitemap output for the configured public routes |
Route groups such as (request) organize files without adding a URL segment. With an application under src/, the generated locations follow that structure. Review the actual output and run your build after generation.
For the default local port, add these values to .env.local:
BTST_SITE_URL=http://localhost:3000
BTST_API_URL=http://localhost:3000
Both values are origins: the scheme, hostname, and port. The generated configuration supplies /pages and /api/data separately. Change the port if your development server uses another one.
In a same-origin production deployment, set both values to the real public application origin. The generated server code requires trusted origins in production rather than silently trusting arbitrary request headers. Keep credential forwarding in the server-only integration; do not copy session headers into browser configuration.
The generated backend uses the v3 entry point:
import { createBackendStack } from "@btst/stack/api"
import { createMemoryAdapter } from "@btst/adapter-memory"
import { blogBackendPlugin } from "@btst/stack/plugins/blog/api"
export const myStack = createBackendStack({
basePath: "/api/data",
plugins: {
blog: blogBackendPlugin(),
},
adapter: (db) => createMemoryAdapter(db)({}),
})
export const { handler, dbSchema } = myStack
That example is the local evaluation backend. The generated Next.js API route adapts its handler with the framework helper:
import { toNextRouteHandlers } from "@btst/stack/next"
import { handler } from "@/lib/stack"
export const { GET, POST, PUT, PATCH, DELETE } =
toNextRouteHandlers(handler)
The page integration uses createNextPage and the request-specific client. It awaits Next.js request headers, loads the matching Blog route, and exports the associated metadata function. Keep this request path for drafts and editor pages; those views must respect the current session when authorization is configured.
Start the app:
npm run dev
Open /pages/blog and confirm that the page renders. An empty list is valid before you add a post. Check /api/data/posts?published=true for a successful JSON response, then use /pages/blog/new to create local evaluation content. Confirm that a published post appears on the list and opens at /pages/blog/your-slug.
The Blog plugin also provides draft management at /pages/blog/drafts, editing at /pages/blog/your-slug/edit, and tag pages at /pages/blog/tag/your-tag. A generated route or a hidden edit button does not establish server authorization.
The generator adds the Blog stylesheet to the application's global CSS:
@import "tailwindcss";
@import "@btst/stack/plugins/blog/css";
Retain your shadcn theme variables alongside these imports. If the editor or article body appears unstyled, work through the BTST Tailwind CSS checklist.
Image uploads are an application task. In app/pages/client-layout.tsx, the generated blog.uploadImage override throws a TODO error until you implement it. Connect it to your authorized upload flow and return the actual stored image URL. A placeholder URL does not upload a file. Verify access controls, supported file types and sizes, and delivery from your storage provider before enabling uploads for editors.
Before making the blog public, complete these three changes:
The minimal generated backend above has no application identity or authorization policy. Do not treat it as an admin-only backend. v3 authorization is configured through the stack's authorization APIs; older examples that return an admin check from onBeforeCreatePost are not the setup used here.
Keep /pages/blog as the initial working route. Codegen also creates static Blog examples under /pages/ssg-blog; choose a deliberate public URL strategy before exposing both versions of the same article.
Static examples introduce build-time data and revalidation requirements. After changing a published post, check the public list, article, metadata, and sitemap as an anonymous visitor. If you adopt cached pages, implement the corresponding invalidation in your create, update, and delete flows. The drafts and caching guide explains the separate public and editorial concerns.
Run a production build with the same origin configuration you intend to deploy. Verify the rendered Blog route, a published article, canonical and social metadata, sitemap entries, and denied anonymous draft/write requests. A passing build is one check; it does not prove your database, identity provider, or upload service works.
Continue with the installation reference for manual integration and the Blog plugin reference for customization. The released v3.1.2 source provides the versioned implementation behind this guide.