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>
);
}