BTST
PluginsQuickstartDocs
Live Blog

From research to product evaluation

Evaluating a publishing workflow for an app you already own?

See what the BTST Blog plugin adds to an existing React or Next.js app
BTST

Open-source TypeScript features for the React application, data, and deployment you already own.

Released plugins

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

Resources

  • Quickstart
  • Documentation
  • All plugins
  • Live Blog
  • GitHub (opens in a new tab)
© 2026 BTST. Open source under the MIT License.
November 28, 2025NextJSWeb Development

Add a Blog to Next.js with BTST v3

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.

Add a Blog to Next.js with BTST v3

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.

Prepare the app#

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:

BASH
  1. 1
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.

Generate the integration#

Run the released generator from your application root:

BASH
  1. 1
  2. 2
  3. 3
  4. 4
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 locationPurpose
lib/stack.tsBackend plugins, database adapter, and API handler
lib/stack-client.tsxBrowser-safe client configuration
lib/stack-client.server.tsRequest headers and trusted origins for server rendering
app/api/data/[[...all]]/route.tsNext.js Route Handler for the Blog API
app/(request)/pages/[[...all]]/page.tsxRequest-aware pages and metadata
app/pages/client-layout.tsxReact Query, StackProvider, routing, and upload override
app/sitemap.tsSitemap 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.

Set trusted origins#

For the default local port, add these values to .env.local:

DOTENV
  1. 1
  2. 2
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.

Check the API and pages#

The generated backend uses the v3 entry point:

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
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:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
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:

BASH
  1. 1
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.

Verify styles and images#

The generator adds the Blog stylesheet to the application's global CSS:

CSS
  1. 1
  2. 2
@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.

Add production controls#

Before making the blog public, complete these three changes:

  1. Persist posts. Replace memory with your chosen database adapter and review the generated schema and migrations. Database adapters covers the supported choices. Test that posts survive a restart; process-local memory cannot provide that guarantee on a hosted deployment.
  2. Authorize operations. Connect trusted server identity resolution and the v3 authorization rules. Allow public reads only for published posts, and restrict drafts and create/update/delete operations to your intended editors. Test anonymous HTTP requests directly, including draft reads and writes. See the authorization guide.
  3. Complete uploads. Implement the Blog upload override and verify its actual stored bytes and URL. If you also use BTST Media, register assets through its operations so storage and the Media library agree.

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.

Choose a public URL#

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.

In This Post

Prepare the appGenerate the integrationSet trusted originsCheck the API and pagesVerify styles and imagesAdd production controlsChoose a public URL