Better Blog

Tanstack Start

SSR/SSG with server prefetch and client hydration

This renders all blog routes under /posts/... with server prefetch + client hydration. Provide separate data providers for server and client.

1) src/routes/posts/$.tsx (server)

Create the Better Blog server adapter, then render via Entry with server prefetch + client hydration.

import { createFileRoute } from "@tanstack/react-router"

import { BlogDataProvider, getDefaultQueryClient } from "better-blog";
import { createServerAdapter } from "better-blog/server";

// Implement your data provider here
const serverBlogConfig: BlogDataProvider = {
  ...
};

const queryClient = getDefaultQueryClient();

// Create the server adapter
const serverAdapter = createServerAdapter(
  serverBlogConfig,
  queryClient
);

export const Route = createFileRoute("/posts/$")({
    component: RouteComponent
})

function RouteComponent() {
    const { _splat } = Route.useParams()

    const { Entry } = serverAdapter;

  return <Entry path={_splat} queryClient={queryClient} />;
}

2) src/routes/__root.tsx

Define the root document and wrap your app with Provider to supply context.

// src/routes/__root.tsx
/// <reference types="vite/client" />
import type { ReactNode } from 'react'
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from '@tanstack/react-router'
import { Provider } from '@/providers'

import appCss from "@/styles/app.css?url"

export const Route = createRootRoute({
  head: () => ({
    meta: [
      {
        charSet: 'utf-8',
      },
      {
        name: 'viewport',
        content: 'width=device-width, initial-scale=1',
      },
      {
        title: 'TanStack Start Starter',
      },
    ],
    links: [
        {
          rel: "stylesheet",
          href: appCss,
        },
    ],
  }),
  component: RootComponent,
})

function RootComponent() {
  return (
    <RootDocument>
      <Outlet />
    </RootDocument>
  )
}

function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
  return (
    <html>
      <head>
        <HeadContent />
      </head>
      <body>
        <Provider>
            {children}
        </Provider>
        <Scripts />
      </body>
    </html>
  )
}

3) src/providers.tsx (client)

Client-side provider wiring TanStack Router Link, React Query, navigation helpers, and optional admin UI.

"use client";

import { ReactNode } from "react";
import { Link, useRouter } from "@tanstack/react-router"
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { BetterBlogContextProvider } from "better-blog/client";
import { ComponentsContextValue, getDefaultQueryClient } from "better-blog";

const components: ComponentsContextValue = {
  Link: ({ href, children, className }) => (
    <Link to={href} className={className}>
      {children}
    </Link>
  ),
  Image: ({ src, alt, className, width, height }) => (
    <img
      src={src}
      alt={alt}
      className={className}
      width={width || 800}
      height={height || 400}
    />
  ),
};


export function Provider({ children }: { children: ReactNode }) {
  const queryClient = getDefaultQueryClient();
  const router = useRouter();

  return (

    <QueryClientProvider client={queryClient}>
      <BetterBlogContextProvider
        localization={{
          BLOG_LIST_TITLE: "Blog Posts",
        }}
        components={components}
        adminUiOptions={{
          // implement your own admin ui logic here
          canCreate:true,
          canUpdate:true,
          canDelete:true
        }}
        navigate={(href) => router.navigate({ href })}
        replace={(href) => router.navigate({ href, replace: true })}
        uploadImage={async (file) => {
          console.log("uploadImage", file);
          // implement your own image upload logic here
          return "https://placehold.co/400/png";
        }}
      >
        {children}
      </BetterBlogContextProvider>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}