Better Stack logoBetter Stack
BlogGitHubDocs
November 28, 2025Web DevelopmentTanStack

The Easiest Way to Add a Blog to TanStack Start in 2026

TanStack Start brings type-safe routing to React. Now add a type-safe blog in minutes — complete with database, API, SSR, and SEO — using Better Stack's modular plugin system.

The Easiest Way to Add a Blog to TanStack Start in 2026

TanStack Start represents everything right about modern React development. Type-safe routing. First-class SSR. File-based conventions that don't fight you. It's the framework for developers who care about architecture.

So when it's time to add a blog, you expect the same quality. Type safety end-to-end. Server-side rendering that just works. Components that respect your patterns.

What you get instead? The same tired options as every other framework. Wire up a CMS. Build from scratch. Spend days on a problem that should be solved.

TanStack Start deserves better.

Type-Safe Features, Not Just Type-Safe Routes#

TanStack Router pioneered type-safe routing in React. But type safety shouldn't stop at navigation. Your features — auth, blogs, dashboards — should be equally typed, equally integrated, equally first-class.

This is the promise of modular full-stack features. Not scaffolding. Not templates. Production-ready functionality that composes into your app with full type inference.

Better Stack delivers this for TanStack Start. A blog plugin that provides typed database schemas, API routes, React components, SSR loaders, and SEO metadata — all designed to work with TanStack's patterns.

Let's build it.

Prerequisites#

Your TanStack Start project needs:

  • shadcn/ui installed with CSS variables enabled
  • Sonner <Toaster /> for notifications
  • TailwindCSS v4 configured
  • @tanstack/react-query installed (TanStack Start projects typically have this)

Step 1: Install Better Stack#

Add the core package and a database adapter. We'll use Prisma in this guide, but Better Stack supports multiple adapters:

  • @btst/adapter-prisma — For Prisma ORM (PostgreSQL, MySQL, SQLite, CockroachDB)
  • @btst/adapter-drizzle — For Drizzle ORM
  • @btst/adapter-kysely — For Kysely query builder
  • @btst/adapter-mongodb — For MongoDB
  • @btst/adapter-memory — For development and testing
BASH
  1. 1
npm install @btst/stack @btst/adapter-prisma

Step 2: Create the Backend Instance#

Set up lib/better-stack.ts:

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
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
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()
  },
  adapter: (db) => createPrismaAdapter(prisma, db, { 
    provider: "postgresql"
  })
})

export { handler, dbSchema }

This gives you a complete API layer — typed handlers, database operations, request validation — from a single configuration.

Step 3: Create the API Route#

TanStack Start uses file-based server handlers. Create a catch-all route:

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 { createFileRoute } from '@tanstack/react-router'
import { handler } from '@/lib/better-stack'

export const Route = createFileRoute('/api/data/$')({
  server: {
    handlers: {
      GET: async ({ request }) => handler(request),
      POST: async ({ request }) => handler(request),
      PUT: async ({ request }) => handler(request),
      DELETE: async ({ request }) => handler(request),
    },
  },
})

Clean. Type-safe. Your blog API is live.

Step 4: Generate the Database Schema#

Generate Prisma schema additions:

BASH
  1. 1
npx @btst/cli generate --config=lib/better-stack.ts --orm=prisma --output=prisma/schema.prisma

Run the migration:

BASH
  1. 1
npx prisma migrate dev --name add-blog

Step 5: Set Up the Client#

Create lib/better-stack-client.tsx:

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

const getBaseURL = () => 
  typeof window !== 'undefined' 
    ? (import.meta.env.VITE_BASE_URL || window.location.origin)
    : (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: queryClient,
        seo: {
          siteName: "My Blog",
          author: "Your Name",
          twitterHandle: "@yourhandle",
          locale: "en_US",
          defaultImage: `${baseURL}/og-image.png`,
        },
      })
    }
  })
}

Step 6: Configure React Query#

Create a query client utility that works with TanStack Start's SSR:

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
  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
