Skip to content
Open
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
229 changes: 228 additions & 1 deletion docs/start/framework/react/guide/seo.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ TanStack Start gives you the building blocks for technical SEO:
- **Static Prerendering** - Pre-generates pages for optimal performance and crawlability
- **Document Head Management** - Full control over meta tags, titles, and structured data
- **Performance** - Fast load times through code-splitting, streaming, and optimal bundling
- **[@tanstack/meta](#using-tanstackmeta)** - Composable, type-safe utilities for meta tags and JSON-LD

## Document Head Management

Expand Down Expand Up @@ -118,9 +119,135 @@ export const Route = createFileRoute('/posts/$postId')({
})
```

## Using @tanstack/meta

For a more streamlined approach to meta tag management, you can use the `@tanstack/meta` package. It provides composable, type-safe utilities that handle the 90% use case with a single function call.

### Installation

```bash
npm install @tanstack/meta
```

### The createMeta Function

The `createMeta` function generates a complete set of meta tags from a simple configuration:

```tsx
import { createFileRoute } from '@tanstack/react-router'
import { createMeta } from '@tanstack/meta'

export const Route = createFileRoute('/')({
head: () => ({
meta: createMeta({
title: 'My App - Home',
description: 'Welcome to My App, a platform for...',
}),
}),
component: HomePage,
})
```

This single call generates:

- `<meta charset="utf-8">`
- `<meta name="viewport" content="width=device-width, initial-scale=1">`
- `<title>My App - Home</title>`
- `<meta name="description" content="...">`
- Open Graph tags (`og:title`, `og:description`, `og:type`)
- Twitter Card tags (`twitter:card`, `twitter:title`, `twitter:description`)

### Full Example with Social Sharing

```tsx
import { createFileRoute } from '@tanstack/react-router'
import { createMeta } from '@tanstack/meta'

export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
head: ({ loaderData }) => ({
meta: createMeta({
title: loaderData.post.title,
description: loaderData.post.excerpt,
url: `https://myapp.com/posts/${loaderData.post.id}`,
image: loaderData.post.coverImage,
type: 'article',
siteName: 'My App',
twitterSite: '@myapp',
}),
}),
component: PostPage,
})
```

### Extending with Custom Meta

You can extend the generated meta with custom tags:

```tsx
import { createMeta } from '@tanstack/meta'

head: () => ({
meta: createMeta({
title: 'My Page',
description: 'Page description',
extend: [
{ name: 'author', content: 'John Doe' },
{ name: 'keywords', content: 'react, tanstack, router' },
],
}),
})
```

### Using Individual Builders

For more control, use the individual builder functions through the `meta` namespace:

```tsx
import { meta } from '@tanstack/meta'

head: () => ({
meta: [
...meta.title('My Page', '%s | My App'), // With template
...meta.description('Page description'),
...meta.robots({ index: true, follow: true }),
...meta.openGraph({
title: 'My Page',
type: 'website',
image: 'https://myapp.com/og.jpg',
}),
...meta.twitter({
card: 'summary_large_image',
site: '@myapp',
}),
],
})
```

### Available Builders

| Builder | Description |
|---------|-------------|
| `meta.title(value, template?)` | Page title with optional template |
| `meta.description(content)` | Meta description |
| `meta.charset()` | UTF-8 charset |
| `meta.viewport(content?)` | Viewport configuration |
| `meta.robots(config)` | Robot directives (index, follow, etc.) |
| `meta.canonical(href)` | Canonical URL link |
| `meta.alternate(links)` | Alternate language links |
| `meta.openGraph(config)` | Open Graph meta tags |
| `meta.twitter(config)` | Twitter Card meta tags |
| `meta.themeColor(color)` | Theme color (supports light/dark) |
| `meta.verification(config)` | Search engine verification codes |

## Structured Data (JSON-LD)

Structured data helps search engines understand your content and can enable rich results in search:
Structured data helps search engines understand your content and can enable rich results in search.

### Manual Approach

```tsx
export const Route = createFileRoute('/posts/$postId')({
Expand Down Expand Up @@ -152,6 +279,106 @@ export const Route = createFileRoute('/posts/$postId')({
})
```

### Using @tanstack/meta/json-ld

The `@tanstack/meta/json-ld` subpath provides type-safe builders for common Schema.org types:

```tsx
import { createFileRoute } from '@tanstack/react-router'
import { createMeta } from '@tanstack/meta'
import { jsonLd } from '@tanstack/meta/json-ld'

export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
head: ({ loaderData }) => ({
meta: [
...createMeta({
title: loaderData.post.title,
description: loaderData.post.excerpt,
url: `https://myapp.com/posts/${loaderData.post.id}`,
image: loaderData.post.coverImage,
type: 'article',
}),
...jsonLd.article({
headline: loaderData.post.title,
description: loaderData.post.excerpt,
author: loaderData.post.author.name,
datePublished: loaderData.post.publishedAt,
image: loaderData.post.coverImage,
}),
],
}),
component: PostPage,
})
```

### Available JSON-LD Builders

| Builder | Description |
|---------|-------------|
| `jsonLd.website(config)` | WebSite schema with optional search action |
| `jsonLd.organization(config)` | Organization with logo, socials, address |
| `jsonLd.person(config)` | Person schema |
| `jsonLd.article(config)` | Article, BlogPosting, NewsArticle |
| `jsonLd.product(config)` | Product with price, availability, ratings |
| `jsonLd.breadcrumbs(items)` | BreadcrumbList for navigation |
| `jsonLd.faq(items)` | FAQPage with questions and answers |
| `jsonLd.event(config)` | Event with location, dates |
| `jsonLd.localBusiness(config)` | LocalBusiness, Restaurant, Store |
| `jsonLd.softwareApp(config)` | SoftwareApplication |
| `jsonLd.video(config)` | VideoObject |
| `jsonLd.recipe(config)` | Recipe with ingredients, instructions |
| `jsonLd.course(config)` | Course with provider |
| `jsonLd.howTo(config)` | HowTo with steps |
| `jsonLd.create(schema)` | Raw JSON-LD for any schema type |

### Product Page Example

```tsx
import { createMeta } from '@tanstack/meta'
import { jsonLd } from '@tanstack/meta/json-ld'

