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, 2025Web DevelopmentReact Router

Add a Blog to React Router with BTST v3

Set up BTST Blog in a React Router v7 Framework Mode app, register its routes, and complete persistence, authorization, and uploads before deployment.

Add a Blog to React Router with BTST v3

BTST's Blog plugin adds an editor, drafts, published posts, tags, and server-rendered articles to a React Router application. This walkthrough uses the v3 generator, then registers its route modules and explains what remains before production.

The target is an existing React Router v7 Framework Mode app with server rendering. A browser-only <BrowserRouter> setup does not supply the server loaders and API route used here. The integration was checked with @btst/codegen 3.1.2, @btst/stack 3.1.2, React Router 7.13.1, and Node.js 22.18.0. These are verification versions, not a claim that they are the newest releases. For an existing BTST installation, review the v3 migration notes first.

Prepare the app#

Start from a working Framework Mode app with Tailwind CSS v4 and shadcn/ui configured with CSS variables. Keep a clean commit so you can review the generated files and handle conflicts without losing your application code.

Install the UI components used by the generated setup:

BASH
  1. 1
npx shadcn@latest add button dropdown-menu sonner

Render Sonner's <Toaster /> once in your root. Retain your document's <Meta />, <Links />, <Scripts />, and <ScrollRestoration />, along with your existing providers and theme. The generated Blog layout supplies React Query and BTST context for its child routes.

If you want to inspect the interface first, open the live Blog. This website uses /p/blog; the generator below uses /pages/blog.

Generate the integration#

Run from the application root:

BASH
  1. 1
  2. 2
  3. 3
  4. 4
npx @btst/codegen@3.1.2 init \
  --framework react-router \
  --adapter memory \
  --plugins blog

Allow it to install the runtime dependencies, then review the generated files, CSS changes, and root provider changes. Codegen 3.1.2's installer requests Stack 3.1.1. For the Stack version tested here, explicitly pin 3.1.2 after generation. This fixture used pnpm 10:

BASH
  1. 1
pnpm add @btst/stack@3.1.2 @btst/yar@1.3.2

Use your existing package manager's equivalent and keep its lockfile. Review newer releases before assuming identical behavior.

The memory adapter provides a private, single-process evaluation. Posts disappear when that process restarts. This setup has no application administrator policy and must stay private until you configure authorization and durable storage.

Generated locationJob
app/lib/stack.tsBackend Blog plugin, memory adapter, API handler
app/lib/stack-client.tsxBrowser-safe client stack and public paths
app/lib/stack-client.server.tsRequest credentials and trusted server origins
app/routes/api/data/$.tsAPI loader and action
app/routes/pages/_layout.tsxReact Query, StackProvider, and upload override
app/routes/pages/$.tsxPage loading, hydration, metadata, and errors
app/routes/sitemap.xml.tsPublic sitemap response

Check the root file before building. In the tested generator, a compact one-line root function could receive an invalid provider insertion. Format that function across multiple lines before generation, or repair the insertion after reviewing the diff. Keep the rest of your root layout intact.

Register the routes#

Creating files is only part of the setup. Codegen 3.1.2 does not add these entries to app/routes.ts. In a configuration-based app, merge the following routes with your existing entries:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
import { type RouteConfig, index, layout, route } from "@react-router/dev/routes"

export default [
  index("routes/home.tsx"),
  layout("routes/pages/_layout.tsx", [
    route("pages/*", "routes/pages/$.tsx"),
  ]),
  route("api/data/*", "routes/api/data/$.ts"),
  route("sitemap.xml", "routes/sitemap.xml.ts"),
] satisfies RouteConfig

The home route is an example of an existing entry; preserve your actual homepage and other routes. The layout supplies context without adding another URL segment. The splat matches the Blog list, individual posts, and editor paths.

If your app uses file route conventions, check its generated route tree and add the equivalent mappings without registering duplicate routes. React Router's routing documentation explains explicit configuration and file convention options.

Check generated route types#

With the tested versions, React Router's generated types rejected the broad React component type exported by page.ErrorBoundary. In app/routes/pages/$.tsx, replace only export const ErrorBoundary = page.ErrorBoundary with this function wrapper:

TSX
  1. 1
  2. 2
  3. 3
  4. 4
export function ErrorBoundary() {
  const Boundary = page.ErrorBoundary
  return <Boundary />
}

It keeps BTST's error UI while exposing a function export that satisfies the route-module type constraint. Retain the generated loader, metadata, and default component. Run your normal route type generation and TypeScript checks; do not suppress their errors.

Bundle the renderer for SSR#

