Skip to main content
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
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",
  };
}
1

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

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
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];
}
1

Set baseUrl

Replace https://demo.getsabo.com with your production domain (must match robots.ts).
2

Add dynamic routes

The example shows blog/changelog from MDX. Add other collections (products, docs, etc.) following the same pattern.
3

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.

Site-wide metadata (Open Graph, Twitter, robots)

File: src/app/layout.tsx
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 },
  },
};
1

Set metadataBase

Update metadataBase to your production domain. This becomes the base for all relative URLs in meta tags.
2

Customize all fields

Update title, description, keywords, and social tags (openGraph, twitter) to match your brand.
3

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.

Route-level metadata

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.
src/app/layout.tsx
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"),
  }}
/>
1

Customize fields

Update name, url, logo, and description. Add social profile URLs to sameAs array.
2

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

1

Manual checks

  • Visit /robots.txt and /sitemap.xml
  • View page source for meta tags and JSON‑LD
2

Tools

  • Google Rich Results Test (structured data)
  • OpenGraph Preview tools (OG tags)
  • Twitter Card Validator
  • Search Console/Bing Webmaster for submission and coverage
3

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.