Better Blog

Next.js App Router

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) app/posts/[[...all]]/page.tsx (server)

Create the Better Blog server adapter, export Next.js data functions, and render with server prefetch + client hydration.

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

import { Metadata } from "next";
import { blogConfig } from "@/lib/blog-data-provider";

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

// Create query client for React Query
const queryClient = getDefaultQueryClient();

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

// Export the required Next.js functions
export const generateStaticParams = serverAdapter.generateStaticParams;
export const generateMetadata: (context: {
  params: Promise<{ all: string[] | undefined }>;
}) => Promise<Metadata> = async ({ params }) => {
  const { all } = await params;
  return serverAdapter.generateMetadata(all?.join("/"));
};

// Main page component
export default async function BlogPage({
  params,
}: {
  params: Promise<{ all: string[] | undefined }>;
}) {
  const { all } = await params;
  const { Entry } = serverAdapter;

  return <Entry path={all?.join("/")} queryClient={queryClient} />;
}

2) app/posts/[[...all]]/layout.tsx (server)

Wrap the route with your Provider to supply Query Client, Better Blog context, and router bindings.

import { ReactNode } from 'react';
import { Provider } from '@/components/providers'; 

export default function BlogLayout({ 
  children 
}: { 
  children: ReactNode 
}) {
  return (
    <Provider>
      {children}
    </Provider>
  );
}

3) components/providers.tsx (client)

Client-side provider wiring Next.js Link/Image, React Query, navigation helpers, and optional admin UI.

"use client";

import { ReactNode } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/navigation"
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";


// Next.js components to be used in the blog
const components: ComponentsContextValue = {
  Link: ({ href, children, className }) => (
    <Link href={href} className={className}>
      {children}
    </Link>
  ),
  Image: ({ src, alt, className, width, height }) => (
    <Image
      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}
        basePath="/posts"
        adminUiOptions={{
          // implement your own admin ui logic here
          canCreate:true,
          canUpdate:true,
          canDelete:true
        }}
        navigate={router.push}
        replace={router.replace}
        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>
  );
}