import type { MetadataRoute } from "next"; import { unstable_noStore as noStore } from "next/cache"; import { routing } from "@/i18n/routing"; import { getMaintenanceMode, getSeoSettings, getSiteSettings } from "@/lib/app-config"; import { getLocalizedPath, type AppLocale } from "@/lib/locale"; import { toAbsoluteUrl } from "@/lib/metadata"; import { getActivePortfolioCategories, getPublishedPortfolioProjects } from "@/lib/portfolio"; export const dynamic = "force-dynamic"; type EntryOptions = Pick; /** * One entry per locale for a path, each carrying hreflang alternates so search * engines link the three language versions together. */ export function buildLocalizedEntries( pathname: string, defaultLocale: AppLocale, options?: EntryOptions, ): MetadataRoute.Sitemap { const languages = Object.fromEntries( routing.locales.map((locale) => [locale, toAbsoluteUrl(getLocalizedPath(locale, pathname, defaultLocale))]), ) as Record; return routing.locales.map((locale) => ({ url: languages[locale], lastModified: options?.lastModified, changeFrequency: options?.changeFrequency, priority: options?.priority, alternates: { languages: { ...languages, "x-default": languages[defaultLocale], }, }, })); } export default async function sitemap(): Promise { noStore(); const [siteSettings, seo, maintenanceEnabled] = await Promise.all([ getSiteSettings(), getSeoSettings(), getMaintenanceMode(), ]); // While the site is hidden (maintenance) or indexing is off, publish an // empty sitemap instead of advertising URLs that redirect or are noindex. if (maintenanceEnabled || !seo.allowIndexing) { return []; } const defaultLocale = siteSettings.defaultLocale; let projects: Awaited> = []; let categories: Awaited> = []; try { [projects, categories] = await Promise.all([ getPublishedPortfolioProjects(), getActivePortfolioCategories(), ]); } catch { projects = []; categories = []; } // Only categories that actually have published work get a landing URL; // an empty category page has nothing to index. const categoriesWithProjects = categories.filter((category) => projects.some((project) => project.category.slug === category.slug), ); const latestProjectDate = projects.reduce((latest, project) => { const date = project.publishedAt ?? undefined; return date && (!latest || date > latest) ? date : latest; }, undefined); return [ ...buildLocalizedEntries("/", defaultLocale, { changeFrequency: "weekly", priority: 1, lastModified: latestProjectDate, }), ...buildLocalizedEntries("/about", defaultLocale, { changeFrequency: "monthly", priority: 0.8, }), ...buildLocalizedEntries("/portfolio", defaultLocale, { changeFrequency: "weekly", priority: 0.9, lastModified: latestProjectDate, }), ...categoriesWithProjects.flatMap((category) => buildLocalizedEntries(`/portfolio/${category.slug}`, defaultLocale, { changeFrequency: "weekly", priority: 0.8, lastModified: latestProjectDate, }), ), ...buildLocalizedEntries("/contact", defaultLocale, { changeFrequency: "monthly", priority: 0.7, }), ...projects.flatMap((project) => buildLocalizedEntries(`/portfolio/${project.slug}`, defaultLocale, { lastModified: project.publishedAt ?? undefined, changeFrequency: "monthly", priority: 0.8, }), ), ]; }