Modern SEO and GEO in TanStack Start: Dynamic Metadata, JSON-LD, and llms.txt

Published: August 31, 2026

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, 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:

// 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):

// 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 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:

// 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:

// 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 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:

// 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:

// 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:

// ❌ 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:

// 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:

# 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.