Files
web/apps/scandic-web/app/api/sitemap/utils.ts
Erik Tiekstra ca93046aad feat/SW-550 sitemap route
* feat(SW-550): Added rewrites to handle sitemap paths

* feat(SW-550): Added sitemap-index generation

* feat(SW-550): Added sitemap xml file generation

* feat(SW-550): Added feature flag 'HIDE_FOR_NEXT_RELEASE' to sitemap routes


Approved-by: Linus Flood
2025-03-17 07:17:08 +00:00

177 lines
5.1 KiB
TypeScript

import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import { removeTrailingSlash } from "@/utils/url"
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<Record<string, SyncItem[]>>((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: removeTrailingSlash(`${env.PUBLIC_URL}/${locale}${url}`),
lastModified,
changeFrequency,
priority,
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<SitemapEntry["changeFrequency"]>((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<Partial<Record<Lang, string>>>((acc, { data }) => {
acc[data.locale] = `${env.PUBLIC_URL}/${data.locale}${data.url}`
return acc
}, {})
}