import type { MetadataRoute } from 'next'; import { getBlogSitemapEntries, getCategorySitemapEntries, getProductSitemapEntries, } from 'brainerce'; import { getServerClient } from '@/core/lib/brainerce.server'; import { getCanonicalSiteUrl } from '@/core/lib/site-url'; /** * Storefront sitemap: static pages + every published product, category, * blog post, and merchant-authored content page. * * Products MUST go through `getProductSitemapEntries` — the public listing * API clamps `limit` to 100, so a naive `getProducts({ limit: 1000 })` call * silently truncates the sitemap for any store with more than 100 products. * The helper uses a dedicated lightweight endpoint with a 5000-product cap. */ export default async function sitemap(): Promise { const baseUrl = await getCanonicalSiteUrl(); const client = await getServerClient(); const storeInfo = await client.getStoreInfo().catch(() => null); const supportedLocales = storeInfo?.i18n?.supportedLocales ?? []; const defaultLoc = storeInfo?.i18n?.defaultLocale ?? storeInfo?.language ?? ''; const isMultiLocale = supportedLocales.length > 1; const locales = isMultiLocale ? supportedLocales : undefined; // Indexable static routes. Cart/checkout/account/login are noindex — never // list them here. const staticPaths: Array<{ path: string; priority: number }> = [ { path: '/', priority: 1 }, { path: '/products', priority: 0.9 }, { path: '/faq', priority: 0.5 }, { path: '/contact', priority: 0.5 }, ]; const staticPages: MetadataRoute.Sitemap = (isMultiLocale ? supportedLocales : ['']).flatMap( (loc) => { const prefix = !isMultiLocale || loc === defaultLoc ? '' : `/${loc}`; return staticPaths.map(({ path, priority }) => ({ url: `${baseUrl}${prefix}${path === '/' ? (prefix ? '' : '/') : path}`, lastModified: new Date(), priority, })); } ); // Each section fails independently to an empty list — a transient API error // on one content type must not blank the whole sitemap. const [productPages, categoryPages, blogPages, contentPages] = await Promise.all([ getProductSitemapEntries(client, { siteUrl: baseUrl, locales, defaultLocale: defaultLoc, }).catch(() => []), // Category (collection) pages — the highest-value organic-SEO surface; // must be crawlable so they can rank for broad research-intent queries. getCategorySitemapEntries(client, { siteUrl: baseUrl, locales, defaultLocale: defaultLoc, }).catch(() => []), // Blog posts — the SEO Autopilot (and manual publishes) must be // discoverable; missing blog entries = articles never get crawled. getBlogSitemapEntries(client, { siteUrl: baseUrl, locales, defaultLocale: defaultLoc, }).catch(() => []), // Merchant-authored content pages (About, policies, …) at /pages/{slug}. client.content.page .list() .then((pages) => pages.flatMap((page) => { const slug = (page.data as { slug?: string }).slug; if (!slug) return []; return [ { url: `${baseUrl}/pages/${slug}`, lastModified: page.updatedAt ? new Date(page.updatedAt) : undefined, priority: 0.5, }, ]; }) ) .catch(() => []), ]); return [...staticPages, ...productPages, ...categoryPages, ...blogPages, ...contentPages]; }