Spacerr includes SEO foundations for public routes, blog posts, social previews, crawlers, RSS readers, and AI discovery. Private product surfaces are kept out of the public index.
What It Includes
The SEO layer includes:
- Route metadata with titles, descriptions, keywords, and canonical URLs.
- Open Graph and Twitter card metadata.
- JSON LD structured data.
- Sitemap generation.
- Robots rules.
- RSS feed generation for blog posts.
llms.txtfor AI crawler discovery.- Noindex metadata for private surfaces.
- Blog post metadata and structured data.
Route Metadata
Route metadata lives in src/features/seo/get-route-metadata.ts.
export async function getRouteMetadata(
routeKey: RouteSeoKey,
canonical: string
): Promise<Metadata> {
const t = await getTranslations("seo.routes")
const title = t(`${routeKey}.title`)
const description = t(`${routeKey}.description`)
return {
title,
description,
alternates: {
canonical,
},
robots: {
index: true,
follow: true,
},
}
}Route copy lives in src/i18n/en/seo.json. Change that file when you need a public route title, description, or keyword update.
{
"routes": {
"root": {
"title": "AI SaaS Starter Kit for Next.js",
"description": "Ship your AI SaaS in days, not months.",
"keywords": "AI SaaS starter kit, Next.js AI template, Vercel AI SDK starter"
}
}
}Structured Data
Structured data lives in src/features/seo/structured-data.tsx.
export function StructuredData({ description = SiteConfig.description }: StructuredDataProps) {
const organization = {
"@context": "https://schema.org",
"@type": "Organization",
name: SiteConfig.name,
url: WebRoutes.product.withBaseUrl(),
description,
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: escapeJsonLd(JSON.stringify(organization)),
}}
/>
)
}Use JSON LD for public pages that qualify for rich results. Keep private dashboard pages out of structured data.
Sitemap And Robots
The sitemap lives in src/app/sitemap.ts.
const staticRoutes: MetadataRoute.Sitemap = [
{
url: WebRoutes.product.withBaseUrl(),
lastModified: now,
changeFrequency: "weekly",
priority: 1,
},
]
const blogRoutes: MetadataRoute.Sitemap = posts.map((post) => ({
url: getBlogPostCanonicalUrl(post.slug),
lastModified: post.updatedAt,
changeFrequency: "monthly",
priority: 0.7,
}))Robots rules live in src/app/robots.ts.
const PRIVATE_ROBOTS_PATHS = [
"/api/",
...withExactAndTrailingSlash(WebRoutes.checkout.path),
...withExactAndTrailingSlash(WebRoutes.admin.path),
...withExactAndTrailingSlash(WebRoutes.dashboardLibrary.path),
] as constAdd public indexable routes to the sitemap. Add private or sensitive routes to robots disallow lists and noindex metadata.
RSS And AI Discovery
The RSS route lives in src/app/rss.xml/route.ts. It reads published blog posts and emits feed XML.
/rss.xmlThe AI discovery file lives in src/app/llms.txt/route.ts and is built by src/features/seo/llms-txt.ts.
/llms.txtUse llms.txt to explain public product surfaces to AI crawlers. Do not include private dashboard states, API routes, admin screens, or user data.
Blog SEO
Blog SEO lives with the blog feature.
src/features/blog/utils/blog-post-seo.ts
src/features/blog/utils/blog-post-json-ld.tsx
src/app/rss.xml/route.ts
src/app/sitemap.tsBlog posts use the stored slug and post fields to generate canonical URLs, Open Graph images, Twitter cards, RSS entries, and sitemap entries.
Where To Customize
Use these files first:
src/i18n/en/seo.jsonfor public route metadata copy.src/lib/site.config.tsfor site name, description, author, social handles, and default product metadata.src/features/seo/get-route-metadata.tsfor route metadata assembly.src/features/seo/metadata.tsfor root metadata assembly.src/features/seo/structured-data.tsxfor JSON LD.src/features/seo/noindex-metadata.tsfor private page metadata.src/app/sitemap.tsfor sitemap entries.src/app/robots.tsfor crawler rules.src/features/seo/llms-txt.tsfor AI discovery content.src/features/blog/utils/blog-post-seo.tsfor blog metadata.