# SEO Configuration for the Tanstack Start Supabase Starter Kit

> Configure sitemaps, metadata, structured data, and search engine optimization for your Makerkit SaaS application.

*Canonical: https://makerkit.dev/docs/tanstack-supabase/development/seo*

---

SEO in Makerkit starts with the TanStack Router route `head` option for page-level metadata, an auto-generated sitemap at `/sitemap.xml`, and a `/robots.txt` server route. The kit handles technical SEO out of the box, so you can focus on content quality and backlink strategy.

{% sequence title="SEO Configuration" description="Set up search engine optimization for your SaaS" %}

[Configure page metadata](#page-metadata)

[Customize the sitemap](#sitemap-configuration)

[Add structured data](#structured-data)

[Submit to Google Search Console](#google-search-console)

{% /sequence %}

## Page Metadata

### Route head option

Set page-level SEO with the route `head` option. It returns `meta` and `links` arrays that TanStack Router merges into the document `<head>`:

```tsx {% title="apps/web/src/routes/_marketing/pricing.tsx" %}
import { createFileRoute } from '@tanstack/react-router';

export const Route = createFileRoute('/_marketing/pricing')({
  head: () => ({
    meta: [
      { title: 'Pricing | Your SaaS Name' },
      {
        name: 'description',
        content:
          'Simple, transparent pricing. Start free, upgrade when you need more.',
      },
      { property: 'og:title', content: 'Pricing | Your SaaS Name' },
      {
        property: 'og:description',
        content: 'Simple, transparent pricing for teams of all sizes.',
      },
      { property: 'og:image', content: '/images/og/pricing.png' },
      { property: 'og:type', content: 'website' },
      { name: 'twitter:card', content: 'summary_large_image' },
      { name: 'twitter:title', content: 'Pricing | Your SaaS Name' },
      {
        name: 'twitter:description',
        content: 'Simple, transparent pricing for teams of all sizes.',
      },
      { name: 'twitter:image', content: '/images/og/pricing.png' },
    ],
  }),
  component: PricingPage,
});

function PricingPage() {
  // ...
}
```

### Dynamic Metadata

For pages with dynamic content, fetch the data in the route `loader` and derive `head` from the loaded data:

```tsx {% title="apps/web/src/routes/_marketing/blog/$slug.tsx" %}
import { createFileRoute } from '@tanstack/react-router';

import { createCmsClient } from '@kit/cms';

export const Route = createFileRoute('/_marketing/blog/$slug')({
  loader: async ({ params }) => {
    const cms = await createCmsClient();
    const post = await cms.getContentBySlug({
      slug: params.slug,
      collection: 'posts',
    });

    return { post };
  },
  head: ({ loaderData }) => {
    const post = loaderData?.post;

    return {
      meta: [
        { title: `${post?.title} | Your SaaS Blog` },
        { name: 'description', content: post?.description },
        { property: 'og:title', content: post?.title },
        { property: 'og:description', content: post?.description },
        { property: 'og:image', content: post?.image },
        { property: 'og:type', content: 'article' },
      ],
    };
  },
  component: BlogPostPage,
});
```

### Global Metadata

Default document metadata lives in `apps/web/src/lib/root-metadata.ts`. The `getRootHead` helper builds the base `meta` + `links`, and the root route at `apps/web/src/routes/__root.tsx` returns them from its `head()`. Individual routes override these per page:

```tsx {% title="apps/web/src/lib/root-metadata.ts" %}
import appConfig from '#/config/app.config.ts';

export function getRootHead() {
  const title = appConfig.title;
  const description = appConfig.description;

  return {
    meta: [
      { charSet: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { title },
      { name: 'description', content: description },
      { name: 'application-name', content: appConfig.name },
      { property: 'og:title', content: title },
      { property: 'og:description', content: description },
      { property: 'og:url', content: appConfig.url },
      { property: 'og:site_name', content: appConfig.name },
      { name: 'twitter:card', content: 'summary_large_image' },
      { name: 'twitter:title', content: title },
      { name: 'twitter:description', content: description },
    ],
    links: [{ rel: 'icon', href: '/images/favicon/favicon.ico' }],
  };
}
```

## Sitemap Configuration

Makerkit auto-generates a sitemap at `/sitemap.xml`. The configuration lives in the TanStack Start server route at `apps/web/src/routes/sitemap[.]xml.ts` (the bracketed filename escapes the dot so the route matches `/sitemap.xml`).

### Adding Static Pages

Add new pages to the `STATIC_PATHS` array:

```tsx {% title="apps/web/src/routes/sitemap[.]xml.ts" %}
// Static, publicly-indexable marketing paths. CMS-backed blog posts and docs
// pages are appended dynamically below.
const STATIC_PATHS = [
  '/',
  '/pricing',
  '/faq',
  '/blog',
  '/docs',
  '/contact',
  '/about', // Add new pages
  '/features',
  '/privacy-policy',
  '/terms-of-service',
  '/cookie-policy',
];
```

The route builds the XML from these paths (prefixed with `appConfig.url`) and returns it from the `GET` server handler.

### Dynamic Content

Blog posts and documentation pages are appended automatically. The CMS integration handles this inside the sitemap builder:

```tsx
// Blog posts and docs are added automatically
const [posts, docs] = await Promise.all([
  client.getContentItems({ collection: 'posts', content: false }),
  client.getContentItems({ collection: 'documentation', content: false }),
]);

posts.items.forEach((item) => paths.push(`/blog/${item.slug}`));
docs.items.forEach((item) => paths.push(`/docs/${item.slug}`));
```

### Excluding Pages

Exclude pages from the sitemap by not including them in `STATIC_PATHS`. For pages that should not be indexed at all, add a `robots` meta tag from the route `head`:

```tsx
head: () => ({
  meta: [{ name: 'robots', content: 'noindex, nofollow' }],
}),
```

## Structured Data

Add JSON-LD structured data for rich search results. Render a `<script type="application/ld+json">` tag inside your route component.

### Organization Schema

Add to your home page component:

```tsx {% title="apps/web/src/routes/_marketing/index.tsx" %}
// JSON-LD structured data using a script tag

function Home() {
  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify({
          '@context': 'https://schema.org',
          '@type': 'Organization',
          name: 'Your SaaS Name',
          url: 'https://yoursaas.com',
          logo: 'https://yoursaas.com/logo.png',
          sameAs: [
            'https://twitter.com/yoursaas',
            'https://github.com/yoursaas',
          ],
          }),
        }}
      />
      {/* Page content */}
    </>
  );
}
```

### Product Schema

Add to your pricing page:

```tsx {% title="apps/web/src/routes/_marketing/pricing.tsx" %}
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify({
      '@context': 'https://schema.org',
      '@type': 'SoftwareApplication',
      name: 'Your SaaS Name',
      applicationCategory: 'BusinessApplication',
      offers: {
        '@type': 'AggregateOffer',
        lowPrice: '0',
        highPrice: '99',
        priceCurrency: 'USD',
        offerCount: 3,
      },
    }),
  }}
/>
```

### FAQ Schema

Use the Markdoc FAQ node for automatic FAQ schema:

```markdown
{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "How do I get started?", "answer": "Sign up for a free account..."},
     {"question": "Can I cancel anytime?", "answer": "Yes, you can cancel..."}
   ]
/%}
```

### Article Schema

Add to blog posts:

```tsx
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify({
      '@context': 'https://schema.org',
      '@type': 'Article',
      headline: post.title,
      description: post.description,
      image: post.image,
      datePublished: post.publishedAt,
      dateModified: post.updatedAt,
      author: {
        '@type': 'Person',
        name: post.author,
      },
    }),
  }}
/>
```

## Robots.txt

The robots.txt is served by the TanStack Start server route at `apps/web/src/routes/robots[.]txt.ts`:

```typescript {% title="apps/web/src/routes/robots[.]txt.ts" %}
import { createFileRoute } from '@tanstack/react-router';

import appConfig from '#/config/app.config.ts';

export const Route = createFileRoute('/robots.txt')({
  server: {
    handlers: {
      GET: () => {
        const body = [
          'User-agent: *',
          'Allow: /',
          'Disallow: /dashboard/',
          'Disallow: /admin/',
          'Disallow: /api/',
          '',
          `Sitemap: ${appConfig.url}/sitemap.xml`,
          '',
        ].join('\n');

        return new Response(body, {
          headers: { 'content-type': 'text/plain' },
        });
      },
    },
  },
});
```

The sitemap URL is derived from `appConfig.url`, so set that to your production domain.

## Google Search Console

### Verification

1. Go to [Google Search Console](https://search.google.com/search-console)
2. Add your property (URL prefix method)
3. Choose verification method:
   - **HTML tag**: Add a meta tag to the root head in `apps/web/src/lib/root-metadata.ts`
   - **HTML file**: Upload to `apps/web/public/`

```tsx
// HTML tag verification — add to the meta array in getRootHead()
{ name: 'google-site-verification', content: 'your-verification-code' },
```

### Submit Sitemap

After verification:

1. Navigate to **Sitemaps** in Search Console
2. Enter `sitemap.xml` in the input field
3. Click **Submit**

Google will crawl and index your sitemap within a few days.

### Monitor Indexing

Check Search Console regularly for:

- **Coverage**: Pages indexed vs. excluded
- **Enhancements**: Structured data validation
- **Core Web Vitals**: Performance metrics
- **Mobile Usability**: Mobile-friendly issues

## SEO Best Practices

### Content Quality

Content quality matters more than technical SEO. Focus on:

- **Helpful content**: Solve problems your customers search for
- **Unique value**: Offer insights competitors don't have
- **Regular updates**: Keep content fresh and accurate
- **Comprehensive coverage**: Answer related questions

### Keyword Strategy

| Element | Recommendation |
|---------|----------------|
| Title | Primary keyword near the beginning |
| Description | Include keyword naturally, focus on click-through |
| H1 | One per page, include primary keyword |
| URL | Short, descriptive, include keyword |
| Content | Use variations naturally, don't stuff |

### Image Optimization

```tsx
<img
  src="/images/feature-screenshot.webp"
  alt="Dashboard showing project analytics with team activity"
  width={1200}
  height={630}
  loading="lazy"
/>
```

- Use WebP format for better compression
- Include descriptive alt text with keywords
- Use descriptive filenames (`project-dashboard.webp` not `img1.webp`)
- Size images appropriately for their display size

### Internal Linking

Link between related content:

```tsx
import { Link } from '@tanstack/react-router';

// In your blog post about authentication
<p>
  Learn more about{' '}
  <Link to="/docs/$slug" params={{ slug: 'authentication/setup' }}>
    setting up authentication
  </Link>{' '}
  in our documentation.
</p>
```

### Page Speed

Makerkit is optimized for performance out of the box:

- Automatic code splitting via Vite + TanStack Router
- Responsive images with `<img>` and modern formats (WebP/AVIF)
- Self-hosted font optimization with `@fontsource-variable/*` (loaded via CSS)
- Server-rendered marketing pages

Check your scores with [PageSpeed Insights](https://pagespeed.web.dev/).

## Backlinks

Backlinks remain the strongest ranking factor. Strategies that work:

| Strategy | Effort | Impact |
|----------|--------|--------|
| Create linkable content (guides, tools, research) | High | High |
| Guest posting on relevant blogs | Medium | Medium |
| Product directories (Product Hunt, etc.) | Low | Medium |
| Open source contributions | Medium | Medium |
| Podcast appearances | Medium | Medium |

Focus on quality over quantity. One link from a high-authority site beats dozens of low-quality links.

## Timeline Expectations

SEO takes time. Typical timelines:

| Milestone | Timeline |
|-----------|----------|
| Initial indexing | 1-2 weeks |
| Rankings for low-competition terms | 1-3 months |
| Rankings for medium-competition terms | 3-6 months |
| Rankings for high-competition terms | 6-12+ months |

Keep creating content and building backlinks. Results compound over time.

## Related Resources

- [Marketing Pages](/docs/tanstack-supabase/development/marketing-pages) for building optimized landing pages
- [CMS Setup](/docs/tanstack-supabase/content/cms) for content marketing
- [App Configuration](/docs/tanstack-supabase/configuration/application-configuration) for base URL and metadata settings
