---
title: Modern SEO and GEO in TanStack Start: Dynamic Metadata, JSON-LD, and llms.txt
publishedAt: 2026-08-31
summary: A practical, step-by-step guide to implementing modern SEO and Generative Engine Optimization (GEO) in TanStack Start with dynamic metadata, Schema.org JSON-LD, and llms.txt server endpoints.
---

# Modern SEO and Generative Engine Optimization (GEO) in TanStack Start: Dynamic Metadata, JSON-LD, llms.txt, and AI-Ready SSR Architectures

Search optimization has expanded beyond traditional ranking algorithms to include Generative Engine Optimization (GEO)—ensuring that AI search engines (like Perplexity, ChatGPT Search, and Claude) can parse, understand, and accurately cite your content. Building on [TanStack Start](https://tanstack.com/start/latest), a full-stack React framework with built-in server-side rendering (SSR), developers can ship fast, type-safe web applications that serve rich metadata to both web crawlers and AI inference pipelines. This guide provides a practical, step-by-step implementation for dynamic OpenGraph tags, JSON-LD structured data, and `llms.txt` endpoints in TanStack Start.

---

## Understanding Modern SEO vs. Generative Engine Optimization (GEO)

Generative Engine Optimization (GEO) focuses on structuring web content so large language models can ingest, synthesize, and cite source materials with minimal token overhead and zero hallucination. While traditional SEO prioritizes search engine indexing via HTML `<meta>` tags and OpenGraph protocols, GEO optimizes for semantic clarity through structured JSON-LD schemas and clean markdown endpoints like `llms.txt`. Combining both approaches in a server-rendered TanStack Start application ensures high visibility across both classical search engines and modern conversational AI engines.

| Feature | Traditional SEO | Generative Engine Optimization (GEO) |
| :--- | :--- | :--- |
| **Primary Target** | Googlebot, Bingbot, Social Crawlers | Perplexity, GPTBot, ClaudeBot, Custom RAG pipelines |
| **Key Formats** | Meta tags, OpenGraph, Canonical URLs | Schema.org JSON-LD, `llms.txt`, Semantic HTML |
| **Delivery Mechanism** | Server-Rendered HTML `<head>` | SSR HTML + Plaintext/Markdown Endpoints |
| **Success Metric** | Search Result Page (SERP) Rankings | Direct AI Citations, Grounded Answers |

---

## Step 1: Configuring Base Site Metadata in the Root Route

The root route defines the global HTML shell, viewport parameters, and fallback metadata for your entire TanStack Start application. By configuring the `head` function in `createRootRouteWithContext` or `createRootRoute`, you ensure every server-rendered page starts with consistent baseline metadata that child routes can selectively override.

In your root route (`app/routes/__root.tsx`), set up your global charset, viewport, base OpenGraph tags, and canonical domain fallbacks:

```tsx
// app/routes/__root.tsx
import {
  Outlet,
  ScrollRestoration,
  createRootRouteWithContext,
  HeadContent,
  Scripts,
} from '@tanstack/react-router'
import type { ReactNode } from 'react'

const SITE_URL = 'https://example.com'

export const Route = createRootRouteWithContext()({
  head: () => ({
    meta: [
      {
        charSet: 'utf-8',
      },
      {
        name: 'viewport',
        content: 'width=device-width, initial-scale=1',
      },
      {
        name: 'description',
        content: 'Production-ready web applications built with TanStack Start.',
      },
      {
        property: 'og:site_name',
        content: 'Furkan Çetinkaya DevLog',
      },
      {
        property: 'og:type',
        content: 'website',
      },
      {
        name: 'twitter:card',
        content: 'summary_large_image',
      },
      {
        name: 'twitter:creator',
        content: '@cetfu',
      },
    ],
    links: [
      {
        rel: 'canonical',
        href: SITE_URL,
      },
      {
        rel: 'icon',
        href: '/favicon.ico',
      },
    ],
  }),
  component: RootComponent,
})

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

function RootDocument({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  )
}
```

---

## Step 2: Implementing Dynamic Metadata and OpenGraph Tags

Dynamic routes like blog posts, documentation pages, or product catalogs require metadata populated from server loaders. TanStack Start's `createFileRoute` provides a type-safe `head` hook that directly receives `loaderData`, enabling route-specific titles, descriptions, canonical links, and social image cards without client-side rendering lag.

Here is how to implement dynamic metadata on a dynamic post route (`app/routes/posts/$slug.tsx`):

```tsx
// app/routes/posts/$slug.tsx
import { createFileRoute, notFound } from '@tanstack/react-router'

interface PostData {
  slug: string
  title: string
  description: string
  publishedAt: string
  updatedAt: string
  coverImage: string
  author: string
}

const SITE_URL = 'https://example.com'

// Mock fetching function - replace with your database or CMS client
async function fetchPost(slug: string): Promise<PostData> {
  // e.g., const post = await db.post.findUnique({ where: { slug } })
  if (!slug || slug === 'not-found') {
    throw notFound()
  }
  return {
    slug,
    title: 'Modern SEO and GEO in TanStack Start',
    description:
      'A practical guide to dynamic metadata, JSON-LD schemas, and llms.txt in TanStack Start.',
    publishedAt: '2026-08-31T00:00:00Z',
    updatedAt: '2026-08-31',
    coverImage: `${SITE_URL}/og/posts/${slug}.png`,
    author: 'Furkan Çetinkaya',
  }
}

export const Route = createFileRoute('/posts/$slug')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.slug)
    return { post }
  },
  head: ({ loaderData }) => {
    if (!loaderData?.post) {
      return {
        meta: [{ title: 'Post Not Found' }],
      }
    }

    const { post } = loaderData
    const canonicalUrl = `${SITE_URL}/posts/${post.slug}`

    return {
      meta: [
        {
          title: `${post.title} | Furkan Çetinkaya`,
        },
        {
          name: 'description',
          content: post.description,
        },
        // OpenGraph
        {
          property: 'og:title',
          content: post.title,
        },
        {
          property: 'og:description',
          content: post.description,
        },
        {
          property: 'og:type',
          content: 'article',
        },
        {
          property: 'og:url',
          content: canonicalUrl,
        },
        {
          property: 'og:image',
          content: post.coverImage,
        },
        {
          property: 'article:published_time',
          content: post.publishedAt,
        },
        {
          property: 'article:modified_time',
          content: post.updatedAt,
        },
        {
          property: 'article:author',
          content: post.author,
        },
        // Twitter
        {
          name: 'twitter:title',
          content: post.title,
        },
        {
          name: 'twitter:description',
          content: post.description,
        },
        {
          name: 'twitter:image',
          content: post.coverImage,
        },
      ],
      links: [
        {
          rel: 'canonical',
          href: canonicalUrl,
        },
      ],
    }
  },
  component: PostComponent,
})

function PostComponent() {
  const { post } = Route.useLoaderData()

  return (
    <article className="prose max-w-3xl mx-auto py-10 px-4">
      <h1 className="text-4xl font-bold">{post.title}</h1>
      <p className="text-gray-600">{post.description}</p>
      <div className="text-sm text-gray-500 mt-2">
        Published on {new Date(post.publishedAt).toLocaleDateString()} by {post.author}
      </div>
    </article>
  )
}
```

---

## Step 3: Injecting Structured JSON-LD Data for Rich Snippets and AI Crawlers

Structured data formatted according to [Schema.org](https://schema.org) standards gives search engines and AI agents unambiguous facts about entities, authors, and article content. In TanStack Start, structured data can be passed into the route's `head` definition as a script object, ensuring it is rendered into the server-generated HTML markup without causing React hydration mismatches.

Define a reusable schema helper and inject it directly into the route's `scripts` array:

```tsx
// app/utils/seo.ts
export interface ArticleSchemaProps {
  url: string
  title: string
  description: string
  publishedAt: string
  updatedAt: string
  images: string[]
  authorName: string
  authorUrl: string
  publisherName: string
  publisherLogoUrl: string
}

export function generateArticleJsonLd(props: ArticleSchemaProps) {
  return {
    '@context': 'https://schema.org',
    '@type': 'TechArticle',
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': props.url,
    },
    headline: props.title,
    description: props.description,
    image: props.images,
    datePublished: props.publishedAt,
    dateModified: props.updatedAt,
    author: {
      '@type': 'Person',
      name: props.authorName,
      url: props.authorUrl,
    },
    publisher: {
      '@type': 'Organization',
      name: props.publisherName,
      logo: {
        '@type': 'ImageObject',
        url: props.publisherLogoUrl,
      },
    },
  }
}
```

Now update `app/routes/posts/$slug.tsx` to include the JSON-LD script inside the `head` callback:

```tsx
// Inside app/routes/posts/$slug.tsx (excerpt)
import { generateArticleJsonLd } from '~/utils/seo'

export const Route = createFileRoute('/posts/$slug')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.slug)
    return { post }
  },
  head: ({ loaderData }) => {
    if (!loaderData?.post) return { meta: [{ title: 'Post Not Found' }] }

    const { post } = loaderData
    const canonicalUrl = `https://example.com/posts/${post.slug}`

    const structuredData = generateArticleJsonLd({
      url: canonicalUrl,
      title: post.title,
      description: post.description,
      publishedAt: post.publishedAt,
      updatedAt: post.updatedAt,
      images: [post.coverImage],
      authorName: post.author,
      authorUrl: 'https://github.com/cetfu',
      publisherName: 'Furkan Çetinkaya',
      publisherLogoUrl: 'https://example.com/logo.png',
    })

    return {
      meta: [
        { title: `${post.title} | Furkan Çetinkaya` },
        { name: 'description', content: post.description },
      ],
      links: [{ rel: 'canonical', href: canonicalUrl }],
      scripts: [
        {
          type: 'application/ld+json',
          children: JSON.stringify(structuredData),
        },
      ],
    }
  },
  component: PostComponent,
})
```

---

## Step 4: Serving `llms.txt` and `llms-full.txt` via TanStack Start API Routes

The `llms.txt` specification proposed by the [llmstxt.org Standard](https://llmstxt.org) offers a standardized, token-efficient summary file that helps AI agents navigate and consume clean Markdown representations of your site. In TanStack Start, server-side API endpoints are easily created using `createAPIFileRoute`, allowing you to dynamically generate and serve `/llms.txt` and `/llms-full.txt` with proper HTTP caching headers.

### 1. Creating `/llms.txt` (Curated Site Overview)

Create `app/routes/llms[.]txt.ts` to serve an index of available documentation, articles, and high-value project links:

```ts
// app/routes/llms[.]txt.ts
import { createAPIFileRoute } from '@tanstack/start/api'

