SEO Configuration for the Tanstack Start Supabase Starter Kit

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

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.

SEO Configuration

Set up search engine optimization for your SaaS

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

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:

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:

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:

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:

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

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:

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:

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:

{% 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:

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

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

ElementRecommendation
TitlePrimary keyword near the beginning
DescriptionInclude keyword naturally, focus on click-through
H1One per page, include primary keyword
URLShort, descriptive, include keyword
ContentUse variations naturally, don't stuff

Image Optimization

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

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.

Backlinks remain the strongest ranking factor. Strategies that work:

StrategyEffortImpact
Create linkable content (guides, tools, research)HighHigh
Guest posting on relevant blogsMediumMedium
Product directories (Product Hunt, etc.)LowMedium
Open source contributionsMediumMedium
Podcast appearancesMediumMedium

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:

MilestoneTimeline
Initial indexing1-2 weeks
Rankings for low-competition terms1-3 months
Rankings for medium-competition terms3-6 months
Rankings for high-competition terms6-12+ months

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