Skip to content
This repository was archived by the owner on Nov 10, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions apps/docs/content/docs/api-reference/hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,6 @@ Utilities for consuming Better Blog context and routing information.

<AutoTypeTable path="../../packages/better-blog/src/context/types.ts" name="BlogUIComponents" />

#### usePageOverrides — Result

<AutoTypeTable path="../../packages/better-blog/src/types/index.ts" name="PageComponentOverrides" />

#### useBasePath — Result

Returns: string
Expand Down
17 changes: 15 additions & 2 deletions apps/docs/content/docs/core-concepts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ description: How Better Blog is structured and key APIs
- `better-blog/hooks`: React hooks (client)
- `better-blog/client`: `BlogPageRouter` and page components (client-only)
- `better-blog/context`: `BlogProvider`, context and UI types
- `better-blog/router`: route helpers (`matchRoute`, `resolveLoadingComponent`)
- `better-blog/router`: pluggable routes powered by [@olliethedev/yar](https://github.com/olliethedev/yar)
- `better-blog/server/pages`: SSR/SSG adapter (`createBlogServerAdapter`)
- `better-blog/api`: API router (`createBlogApiRouter`)
- `better-blog/sitemap`: sitemap generation (`createBlogSitemap`)
Expand Down Expand Up @@ -41,9 +41,22 @@ export interface BlogDataProvider {
}
```

### Router

Better Blog uses [@olliethedev/yar](https://github.com/olliethedev/yar) for its routing layer.

Better Blog ships with pre-configured routes:
- `/` - Home (list of posts)
- `/new` - Create new post
- `/drafts` - Draft posts
- `/:slug` - View post by slug
- `/:slug/edit` - Edit post
- `/tag/:tag` - Posts by tag

### Architecture

- Schema-driven routes and data; minimal state in components
- Pluggable, composable routes powered by yar
- Schema-driven data with minimal state in components
- Strict client/server separation via dedicated entry points

### TanStack Query
Expand Down
40 changes: 30 additions & 10 deletions apps/docs/content/docs/overrides.mdx
Original file line number Diff line number Diff line change
@@ -1,35 +1,55 @@
---
title: Overriding components
description: Replace built-in pages and wire your own Link/Image
description: Wire your own Link/Image and customize UI components
---

Use `pageOverrides` to replace any built-in page or loading component, and `components` to wire your router-specific `Link`/`Image`.
Use the `components` prop to wire your router-specific `Link`/`Image` components and customize the blog's UI building blocks.

```tsx
import { BlogProvider } from 'better-blog/client';

function MyLink({ href, children, className }) {
return <a href={href} className={className}>{children}</a>;
}

function MyImage({ src, alt, className, width, height }) {
return <img src={src} alt={alt} className={className} width={width} height={height} />;
}

function MyHome() { return <div>Custom Home</div>; }
function MyHomeLoading() { return <div>Loading home…</div>; }
function MyPostCard({ post }) {
return (
<div className="custom-post-card">
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</div>
);
}

function MyPostCardSkeleton() {
return <div className="skeleton">Loading...</div>;
}

<BlogProvider
dataProvider={dataProvider}
components={{ Link: MyLink, Image: MyImage }}
pageOverrides={{ HomeComponent: MyHome, HomeLoadingComponent: MyHomeLoading }}
components={{
Link: MyLink,
Image: MyImage,
PostCard: MyPostCard,
PostCardSkeleton: MyPostCardSkeleton
}}
>
{children}
</BlogProvider>
```

Common overrides:
## Available Component Overrides

- **Link**: Component used for navigation (integrate with your router framework)
- **Image**: Component used for rendering images (e.g., Next.js Image)
- **PostCard**: Component for rendering individual blog post cards in lists
- **PostCardSkeleton**: Skeleton component shown while post cards are loading

## Customizing Pages

- Replace home, post list, tag list, post detail pages
- Swap loading/skeleton components for each route
- Inject your design system primitives via `components`
To customize entire pages, use the yar router to create custom routes. See the [routing documentation](/docs/routing) for more details.

4 changes: 1 addition & 3 deletions apps/docs/content/docs/react-router.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,15 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
```tsx
import { useLocation } from "react-router-dom"
import { BlogMetaTags, BlogPageRouter } from "better-blog/client"
import { matchRoute } from "better-blog/router"
import { useBlogDataProvider } from "./useBlogDataProvider"

export default function BlogEntryPage() {
const location = useLocation()
const dataProvider = useBlogDataProvider()
const routeMatch = matchRoute(location.pathname.split("/").filter(Boolean))
return (
<main>
{dataProvider && (
<BlogMetaTags routeMatch={routeMatch} provider={dataProvider} />
<BlogMetaTags path={location.pathname} provider={dataProvider} />
)}
<BlogPageRouter path={location.pathname} />
</main>
Expand Down
73 changes: 60 additions & 13 deletions apps/docs/content/docs/tanstack-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,76 @@ description: 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, prefetch data in the loader, and render via `BlogPageRouter`. Set the page head with `buildTanStackHead`.
Prefetch data in the loader, render via `BlogPageRouter`, and build SEO metadata using the router's meta and extra fields.

```tsx
import { createFileRoute } from "@tanstack/react-router"
import { createBlogServerAdapter } from "better-blog/server/pages";
import { buildTanStackHead } from "better-blog/router"
import { BlogPageRouter } from "better-blog/client"
import { blogDataProvider } from "../../lib/blog-data-provider";
import { getRouteInfo, prefetchRoute, resolveSEO } from "better-blog/router"
import { blogDataProvider } from "../../lib/blog-data-provider"

// Helper to normalize path from TanStack params
function normalizePath(splat?: string): string {
const pathSegments = splat?.split("/").filter(Boolean) || []
return pathSegments.length ? `/${pathSegments.join("/")}` : "/"
}

export const Route = createFileRoute("/posts/$")({
ssr: true,
head: ({ loaderData }) => loaderData as { meta?: any[]; links?: any[] },
component: RouteComponent,
loader: async ({ params, context }) => {
const serverAdapter = createBlogServerAdapter({
provider: blogDataProvider,
queryClient: context.queryClient,
const routePath = normalizePath(params._splat)
await prefetchRoute(routePath, blogDataProvider, context.queryClient)
return null
},
head: async ({ params }) => {
const routePath = normalizePath(params._splat)
const routeInfo = getRouteInfo(routePath)
const seo = await resolveSEO(routeInfo, blogDataProvider)

// Map SEO to TanStack head format
const meta: Array<{ name?: string; property?: string; content?: string; title?: string }> = []

if (seo.meta.title) meta.push({ title: seo.meta.title })
if (seo.meta.description) meta.push({ name: "description", content: seo.meta.description })
if (seo.meta.robots) meta.push({ name: "robots", content: seo.meta.robots })

// Open Graph
meta.push({
property: "og:type",
content: seo.meta.openGraph?.type ?? (seo.meta.openGraph?.title ? "article" : "website")
})
if (seo.meta.openGraph?.title ?? seo.meta.title) {
meta.push({ property: "og:title", content: seo.meta.openGraph?.title ?? seo.meta.title })
}
if (seo.meta.openGraph?.description ?? seo.meta.description) {
meta.push({ property: "og:description", content: seo.meta.openGraph?.description ?? seo.meta.description })
}
if (seo.meta.openGraph?.url) meta.push({ property: "og:url", content: seo.meta.openGraph.url })

const ogImage = seo.meta.openGraph?.images?.[0]
const ogImageUrl = typeof ogImage === "string" ? ogImage : ogImage?.url
if (ogImageUrl) meta.push({ property: "og:image", content: ogImageUrl })

// Twitter
meta.push({
name: "twitter:card",
content: seo.meta.twitter?.card ?? (ogImageUrl ? "summary_large_image" : "summary")
})
await serverAdapter.prefetch({ path: params._splat })
// Build head data on server
const head = await buildTanStackHead({ path: params._splat, provider: blogDataProvider })
return head
}
if (seo.meta.twitter?.title ?? seo.meta.title) {
meta.push({ name: "twitter:title", content: seo.meta.twitter?.title ?? seo.meta.title })
}
if (seo.meta.twitter?.description ?? seo.meta.description) {
meta.push({ name: "twitter:description", content: seo.meta.twitter?.description ?? seo.meta.description })
}
if (ogImageUrl) meta.push({ name: "twitter:image", content: ogImageUrl })

// Links
const links: Array<{ rel: string; href: string }> = []
if (seo.meta.canonicalUrl) links.push({ rel: "canonical", href: seo.meta.canonicalUrl })

return { meta, links }
},
})

function RouteComponent() {
Expand Down
9 changes: 5 additions & 4 deletions apps/examples/nextjs/app/posts/[[...all]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const generateMetadata: (context: {
params: Promise<{ all: string[] | undefined }>
}) => Promise<Metadata> = async ({ params }) => {
const { all } = await params
return serverAdapter.generateNextMetadata(all?.join("/")) as unknown as Metadata
return serverAdapter.generateNextMetadata(all?.join("/")) as Metadata
}

// Main page component
Expand All @@ -29,7 +29,8 @@ export default async function BlogPage({
params: Promise<{ all: string[] | undefined }>
}) {
const { all } = await params
const { BlogServerRouter } = serverAdapter

return <BlogServerRouter path={all?.join("/")} />
const path = all?.join("/")

// Use the server adapter's BlogServerRouter for SSR with hydration and prefetch
return <serverAdapter.BlogServerRouter path={path} />
}
4 changes: 2 additions & 2 deletions apps/examples/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"dev": "next dev",
"build": "next build",
"start": "next start",
"start:e2e": "rm -rf .next && pnpm build && NODE_ENV=test next start",
"lint": "eslint",
Expand Down
8 changes: 1 addition & 7 deletions apps/examples/react/src/BlogPage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import { BlogMetaTags, BlogPageRouter } from "better-blog/client"
import { matchRoute } from "better-blog/router"
import { useLocation } from "react-router-dom"
import { useBlogDataProvider } from "./useBlogDataProvider"

export default function BlogEntryPage() {
const location = useLocation()

const dataProvider = useBlogDataProvider()
const routeMatch = matchRoute(
location.pathname.split("/").filter(Boolean),
"/posts"
)

return (
<main >
{dataProvider && (
<BlogMetaTags routeMatch={routeMatch} provider={dataProvider} />
<BlogMetaTags path={location.pathname} provider={dataProvider} />
)}
<BlogPageRouter path={location.pathname} />
</main>
Expand Down
3 changes: 2 additions & 1 deletion apps/examples/tanstack/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules
.tanstack
.nitro
.output
.turbo
.turbo
dist
2 changes: 2 additions & 0 deletions apps/examples/tanstack/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
22.17.1

8 changes: 4 additions & 4 deletions apps/examples/tanstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.85.5",
"@tanstack/react-query-devtools": "^5.85.5",
"@tanstack/react-router": "^1.131.28",
"@tanstack/react-router-ssr-query": "^1.131.28",
"@tanstack/react-start": "^1.131.28",
"@tanstack/react-router": "1.131.28",
"@tanstack/react-router-ssr-query": "1.131.28",
"@tanstack/react-start": "1.131.28",
"better-blog": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -30,7 +30,7 @@
"react-dom": "^19.1.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.12",
"vite": "^7.1.3",
"vite": "7.1.3",
"zod": "^4.1.9"
},
"devDependencies": {
Expand Down
7 changes: 6 additions & 1 deletion apps/examples/tanstack/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { QueryClient } from '@tanstack/react-query'
// src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { QueryClient } from '@tanstack/react-query'
import { routeTree } from './routeTree.gen'
// The root route is defined in `routes/__root.tsx`

Expand Down Expand Up @@ -32,6 +32,11 @@ export function createRouter() {
return router
}

// TanStack Start requires getRouter to be exported
export function getRouter() {
return createRouter()
}

declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
Expand Down
Loading