export const APIRoute = createAPIFileRoute('/llms.txt')({
  GET: async () => {
    const content = `# Furkan Çetinkaya - Developer Documentation & Articles

> Practical engineering guides for Mobile, React Native, and full-stack SSR architectures.

## Core Guides
- [Modern SEO and GEO in TanStack Start](https://example.com/posts/modern-seo-tanstack-start): Complete guide to SSR metadata, JSON-LD, and llms.txt.
- [React Native Native Bridge Architecture](https://example.com/posts/react-native-bridge-guide): Practical Kotlin & Swift native module patterns.
- [Offline-First State Sync](https://example.com/posts/offline-state-sync): Sync engines for mobile and web clients.

## Developer Links
- GitHub: https://github.com/cetfu
- LinkedIn: https://www.linkedin.com/in/cetfu
- Full Markdown Index: https://example.com/llms-full.txt
`

    return new Response(content, {
      status: 200,
      headers: {
        'Content-Type': 'text/plain; charset=utf-8',
        'Cache-Control': 'public, max-age=3600, s-maxage=86400',
      },
    })
  },
})
```

### 2. Creating `/llms-full.txt` (Full Content Feed)

Create `app/routes/llms-full[.]txt.ts` to aggregate full markdown versions of articles for deep reasoning and RAG context injection:

```ts
// app/routes/llms-full[.]txt.ts
import { createAPIFileRoute } from '@tanstack/start/api'

