This guide shows how to configure search engine indexing for your Sabo app: robots rules, a dynamic sitemap, site-wide metadata (Open Graph/Twitter), and JSON‑LD structured data.
What’s included
- robots:
src/app/robots.ts
- sitemap:
src/app/sitemap.ts
- Site-wide metadata (Open Graph, Twitter, robots):
src/app/layout.tsx
- JSON‑LD (structured data, Organization):
src/app/layout.tsx
Always use your production domain in SEO outputs (e.g., https://yourdomain.com), and restrict indexing in preview environments.
Key concepts at a glance
- robots.txt: A plain-text file that tells crawlers which paths they may crawl or must avoid, and where your sitemap lives.
- sitemap.xml: A machine-readable list of your site’s URLs (and optional metadata like last modified). Helps crawlers discover content faster.
- Site-wide metadata (Open Graph, Twitter, robots): Meta tags that control link previews (title, description, image) and crawling behavior.
- JSON‑LD (structured data): Embedded JSON that describes your pages to search engines (e.g., Organization, BlogPosting) to enable rich results.
robots.txt
File: src/app/robots.ts
import { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: [
"/dashboard/*",
"/sign-in",
"/sign-up",
"/forgot-password",
"/reset-password",
],
},
],
sitemap: "https://demo.getsabo.com/sitemap.xml",
};
}
Set your domain
Change the sitemap URL to your production domain.Visit /robots.txt and confirm the sitemap points to https://yourdomain.com/sitemap.xml.
Control indexing
Add items to disallow to prevent indexing of private/auth pages. For preview deploys, disallow everything or use a noindex robots policy via site metadata (see below).
sitemap.xml
File: src/app/sitemap.ts
import { MetadataRoute } from "next";
import { getAllPosts } from "@/lib/posts";
import { getChangelogEntries } from "@/lib/changelog";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = "https://demo.getsabo.com";
// Static routes (homepage, pricing, contact, etc.)
const staticRoutes: MetadataRoute.Sitemap = [
{ url: baseUrl, lastModified: new Date(), changeFrequency: "weekly", priority: 1.0 },
{ url: `${baseUrl}/pricing`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.9 },
// ... more static routes
];
// Dynamic blog posts from MDX files
const posts = await getAllPosts();
const blogRoutes: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: "monthly" as const, // TypeScript strict mode
priority: 0.7,
}));
// Dynamic changelog entries
const changelogEntries = await getChangelogEntries();
const changelogRoutes: MetadataRoute.Sitemap = changelogEntries.map((entry) => ({
url: `${baseUrl}/changelog/${entry.slug}`,
lastModified: new Date(entry.releaseDate),
changeFrequency: "monthly" as const,
priority: 0.6,
}));
return [...staticRoutes, ...blogRoutes, ...changelogRoutes];
}
Set baseUrl
Replace https://demo.getsabo.com with your production domain (must match robots.ts).
Add dynamic routes
The example shows blog/changelog from MDX. Add other collections (products, docs, etc.) following the same pattern.
Test the output
Visit /sitemap.xml and verify all routes appear with correct timestamps.
The file is sitemap.ts but Next.js serves it as /sitemap.xml automatically. Use as const for TypeScript strict mode.
File: src/app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://demo.getsabo.com"),
title: {
default: "Sabo - Modern Next.js SaaS Boilerplate",
template: "%s | Sabo", // Page title | Site name
},
description: "A modern, production-ready Next.js SaaS boilerplate...",
keywords: ["Next.js", "React", "TypeScript", "SaaS", "Boilerplate"],
openGraph: {
type: "website",
locale: "en_US",
url: "https://demo.getsabo.com",
siteName: "Sabo",
title: "Sabo - Modern Next.js SaaS Boilerplate",
description: "A modern, production-ready Next.js SaaS boilerplate...",
images: [{ url: "/og/homepage.png", width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
title: "Sabo - Modern Next.js SaaS Boilerplate",
description: "A modern, production-ready Next.js SaaS boilerplate...",
images: ["/og/homepage.png"],
creator: "@sabo",
},
robots: {
index: true,
follow: true,
googleBot: { index: true, follow: true },
},
};
Set metadataBase
Update metadataBase to your production domain. This becomes the base for all relative URLs in meta tags.
Customize all fields
Update title, description, keywords, and social tags (openGraph, twitter) to match your brand.
Add OG images
Create 1200×630 images in public/og/. Keep titles/descriptions consistent across openGraph and twitter.
For preview/staging environments, conditionally set robots.index: false to prevent accidental indexing.
For specific pages, you can export page-level metadata (or generateMetadata) to override the defaults.
// src/app/blog/[slug]/page.tsx (example)
export const metadata = {
title: "Post Title",
description: "Short summary for this post.",
alternates: { canonical: "/blog/post-slug" },
};
Use alternates.canonical to avoid duplicate content when pages can be reached by multiple URLs.
JSON‑LD (Structured data)
File: src/app/layout.tsx (Organization). You can add per‑page JSON‑LD (e.g., BlogPosting) in page files.
import type { Organization, WithContext } from "schema-dts";
// Inside RootLayout component
const jsonLd: WithContext<Organization> = {
"@context": "https://schema.org",
"@type": "Organization",
name: "Sabo",
url: "https://demo.getsabo.com",
logo: "https://demo.getsabo.com/logo.png",
description: "A modern, production-ready Next.js SaaS boilerplate...",
sameAs: [], // Add social profiles: ["https://twitter.com/...", "https://linkedin.com/..."]
};
// In <head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
Customize fields
Update name, url, logo, and description. Add social profile URLs to sameAs array.
Add to layout
Place the script in <head> of your root layout. The .replace(/</g, "\\u003c") prevents script injection.
For blog posts, add page-level JSON-LD with @type: "BlogPosting" including author, datePublished, and image.
Verification and testing
Manual checks
- Visit
/robots.txt and /sitemap.xml
- View page source for meta tags and JSON‑LD
Tools
- Google Rich Results Test (structured data)
- OpenGraph Preview tools (OG tags)
- Twitter Card Validator
- Search Console/Bing Webmaster for submission and coverage
Common issues
- Wrong domain in
metadataBase or sitemap
- Preview builds accidentally indexed (set
noindex)
- OG images missing or wrong path
Deployed site exposes correct robots and sitemap, has consistent OG/Twitter meta, and validates in rich results tests.