import { QueryClient, isServer } from "@tanstack/react-query"

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: isServer ? 60 * 1000 : 0,
        refetchOnMount: false,
        refetchOnWindowFocus: false,
        retry: false
      },
      dehydrate: {
        shouldDehydrateQuery: () => true
      }
    }
  })
}

let browserQueryClient: QueryClient | undefined = undefined

export function getOrCreateQueryClient() {
  if (isServer) {
    return makeQueryClient()
  }
  if (!browserQueryClient) browserQueryClient = makeQueryClient()
  return browserQueryClient
}

Configure your router with the query client:

TSX
  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
  36. 36
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
import { QueryClient } from '@tanstack/react-query'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { getOrCreateQueryClient } from '@/lib/query-client'

export interface MyRouterContext {
  queryClient: QueryClient
}

export function getRouter() {
  const queryClient = getOrCreateQueryClient()
  
  const router = createRouter({
    routeTree,
    scrollRestoration: true,
    defaultPreload: false,
    context: {
      queryClient,
    },
    notFoundMode: "root",
  })

  setupRouterSsrQueryIntegration({
    router,
    queryClient,
  })

  return router
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof getRouter>
  }
}

Step 7: Import Plugin Styles#

Add the blog CSS:

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

Step 8: Create the Layout Provider#

Set up TanStack-specific overrides:

TSX
  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
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
import { BetterStackProvider } from "@btst/stack/context"
import { QueryClientProvider } from "@tanstack/react-query"
import type { BlogPluginOverrides } from "@btst/stack/plugins/blog/client"
import { Link, useRouter, Outlet, createFileRoute } from "@tanstack/react-router"

const getBaseURL = () => 
  typeof window !== 'undefined' 
    ? (import.meta.env.VITE_BASE_URL || window.location.origin)
    : (process.env.BASE_URL || "http://localhost:3000")

type PluginOverrides = {
  blog: BlogPluginOverrides
}

export const Route = createFileRoute('/pages')({
  component: Layout
})

function Layout() {
  const router = useRouter()
  const context = Route.useRouteContext()
  const baseURL = getBaseURL()

  return (
    <QueryClientProvider client={context.queryClient}>
      <BetterStackProvider<PluginOverrides>
        basePath="/pages"
        overrides={{
          blog: {
            apiBaseURL: baseURL,
            apiBasePath: "/api/data",
            navigate: (href) => router.navigate({ to: href }),
            uploadImage: async (file) => {
              // Implement your image upload logic
              return "https://example.com/uploads/image.jpg"
            },
            Link: ({ href, children, className, ...props }) => (
              <Link to={href} className={className} {...props}>
                {children}
              </Link>
            ),
          }
        }}
      >
        <Outlet />
      </BetterStackProvider>
    </QueryClientProvider>
  )
}

Step 9: Create the Page Handler#

TanStack Start's loader pattern integrates perfectly with Better Stack:

TSX
  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
import { createFileRoute, notFound } from "@tanstack/react-router"
import { getStackClient } from "@/lib/better-stack-client"
import { normalizePath } from "@btst/stack/client"

export const Route = createFileRoute("/pages/$")({
  ssr: true,
  component: Page,
  loader: async ({ params, context }) => {
    const routePath = normalizePath(params._splat)
    const stackClient = getStackClient(context.queryClient)
    const route = stackClient.router.getRoute(routePath)
    
    if (!route) throw notFound()
    if (route?.loader) await route.loader()
    
    return { meta: await route?.meta?.() }
  },
  head: ({ loaderData }) => {
    return loaderData?.meta && Array.isArray(loaderData.meta) 
      ? { meta: loaderData.meta } 
      : { meta: [{ title: "Blog" }] }
  },
  notFoundComponent: () => <p>This page doesn't exist!</p>
})

function Page() {
  const context = Route.useRouteContext()
  const { _splat } = Route.useParams()
  const routePath = normalizePath(_splat)
  const route = getStackClient(context.queryClient).router.getRoute(routePath)
  
  return route && route.PageComponent ? <route.PageComponent /> : <div>Route not found</div>
}

Notice how naturally this fits TanStack Start's patterns:

  • loader handles server-side data prefetching
  • head generates SEO metadata
  • ssr: true enables server rendering
  • Type inference flows through the entire chain