export const APIRoute = createAPIFileRoute('/llms-full.txt')({
  GET: async () => {
    // In production, query your CMS, Markdown files, or database
    const markdownContent = `# Furkan Çetinkaya - Full Engineering Articles Archive

---
## Modern SEO and GEO in TanStack Start
URL: https://example.com/posts/modern-seo-tanstack-start
Published: 2026-08-31

Generative Engine Optimization (GEO) pairs traditional OpenGraph tags with Schema.org JSON-LD and clean Markdown endpoints.
TanStack Start executes route loaders on the server, injecting complete metadata into the initial SSR HTML payload...

---
## React Native Native Bridge Integration (Kotlin & Swift)
URL: https://example.com/posts/react-native-bridge-guide
Published: 2026-08-15

Writing custom native bridges requires handling thread boundaries, memory safety, and event emitters cleanly...
`

    return new Response(markdownContent, {
      status: 200,
      headers: {
        'Content-Type': 'text/plain; charset=utf-8',
        'Cache-Control': 'public, max-age=3600, s-maxage=86400',
      },
    })
  },
})
```

---

## Step 5: Production Best Practices and Common Pitfalls

Deploying an AI-ready SSR architecture requires strict consistency across canonical domains, absolute URL formatting, and crawler permissions. Omitting absolute domain URLs or blocking AI user agents in `robots.txt` can silently degrade your indexing performance across search and generative discovery platforms.

### 1. Always Use Absolute URLs for Social and Schema Metadata
Crawlers like Facebook external hit, Twitterbot, and PerplexityBot will fail to resolve relative paths for OpenGraph images or JSON-LD schema IDs. Always prepend your production domain:

```tsx
// ❌ Bad: Relative paths fail in social link unfurls
{ property: 'og:image', content: '/og-image.png' }

