294 lines
7.7 KiB
TypeScript
294 lines
7.7 KiB
TypeScript
import { type Lang } from "@scandic-hotels/common/constants/language"
|
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
|
import { removeTrailingSlash } from "@scandic-hotels/common/utils/url"
|
|
|
|
import { env } from "@/env/server"
|
|
|
|
import type {
|
|
HotelFilterEntries,
|
|
HotelFilterEntry,
|
|
SeoFilter,
|
|
SyncItem,
|
|
} from "@/types/sitemap"
|
|
|
|
export function mergeEntries(
|
|
currentEntries: SyncItem[],
|
|
newEntries: SyncItem[]
|
|
) {
|
|
const entriesMergeCounter = createCounter("sitemap", "entries.merge")
|
|
const metricsEntriesMerge = entriesMergeCounter.init({
|
|
currentEntriesCount: currentEntries.length,
|
|
newEntriesCount: newEntries.length,
|
|
})
|
|
|
|
metricsEntriesMerge.start()
|
|
|
|
const entries = [...currentEntries]
|
|
newEntries.forEach((entry) => {
|
|
if (!entry?.data?.uid || !entry?.data?.locale) {
|
|
metricsEntriesMerge.dataError(
|
|
`Invalid entry data, missing uid or locale`,
|
|
{ entry }
|
|
)
|
|
return
|
|
}
|
|
|
|
const index = entries.findIndex(({ data }) => {
|
|
if (!data) {
|
|
metricsEntriesMerge.dataError(`Data is null or undefined,`)
|
|
return false
|
|
}
|
|
|
|
return data.uid === entry.data.uid && data.locale === entry.data.locale
|
|
})
|
|
if (index > -1) {
|
|
entries[index] = entry
|
|
} else {
|
|
entries.push(entry)
|
|
}
|
|
})
|
|
|
|
metricsEntriesMerge.success({
|
|
entriesCount: entries.length,
|
|
})
|
|
|
|
return entries
|
|
}
|
|
|
|
export function mapEntriesToHotelFilters(entries: SyncItem[]) {
|
|
const entriesTransformToHotelFiltersCounter = createCounter(
|
|
"sitemap",
|
|
"entries.transform.hotelFilters"
|
|
)
|
|
const metricsEntriesTransformToHotelFilters =
|
|
entriesTransformToHotelFiltersCounter.init({
|
|
entriesCount: entries.length,
|
|
})
|
|
|
|
metricsEntriesTransformToHotelFilters.start()
|
|
|
|
const filteredEntries = entries
|
|
.filter(
|
|
(entry) => entry.content_type_uid === "hotel_filter" && entry.data.slug
|
|
)
|
|
.map((entry) => ({
|
|
uid: entry.data.uid,
|
|
locale: entry.data.locale,
|
|
slug: entry.data.slug,
|
|
}))
|
|
const entriesByUid = groupHotelFilterEntriesByUid(
|
|
filteredEntries as HotelFilterEntry[]
|
|
)
|
|
|
|
metricsEntriesTransformToHotelFilters.success({
|
|
entriesCount: entries.length,
|
|
hotelFilterEntriesCount: filteredEntries.length,
|
|
uniqueHotelFilterUidsCount: Object.keys(entriesByUid).length,
|
|
})
|
|
|
|
return entriesByUid
|
|
}
|
|
|
|
export function mapEntriesToSitemapData(
|
|
entries: SyncItem[],
|
|
hotelFilters: HotelFilterEntries
|
|
) {
|
|
const entriesTransformCounter = createCounter("sitemap", "entries.transform")
|
|
const metricsEntriesTransform = entriesTransformCounter.init({
|
|
entriesCount: entries.length,
|
|
})
|
|
|
|
metricsEntriesTransform.start()
|
|
|
|
const filteredEntries = filterEntriesToSitemapEntries(entries)
|
|
|
|
const entriesByUid = groupEntriesByUid(filteredEntries)
|
|
const sitemapEntries = Object.entries(entriesByUid)
|
|
.map(([_, entries]) => mapEntriesToSitemapEntries(entries, hotelFilters))
|
|
.flat()
|
|
|
|
metricsEntriesTransform.success({
|
|
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 groupHotelFilterEntriesByUid(entries: HotelFilterEntry[]) {
|
|
return entries.reduce<HotelFilterEntries>((acc, entry) => {
|
|
const uid = entry.uid
|
|
if (!acc[uid]) {
|
|
acc[uid] = []
|
|
}
|
|
acc[uid].push(entry)
|
|
return acc
|
|
}, {})
|
|
}
|
|
|
|
function isDestinationPageWithSeoFilters(entry: SyncItem) {
|
|
const destinationPageUids = [
|
|
"destination_city_page",
|
|
"destination_country_page",
|
|
]
|
|
|
|
return (
|
|
destinationPageUids.includes(entry.content_type_uid) &&
|
|
!!entry.data.seo_filters?.length
|
|
)
|
|
}
|
|
|
|
function mapEntriesToSitemapEntries(
|
|
entries: SyncItem[],
|
|
hotelFilters: HotelFilterEntries
|
|
) {
|
|
const alternates = getAlternates(entries)
|
|
const seoFilterEntries = mapEntriesToSeoFilterSitemapEntries(
|
|
entries,
|
|
hotelFilters
|
|
)
|
|
const pageEntries = entries.map((currentEntry) => ({
|
|
url: removeTrailingSlash(
|
|
`${env.PUBLIC_URL}/${currentEntry.data.locale}${currentEntry.data.url}`
|
|
),
|
|
lastModified: currentEntry.data.updated_at,
|
|
changeFrequency:
|
|
currentEntry.data.web?.seo_metadata?.sitemap?.change_frequency ?? "daily",
|
|
priority: currentEntry.data.web?.seo_metadata?.sitemap?.priority ?? 0.5,
|
|
alternates,
|
|
}))
|
|
|
|
return [...pageEntries, ...seoFilterEntries]
|
|
}
|
|
|
|
function mapEntriesToSeoFilterSitemapEntries(
|
|
entries: SyncItem[],
|
|
hotelFilters: HotelFilterEntries
|
|
) {
|
|
const relevantEntries = entries.filter((entry) =>
|
|
isDestinationPageWithSeoFilters(entry)
|
|
)
|
|
|
|
if (!relevantEntries.length) {
|
|
return []
|
|
}
|
|
|
|
return relevantEntries.flatMap((entry) => {
|
|
const seoFilters = entry.data.seo_filters?.filter(
|
|
(seoFilter) => !seoFilter.seo_metadata?.noindex
|
|
)
|
|
|
|
if (!seoFilters?.length) {
|
|
return []
|
|
}
|
|
|
|
return seoFilters
|
|
.map((seoFilter) =>
|
|
getEntryBySeoFilter(entry, entries, seoFilter, hotelFilters)
|
|
)
|
|
.filter((e): e is NonNullable<typeof e> => !!e)
|
|
})
|
|
}
|
|
|
|
function getEntryBySeoFilter(
|
|
currentEntry: SyncItem,
|
|
entries: SyncItem[],
|
|
seoFilter: SeoFilter,
|
|
hotelFilters: HotelFilterEntries
|
|
) {
|
|
const filterUid = seoFilter.filter?.[0]?.uid
|
|
if (!filterUid) {
|
|
return null
|
|
}
|
|
const locale = currentEntry.data.locale
|
|
const baseUrl = removeTrailingSlash(
|
|
`${env.PUBLIC_URL}/${locale}${currentEntry.data.url}`
|
|
)
|
|
const defaultMetadata = currentEntry.data.web?.seo_metadata
|
|
const matchedHotelFilter = hotelFilters[filterUid]
|
|
const matchedHotelFilterForLocale = matchedHotelFilter?.find(
|
|
(f) => f.locale === locale
|
|
)
|
|
if (!matchedHotelFilterForLocale) {
|
|
return null
|
|
}
|
|
|
|
const alternates = getHotelFilterAlternates(
|
|
entries,
|
|
matchedHotelFilter,
|
|
filterUid
|
|
)
|
|
|
|
return {
|
|
url: removeTrailingSlash(`${baseUrl}/${matchedHotelFilterForLocale.slug}`),
|
|
lastModified: currentEntry.data.updated_at,
|
|
changeFrequency:
|
|
seoFilter.seo_metadata?.sitemap?.change_frequency ??
|
|
defaultMetadata?.sitemap?.change_frequency ??
|
|
"daily",
|
|
priority:
|
|
seoFilter.seo_metadata?.sitemap?.priority ??
|
|
defaultMetadata?.sitemap?.priority ??
|
|
0.5,
|
|
alternates,
|
|
}
|
|
}
|
|
|
|
function getAlternates(entries: SyncItem[]) {
|
|
return entries.reduce<Partial<Record<Lang, string>>>((acc, entry) => {
|
|
acc[entry.data.locale] = removeTrailingSlash(
|
|
`${env.PUBLIC_URL}/${entry.data.locale}${entry.data.url}`
|
|
)
|
|
return acc
|
|
}, {})
|
|
}
|
|
|
|
function getHotelFilterAlternates(
|
|
entries: SyncItem[],
|
|
matchedHotelFilter: HotelFilterEntry[],
|
|
filterUid: string
|
|
) {
|
|
const matchingFilterInOtherLocales = entries.filter((entry) =>
|
|
entry.data.seo_filters?.some(
|
|
(sf) =>
|
|
sf.filter?.some((f) => f.uid === filterUid) && !sf.seo_metadata?.noindex
|
|
)
|
|
)
|
|
|
|
return matchingFilterInOtherLocales.reduce<Partial<Record<Lang, string>>>(
|
|
(acc, entry) => {
|
|
const locale = entry.data.locale
|
|
const foundFilter = matchedHotelFilter?.find((f) => f.locale === locale)
|
|
if (!foundFilter) {
|
|
return acc
|
|
}
|
|
acc[locale] = removeTrailingSlash(
|
|
`${env.PUBLIC_URL}/${locale}${entry.data.url}/${foundFilter.slug}`
|
|
)
|
|
return acc
|
|
},
|
|
{}
|
|
)
|
|
}
|