Sitemap
How the XML sitemap is generated in Nebutra-Sailor, how to include dynamic blog posts from Sanity, and how to submit it to Google Search Console.
How the sitemap works
The sitemap is generated by the Next.js App Router file-system convention. A sitemap.ts file at the root of the app directory exports an async function that returns an array of URL objects. Next.js converts this to a valid XML sitemap served at /sitemap.xml.
File location:
apps/landing/src/app/sitemap.tsStatic sitemap
For sites with only static pages, export an array of URL objects:
import type { MetadataRoute } from "next";
const BASE_URL = "https://nebutra.com";
const LOCALES = ["en", "zh", "ja", "ko", "de", "fr", "es"];
export default function sitemap(): MetadataRoute.Sitemap {
const staticPages = [
"",
"/pricing",
"/changelog",
"/about",
"/contact",
"/blog",
];
const entries: MetadataRoute.Sitemap = [];
for (const locale of LOCALES) {
for (const path of staticPages) {
entries.push({
url: `${BASE_URL}/${locale}${path}`,
lastModified: new Date(),
changeFrequency: path === "" ? "weekly" : "monthly",
priority: path === "" ? 1.0 : 0.8,
});
}
}
return entries;
}Dynamic sitemap with Sanity blog posts
When FEATURE_BLOG=true, fetch blog post slugs from Sanity and add them to the sitemap:
import type { MetadataRoute } from "next";
import { getAllPostSlugs } from "@/lib/sanity";
const BASE_URL = "https://nebutra.com";
const LOCALES = ["en", "zh", "ja", "ko", "de", "fr", "es"];
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const staticPages = ["", "/pricing", "/changelog", "/about", "/blog"];
const staticEntries: MetadataRoute.Sitemap = LOCALES.flatMap((locale) =>
staticPages.map((path) => ({
url: `${BASE_URL}/${locale}${path}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: path === "" ? 1.0 : 0.7,
}))
);
// Dynamic blog posts from Sanity
const posts = await getAllPostSlugs(); // returns [{ slug, updatedAt, locale }]
const blogEntries: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${BASE_URL}/${post.locale}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: "monthly" as const,
priority: 0.6,
}));
return [...staticEntries, ...blogEntries];
}Make sitemap() async only when you need to fetch dynamic data. For fully static sites, a synchronous function avoids an unnecessary async boundary.
Robots.txt
The robots.ts file controls which paths crawlers can access. It is separate from the sitemap but typically references it:
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/_next/", "/studio/"],
},
],
sitemap: "https://nebutra.com/sitemap.xml",
};
}Never include authenticated app routes (e.g., app.nebutra.com/dashboard) in the sitemap. Only the public landing page (nebutra.com) should be indexed. The apps/web app should have its own robots.ts that disallows all crawlers.
Verifying the sitemap locally
pnpm --filter @nebutra/landing dev
# Visit: http://localhost:3002/sitemap.xml
# Visit: http://localhost:3002/robots.txtThe sitemap is rendered on demand in development and cached at the CDN edge in production.
Submitting to Google Search Console
In Google Search Console, add your property (https://nebutra.com) and verify ownership via DNS TXT record or the HTML file method.
In Search Console, go to Sitemaps in the left sidebar. Enter sitemap.xml in the URL field and click Submit.
After submission, Search Console shows how many URLs have been discovered and indexed. It typically takes 24–72 hours for a new sitemap to be processed.
Google recrawls sitemaps automatically, but after a major release (new locale, large number of new blog posts), manually re-submitting speeds up re-indexing.
Sitemap size limits
Google supports sitemaps of up to 50,000 URLs or 50 MB (uncompressed). If your sitemap exceeds this:
- Split it into multiple sitemaps (e.g.,
sitemap-static.xml,sitemap-blog.xml). - Create a sitemap index file at
/sitemap.xmlthat references the child sitemaps.
When you need a sitemap index, split your routes across multiple sitemap.ts files using Next.js's built-in generateSitemaps() export:
// app/sitemap.ts ← becomes the index at /sitemap.xml
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
// static + short-lived pages only
return [
{ url: "https://nebutra.com", lastModified: new Date() },
{ url: "https://nebutra.com/pricing", lastModified: new Date() },
];
}// app/blog/sitemap.ts ← /blog/sitemap.xml
import type { MetadataRoute } from "next";
import { getAllPostSlugs } from "@/lib/blog";
export async function generateSitemaps() {
// Return one entry per 5,000-URL batch
const slugs = await getAllPostSlugs();
const batches = Math.ceil(slugs.length / 5000);
return Array.from({ length: batches }, (_, i) => ({ id: i }));
}
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
const slugs = await getAllPostSlugs();
const page = slugs.slice(id * 5000, (id + 1) * 5000);
return page.map((slug) => ({
url: `https://nebutra.com/blog/${slug}`,
lastModified: new Date(),
}));
}Next.js automatically generates a sitemap index at /sitemap.xml that references /blog/sitemap/0.xml, /blog/sitemap/1.xml, etc. Register the index URL in Google Search Console.
Related
SEO Overview
Full overview of built-in SEO features including OG images and structured data.
Meta Tags
Static and dynamic metadata — titles, descriptions, OG, Twitter cards.
Feature Flags
Enabling the blog and changelog that add dynamic URLs to the sitemap.
Deployment
Production domain configuration for canonical URLs and sitemap base URL.
How is this guide?
Last updated on