// ✅ Good: Absolute URL
{ property: 'og:image', content: 'https://example.com/og-image.png' }
```

### 2. Configure `robots.txt` for AI Crawlers
Avoid blocking legitimate AI search crawlers if you want your technical content cited in generative answers. Create `app/routes/robots[.]txt.ts`:

```ts
// app/routes/robots[.]txt.ts
import { createAPIFileRoute } from '@tanstack/start/api'

export const APIRoute = createAPIFileRoute('/robots.txt')({
  GET: async () => {
    const robots = `User-agent: *
Allow: /

# Explicit permissions for AI Discovery Engines
User-agent: GPTBot
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: ClaudeBot
Allow: /

Sitemap: https://example.com/sitemap.xml
`
    return new Response(robots, {
      headers: { 'Content-Type': 'text/plain; charset=utf-8' },
    })
  },
})
```

### 3. Verify SSR Markup via Direct CURL Requests
Test that your server output contains the complete `<title>`, OpenGraph `<meta>`, and `<script type="application/ld+json">` tags before client hydration:

```bash
# Verify raw SSR output without executing JavaScript
curl -s https://your-domain.com/posts/modern-seo-tanstack-start | grep -E "(og:|application/ld\+json|<title>)"
```

---

## Frequently Asked Questions (FAQ)

### How do I inject dynamic JSON-LD in TanStack Start routes?
In TanStack Start, return a `scripts` array from the route's `head` configuration callback. The `head` function receives `loaderData`, allowing you to construct valid Schema.org structured data (like `TechArticle` or `BreadcrumbList`) on the server and serialize it into a `<script type="application/ld+json">` tag directly in the initial SSR payload.

### What is the purpose of llms.txt compared to robots.txt?
`robots.txt` is an access control file that tells crawlers which directories they are allowed or forbidden to crawl. In contrast, `llms.txt` is a content curation file that provides LLMs and AI search engines with clean, token-efficient Markdown representations and direct summaries of your key pages, eliminating the overhead of parsing HTML tags, styling, and navigation scripts.

### Does TanStack Start handle social media crawlers and AI bots automatically during SSR?
Yes. Because TanStack Start executes route loaders and renders components on the server before sending the response to the client, web crawlers (such as Googlebot, Bingbot, PerplexityBot, and Twitterbot) receive fully formed HTML with all dynamic `<meta>`, `<link rel="canonical">`, and OpenGraph headers intact without requiring client-side JavaScript execution.

---

### About the Author
**Furkan Çetinkaya** is a Mobile-focused Software Developer specializing in React Native, native bridge integrations (Kotlin & Swift), and supporting backend services. Experienced in maintaining high-impact mobile applications and developer SDKs.
- [GitHub](https://github.com/cetfu)
- [LinkedIn](https://www.linkedin.com/in/cetfu)