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

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.
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:
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.
Run from the application root:
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:
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 location | Job |
|---|---|
app/lib/stack.ts | Backend Blog plugin, memory adapter, API handler |
app/lib/stack-client.tsx | Browser-safe client stack and public paths |
app/lib/stack-client.server.ts | Request credentials and trusted server origins |
app/routes/api/data/$.ts | API loader and action |
app/routes/pages/_layout.tsx | React Query, StackProvider, and upload override |
app/routes/pages/$.tsx | Page loading, hydration, metadata, and errors |
app/routes/sitemap.xml.ts | Public 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.
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:
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.
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:
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.
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:
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.
For a local Vite server on port 5173, put these values in .env:
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.
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
The generated API route adapts that handler to React Router:
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.
Start the application with its existing development command:
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.
The generator adds Blog styles to your global CSS:
@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.
Complete these application-specific steps before exposing the blog:
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.