Your Type-Safe Blog is Live#

Navigate to /pages/blog. Here's what you have:

Pages:

  • /pages/blog — Blog homepage with published posts
  • /pages/blog/drafts — Draft management
  • /pages/blog/new — Rich markdown editor
  • /pages/blog/:slug — Individual post pages
  • /pages/blog/:slug/edit — Edit existing posts
  • /pages/blog/tag/:tagSlug — Tag-filtered views

Features:

  • Full SSR with TanStack Start's loader pattern
  • SEO metadata via head function
  • Type-safe routing integration
  • React Query hydration handled automatically
  • Markdown editor with live preview
  • Draft/publish workflow

API:

  • GET /api/data/posts — List with filtering
  • POST /api/data/posts — Create
  • PUT /api/data/posts/:id — Update
  • DELETE /api/data/posts/:id — Delete
  • GET /api/data/tags — List tags

Adding Sitemap Support#

Generate a sitemap from all plugins:

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
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
import { createFileRoute } from "@tanstack/react-router"
import { QueryClient } from "@tanstack/react-query"
import { getStackClient } from "@/lib/better-stack-client"
import { sitemapEntryToXmlString } from "@btst/stack/client"

export const Route = createFileRoute("/sitemap.xml")({
  server: {
    handlers: {
      GET: async () => {
        const queryClient = new QueryClient()
        const stackClient = getStackClient(queryClient)
        const entries = await stackClient.generateSitemap()
        const xml = sitemapEntryToXmlString(entries)

        return new Response(xml, {
          headers: {
            "Content-Type": "application/xml; charset=utf-8",
            "Cache-Control": "public, max-age=0, s-maxage=3600",
          },
        })
      },
    },
  },
})

Adding Authorization#

Protect your blog with typed hooks:

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
  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
import { blogBackendPlugin, type BlogBackendHooks } from "@btst/stack/plugins/blog/api"

const blogHooks: BlogBackendHooks = {
  onBeforeListPosts(filter, context) {
    if (filter.published === false) {
      return isAdmin(context.headers as Headers)
    }
    return true
  },
  onBeforeCreatePost(data, context) {
    return isAdmin(context.headers as Headers)
  },
  onBeforeUpdatePost(postId, data, context) {
    return isAdmin(context.headers as Headers)
  },
  onBeforeDeletePost(postId, context) {
    return isAdmin(context.headers as Headers)
  },
}

const { handler, dbSchema } = betterStack({
  plugins: {
    blog: blogBackendPlugin(blogHooks)
  },
  // ...
})

Why TanStack Start + Better Stack#

TanStack Start is built on principles: type safety, composition, framework-agnostic design. Better Stack shares these values.

The blog plugin isn't a black box. It's typed end-to-end. The database schema is inspectable. The API routes are overridable. The React components use your design tokens. Everything flows through React Query, which TanStack Start already integrates natively.

This is what modular full-stack development looks like when both the framework and the feature library share the same philosophy. No impedance mismatch. No fighting abstractions. Just composition.

TanStack brought type-safe routing to React. Better Stack brings type-safe features. Together, they represent a future where building production apps means assembling well-designed modules — not rewriting infrastructure.

Build with better blocks.


Resources:

  • Full Installation Guide — Complete setup instructions for all frameworks and adapters
  • Better Stack Documentation
  • Blog Plugin Reference
  • Database Adapters — Detailed configuration for Prisma, Drizzle, Kysely, and MongoDB
  • TanStack Start Example Repository

In This Post

Type-Safe Features, Not Just Type-Safe RoutesPrerequisitesStep 1: Install Better StackStep 2: Create the Backend InstanceStep 3: Create the API RouteStep 4: Generate the Database SchemaStep 5: Set Up the ClientStep 6: Configure React QueryStep 7: Import Plugin StylesStep 8: Create the Layout ProviderStep 9: Create the Page HandlerYour Type-Safe Blog is LiveAdding Sitemap SupportAdding AuthorizationWhy TanStack Start + Better Stack

Powered by Better-Stack