import { Lang } from "@/constants/languages" import { env } from "@/env/server" import { mergeEntriesCounter, mergeEntriesSuccessCounter, transformEntriesCounter, transformEntriesSuccessCounter, } from "./telemetry" import type { SitemapEntry, SyncItem } from "@/types/sitemap" export function mergeEntries( currentEntries: SyncItem[], newEntries: SyncItem[] ) { mergeEntriesCounter.add(1, { currentEntriesCount: currentEntries.length, newEntriesCount: newEntries.length, }) console.info( "sitemap.entries.merge start", JSON.stringify({ currentEntriesCount: currentEntries.length, newEntriesCount: newEntries.length, }) ) const entries = [...currentEntries] newEntries.forEach((entry) => { const index = entries.findIndex( ({ data }) => data.uid === entry.data.uid && data.locale === entry.data.locale ) if (index > -1) { entries[index] = entry } else { entries.push(entry) } }) mergeEntriesSuccessCounter.add(1, { entriesCount: entries.length, }) console.info( "sitemap.entries.merge success", JSON.stringify({ entriesCount: entries.length, }) ) return entries } export function mapEntriesToSitemapData(entries: SyncItem[]) { transformEntriesCounter.add(1, { entriesCount: entries.length }) console.info( "sitemap.entries.transform start", JSON.stringify({ entriesCount: entries.length, }) ) const filteredEntries = filterEntriesToSitemapEntries(entries) const entriesByUid = groupEntriesByUid(filteredEntries) const sitemapEntries = Object.entries(entriesByUid) .map(([_, entries]) => mapEntriesToSitemapEntry(entries)) .filter((entry): entry is SitemapEntry => !!entry) transformEntriesSuccessCounter.add(1, { entriesCount: entries.length, sitemapEntriesCount: sitemapEntries.length, }) console.info( "sitemap.entries.transform success", JSON.stringify({ entriesCount: entries.length, sitemapEntriesCount: sitemapEntries.length, }) ) return sitemapEntries } function filterEntriesToSitemapEntries(entries: SyncItem[]) { return entries.filter((entry: SyncItem) => { const shouldIndex = !entry.data.web?.seo_metadata?.noindex return !!(entry.type === "entry_published" && entry.data.url && shouldIndex) }) } // We group the entries by UID because Contentstack has the same `uid` for an // entry regardless of locale. We want to display each locale as an alternate // in the sitemap, therefore we group them here by `uid`. function groupEntriesByUid(entries: SyncItem[]) { return entries.reduce>((acc, entry) => { const uid = entry.data.uid if (!acc[uid]) { acc[uid] = [] } acc[uid].push(entry) return acc }, {}) } function mapEntriesToSitemapEntry(entries: SyncItem[]) { // Main entry is always English. Without English, there can't be other pages in ContentStack. const mainEntry = entries.find((entry) => entry.data.locale === Lang.en) const alternates = getAlternates(entries) const lastModified = getLastModified(entries) const changeFrequency = getChangeFrequency(entries) const priority = getPriority(entries) if (mainEntry) { const { locale, url } = mainEntry.data const sitemapEntry: SitemapEntry = { url: `${env.PUBLIC_URL}/${locale}${url}`, lastModified, changeFrequency, priority, } if (alternates) { sitemapEntry.alternates = alternates } return sitemapEntry } } function getLastModified(entries: SyncItem[]) { // Localized versions of the data can have a different last modified value. // We make sure we take the latest. return entries.reduce((latest, entry) => { const entryDate = entry.data.updated_at return entryDate > latest ? entryDate : latest }, "") } function getChangeFrequency(entries: SyncItem[]) { // Localized versions of the data can have a different changeFrequency value. // We make sure we take the highest. const changeFrequencyPriority: SitemapEntry["changeFrequency"][] = [ "never", "yearly", "monthly", "weekly", "daily", "hourly", "always", ] return entries.reduce((highest, entry) => { const changeFrequency = entry.data.web?.seo_metadata?.sitemap?.change_frequency ?? "daily" return changeFrequencyPriority.indexOf(changeFrequency) > changeFrequencyPriority.indexOf(highest) ? changeFrequency : highest }, "never") } function getPriority(entries: SyncItem[]) { // Localized versions of the data can have a different priority. // We make sure we take the highest. return entries.reduce((highest, entry) => { const priority = entry.data.web?.seo_metadata?.sitemap?.priority ?? 0.5 return priority > highest ? priority : highest }, 0.0) } function getAlternates(entries: SyncItem[]) { return entries .filter((entry) => entry.data.locale !== Lang.en) .reduce>>((acc, { data }) => { acc[data.locale] = `${env.PUBLIC_URL}/${data.locale}${data.url}` return acc }, {}) }