Include BTST and its Markdown renderer in Vite's server bundle. Merge this option into your existing vite.config.ts, retaining its React Router, Tailwind, and path-alias plugins:

TS
  1. 1
  2. 2
  3. 3
ssr: {
  noExternal: ["@btst/stack", "@btst/yar"],
},

Without this setting, our production fixture built successfully but returned HTTP 500 for an article: Node tried to load a syntax-highlighting CSS file from an externalized dependency. The browser could still display the article after hydration, which hid the server failure. Check the HTTP response and rendered HTML as well as the visible page.

Set the origins#

For a local Vite server on port 5173, put these values in .env:

DOTENV
  1. 1
  2. 2
BTST_SITE_URL=http://localhost:5173
BTST_API_URL=http://localhost:5173

Use the port your app actually serves. Each value is an origin: scheme, host, and port. The generated client supplies /pages and /api/data separately.

For a same-origin production deployment, configure both values as the public application origin in the runtime environment. The generated server client requires trusted origins in production. Keep request credentials in the server integration and pass only the resolved origins into the browser layout. Do not replace that boundary with arbitrary incoming host headers.

Understand the API bridge#

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

The generated API route adapts that handler to React Router:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
import { toReactRouterHandlers } from "@btst/stack/react-router"
import { handler } from "~/lib/stack"

const handlers = toReactRouterHandlers(handler)
export const loader = handlers.loader
export const action = handlers.action

Keep the individual exports. React Router's build needs to identify the server-only loader and action; the released template avoids a destructured export here. With no default component, this is a resource route: reads go through its loader and mutations through its action.

The page route uses createReactRouterPage and getStackClientForRequest. It resolves the Blog path, preloads data, supplies hydration and metadata, and handles missing routes. Its layout passes the client stack to StackProvider with reactRouter(). Use these generated v3 helpers instead of copying the older createStackClient and BetterStackProvider configuration.

Check the local blog#

Start the application with its existing development command:

BASH
  1. 1
npm run dev

Open /pages/blog. An empty list is expected before adding posts. Check /api/data/posts?published=true for a successful JSON response, then open /pages/blog/new and create a local evaluation post. Confirm it appears on the list and at /pages/blog/your-slug after publication.

Other Blog routes include /pages/blog/drafts, /pages/blog/your-slug/edit, and /pages/blog/tag/your-tag. Test direct URL loads and client navigation; both must render correctly. The SSR and hydration guide covers request-specific data when you customize this integration.

If the route is missing, inspect app/routes.ts first. If API JSON works but the page fails, check the page layout, request client, and origin configuration. Regenerate route types and run your typecheck after adding route files.

Finish styles and uploads#

The generator adds Blog styles to your global CSS:

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

Keep your shadcn theme variables. Use the BTST Tailwind checklist if the editor or article body appears unstyled.

The generated blog.uploadImage override in app/routes/pages/_layout.tsx throws a TODO error until you implement it. Connect it to an authorized upload service and return the actual stored image URL. Check file types, sizes, permissions, and delivery. A placeholder URL does not upload bytes. If you use BTST Media, register the asset through its operations so the Media library matches storage.

Add production controls#

Complete these application-specific steps before exposing the blog:

  1. Persist content. Replace the memory adapter with a supported database adapter. Review schema generation and migrations, then verify that posts survive a restart. See database adapters.
  2. Authorize reads and writes. Configure trusted server identity and v3 authorization rules. Permit anonymous reads of published posts while restricting drafts, creation, editing, and deletion to your editors. Test the API directly as both an anonymous visitor and an editor. Follow the authorization reference and React Router permissions guide.
  3. Complete uploads and delivery. Exercise the real upload flow and inspect the resulting images on cards, article headers, and social metadata.

The minimal backend above omits application authorization. A working editor route or a hidden button does not protect its API. Older examples that return an admin check from lifecycle hooks are not a substitute for the v3 authorization setup.

Run your production build with the intended origin configuration. Verify a published article's HTML, canonical and social metadata, and /sitemap.xml anonymously. The tested default article metadata included an Open Graph URL but no canonical link; add your application’s canonical link and verify its public origin. The generated sitemap has cache headers, so include sitemap freshness when checking edits and deployments. A passing build does not prove your database, identity provider, or upload service works.

Continue with the installation reference, Blog plugin reference, and released BTST v3.1.2 source.

In This Post

Prepare the appGenerate the integrationRegister the routesCheck generated route typesBundle the renderer for SSRSet the originsUnderstand the API bridgeCheck the local blogFinish styles and uploadsAdd production controls