feat(BOOK-58): Added destination filter pages to sitemap
Approved-by: Linus Flood
This commit is contained in:
@@ -9,13 +9,18 @@ import {
|
||||
getEntries,
|
||||
getSyncToken,
|
||||
saveEntries,
|
||||
saveHotelFilters,
|
||||
saveLastUpdatedDate,
|
||||
saveSitemapData,
|
||||
saveSyncToken,
|
||||
} from "@/utils/sitemap"
|
||||
|
||||
import { contentstackSync } from "./sync"
|
||||
import { mapEntriesToSitemapData, mergeEntries } from "./utils"
|
||||
import {
|
||||
mapEntriesToHotelFilters,
|
||||
mapEntriesToSitemapData,
|
||||
mergeEntries,
|
||||
} from "./utils"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
@@ -36,7 +41,6 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const syncToken = await getSyncToken()
|
||||
const currentEntries = await getEntries()
|
||||
|
||||
const responseData = await contentstackSync(syncToken)
|
||||
const mergedEntries = mergeEntries(currentEntries, responseData.entries)
|
||||
|
||||
@@ -49,7 +53,10 @@ export async function GET(request: NextRequest) {
|
||||
await saveEntries(mergedEntries)
|
||||
metricsEntriesSave.success()
|
||||
|
||||
const sitemapData = mapEntriesToSitemapData(mergedEntries)
|
||||
const hotelFilters = mapEntriesToHotelFilters(mergedEntries)
|
||||
await saveHotelFilters(hotelFilters)
|
||||
|
||||
const sitemapData = mapEntriesToSitemapData(mergedEntries, hotelFilters)
|
||||
const lastUpdated = dt().utc().format()
|
||||
|
||||
const saveDataCounter = createCounter("sitemap", "data.save")
|
||||
|
||||
@@ -4,7 +4,12 @@ import { removeTrailingSlash } from "@scandic-hotels/common/utils/url"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import type { SyncItem } from "@/types/sitemap"
|
||||
import type {
|
||||
HotelFilterEntries,
|
||||
HotelFilterEntry,
|
||||
SeoFilter,
|
||||
SyncItem,
|
||||
} from "@/types/sitemap"
|
||||
|
||||
export function mergeEntries(
|
||||
currentEntries: SyncItem[],
|
||||
@@ -50,7 +55,44 @@ export function mergeEntries(
|
||||
return entries
|
||||
}
|
||||
|
||||
export function mapEntriesToSitemapData(entries: SyncItem[]) {
|
||||
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,
|
||||
@@ -62,7 +104,7 @@ export function mapEntriesToSitemapData(entries: SyncItem[]) {
|
||||
|
||||
const entriesByUid = groupEntriesByUid(filteredEntries)
|
||||
const sitemapEntries = Object.entries(entriesByUid)
|
||||
.map(([_, entries]) => mapEntriesToSitemapEntries(entries))
|
||||
.map(([_, entries]) => mapEntriesToSitemapEntries(entries, hotelFilters))
|
||||
.flat()
|
||||
|
||||
metricsEntriesTransform.success({
|
||||
@@ -94,9 +136,39 @@ function groupEntriesByUid(entries: SyncItem[]) {
|
||||
}, {})
|
||||
}
|
||||
|
||||
function mapEntriesToSitemapEntries(entries: SyncItem[]) {
|
||||
const alternates = mapToAlternates(entries)
|
||||
return entries.map((currentEntry) => ({
|
||||
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}`
|
||||
),
|
||||
@@ -106,9 +178,84 @@ function mapEntriesToSitemapEntries(entries: SyncItem[]) {
|
||||
priority: currentEntry.data.web?.seo_metadata?.sitemap?.priority ?? 0.5,
|
||||
alternates,
|
||||
}))
|
||||
|
||||
return [...pageEntries, ...seoFilterEntries]
|
||||
}
|
||||
|
||||
function mapToAlternates(entries: SyncItem[]) {
|
||||
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}`
|
||||
@@ -116,3 +263,31 @@ function mapToAlternates(entries: SyncItem[]) {
|
||||
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
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,23 +20,43 @@ export interface SitemapEntry {
|
||||
|
||||
export type SitemapData = SitemapEntry[]
|
||||
|
||||
export interface SeoMetadata {
|
||||
noindex?: boolean | null
|
||||
sitemap?: {
|
||||
change_frequency: SitemapEntry["changeFrequency"]
|
||||
priority: SitemapEntry["priority"]
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface SeoFilter {
|
||||
filter?: {
|
||||
uid?: string
|
||||
}[]
|
||||
seo_metadata?: SeoMetadata
|
||||
}
|
||||
export interface HotelFilterEntry {
|
||||
uid: string
|
||||
locale: Lang
|
||||
slug: string
|
||||
}
|
||||
|
||||
export type HotelFilterEntries = Record<string, HotelFilterEntry[]>
|
||||
|
||||
export interface SyncItemData {
|
||||
uid: string
|
||||
locale: Lang
|
||||
url?: string
|
||||
updated_at: string
|
||||
web?: {
|
||||
seo_metadata?: {
|
||||
noindex?: boolean | null
|
||||
sitemap?: {
|
||||
change_frequency: SitemapEntry["changeFrequency"]
|
||||
priority: SitemapEntry["priority"]
|
||||
} | null
|
||||
}
|
||||
seo_metadata?: SeoMetadata
|
||||
}
|
||||
// SEO Filters on destination pages
|
||||
slug?: string
|
||||
seo_filters?: SeoFilter[]
|
||||
}
|
||||
export interface SyncItem {
|
||||
type: string
|
||||
content_type_uid: string
|
||||
data: SyncItemData
|
||||
}
|
||||
export interface SyncResponse extends Omit<SyncResult, "items"> {
|
||||
|
||||
@@ -2,11 +2,12 @@ import { getStore } from "@netlify/blobs"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import type { SitemapData, SyncItem } from "@/types/sitemap"
|
||||
import type { HotelFilterEntry, SitemapData, SyncItem } from "@/types/sitemap"
|
||||
|
||||
const branch = env.CMS_BRANCH
|
||||
const environment = env.CMS_ENVIRONMENT
|
||||
const entriesKey = `${environment}/${branch}/entries`
|
||||
const hotelFiltersKey = `${environment}/${branch}/hotelFilters`
|
||||
const syncTokenKey = `${environment}/${branch}/syncToken`
|
||||
const sitemapDataKey = `${environment}/${branch}/sitemapData`
|
||||
const lastUpdatedKey = `${environment}/${branch}/lastUpdated`
|
||||
@@ -14,6 +15,13 @@ const MAX_ENTRIES_PER_SITEMAP = 50000
|
||||
|
||||
// We need to wrap `getStore` because calling it in the root of the file causes
|
||||
// it to be executed during build time. This is not supported by Netlify.
|
||||
// To run this locally, you need to change the arguments to `getStore` like this:
|
||||
// return getStore({
|
||||
// name: "sitemap",
|
||||
// siteID: "SITE_ID from netlify",
|
||||
// token: "Personal access token from Netlify",
|
||||
// })
|
||||
// See https://docs.netlify.com/build/data-and-storage/netlify-blobs/#getstore for more info.
|
||||
function store() {
|
||||
return getStore("sitemap")
|
||||
}
|
||||
@@ -22,6 +30,12 @@ export async function saveEntries(entries: SyncItem[]) {
|
||||
await store().setJSON(entriesKey, entries)
|
||||
}
|
||||
|
||||
export async function saveHotelFilters(
|
||||
hotelFilters: Record<string, HotelFilterEntry[]>
|
||||
) {
|
||||
await store().setJSON(hotelFiltersKey, hotelFilters)
|
||||
}
|
||||
|
||||
export async function saveSitemapData(sitemapData: SitemapData) {
|
||||
await store().setJSON(sitemapDataKey, sitemapData)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user