export const Route = createFileRoute('/products/$productId')({
loader: async ({ params }) => {
const product = await fetchProduct(params.productId)
return { product }
},
head: ({ loaderData }) => ({
meta: [
...createMeta({
title: loaderData.product.name,
description: loaderData.product.description,
url: `https://myapp.com/products/${loaderData.product.id}`,
image: loaderData.product.image,
type: 'product',
}),
...jsonLd.product({
name: loaderData.product.name,
description: loaderData.product.description,
image: loaderData.product.image,
price: loaderData.product.price,
currency: 'USD',
availability: 'InStock',
brand: loaderData.product.brand,
rating: {
value: loaderData.product.rating,
count: loaderData.product.reviewCount,
},
}),
...jsonLd.breadcrumbs([
{ name: 'Home', url: 'https://myapp.com' },
{ name: 'Products', url: 'https://myapp.com/products' },
{ name: loaderData.product.name, url: `https://myapp.com/products/${loaderData.product.id}` },
]),
],
}),
component: ProductPage,
})
```

## Server-Side Rendering

SSR is enabled by default in TanStack Start. This ensures that search engine crawlers receive fully rendered HTML content, which is critical for SEO.
Expand Down
1 change: 1 addition & 0 deletions examples/react/start-basic-react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"start": "pnpx srvx --prod -s ../client dist/server/server.js"
},
"dependencies": {
"@tanstack/meta": "workspace:*",
"@tanstack/react-query": "^5.90.0",
"@tanstack/react-query-devtools": "^5.90.0",
"@tanstack/react-router": "^1.144.0",
Expand Down
24 changes: 9 additions & 15 deletions examples/react/start-basic-react-query/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,25 @@ import {
} from '@tanstack/react-router'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import { createMeta } from '@tanstack/meta'
import * as React from 'react'
import type { QueryClient } from '@tanstack/react-query'
import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
import { NotFound } from '~/components/NotFound'
import appCss from '~/styles/app.css?url'
import { seo } from '~/utils/seo'

export const Route = createRootRouteWithContext<{
queryClient: QueryClient
}>()({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
...seo({
title:
'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework',
description: `TanStack Start is a type-safe, client-first, full-stack React framework. `,
}),
],
meta: createMeta({
title:
'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework',
description:
'TanStack Start is a type-safe, client-first, full-stack React framework.',
twitterCreator: '@tanaborlabs',
twitterSite: '@tanaborlabs',
}),
links: [
{ rel: 'stylesheet', href: appCss },
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ErrorComponent, Link, createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from '@tanstack/react-query'
import { createMeta } from '@tanstack/meta'
import { postQueryOptions } from '../utils/posts'
import type { ErrorComponentProps } from '@tanstack/react-router'
import { NotFound } from '~/components/NotFound'
Expand All @@ -12,10 +13,23 @@ export const Route = createFileRoute('/posts/$postId')({

return {
title: data.title,
body: data.body,
id: data.id,
}
},
head: ({ loaderData }) => ({
meta: loaderData ? [{ title: loaderData.title }] : undefined,
meta: loaderData
? createMeta({
title: loaderData.title,
description: loaderData.body.slice(0, 160),
// In a real app, you'd use an actual URL
url: `https://example.com/posts/${loaderData.id}`,
titleTemplate: '%s | TanStack Start',
// Disable charset/viewport since root already provides them
charset: false,
viewport: false,
})
: undefined,
}),
errorComponent: PostErrorComponent,
notFoundComponent: () => {
Expand Down
33 changes: 0 additions & 33 deletions examples/react/start-basic-react-query/src/utils/seo.ts

This file was deleted.

Loading
Loading