* 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
177 lines
5.1 KiB
TypeScript
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
|
|
}, {})
|
|
}
|