Merged in feat/SW-1845-metadata-hreflang (pull request #1504)

feat(SW-1845): Added alternates to metadata

* feat(SW-1845): Added alternates to metadata


Approved-by: Linus Flood
This commit is contained in:
Erik Tiekstra
2025-03-10 10:15:02 +00:00
parent 393546d35d
commit 131cbfcda3
3 changed files with 110 additions and 42 deletions

View File

@@ -1,15 +1,12 @@
import { baseUrls } from "@/constants/routes/baseUrls"
import { findMyBooking } from "@/constants/routes/findMyBooking"
import { hotelreservation } from "@/constants/routes/hotelReservation"
import { publicProcedure, router } from "@/server/trpc" import { publicProcedure, router } from "@/server/trpc"
import { getUidAndContentTypeByPath } from "@/services/cms/getUidAndContentTypeByPath" import { getUidAndContentTypeByPath } from "@/services/cms/getUidAndContentTypeByPath"
import { getNonContentstackUrls } from "../metadata/output"
import { getLanguageSwitcherInput } from "./input" import { getLanguageSwitcherInput } from "./input"
import { getUrlsOfAllLanguages } from "./utils" import { getUrlsOfAllLanguages } from "./utils"
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
import type { Lang } from "@/constants/languages"
export const languageSwitcherQueryRouter = router({ export const languageSwitcherQueryRouter = router({
get: publicProcedure get: publicProcedure
@@ -17,29 +14,14 @@ export const languageSwitcherQueryRouter = router({
.query(async ({ input }) => { .query(async ({ input }) => {
const { pathName, lang } = input const { pathName, lang } = input
const { uid, contentType } = await getUidAndContentTypeByPath(pathName) const { uid, contentType } = await getUidAndContentTypeByPath(pathName)
let urls: LanguageSwitcherData | null = null
if (!uid || !contentType) { if (!uid || !contentType) {
// we have pages that are not currently routed within contentstack context, urls = getNonContentstackUrls(lang, pathName)
// therefor this fix is needed for some of these pages } else {
if (Object.values(findMyBooking).includes(pathName)) { urls = await getUrlsOfAllLanguages(lang, uid, contentType)
const urls: LanguageSwitcherData = {}
return {
lang,
urls: Object.entries(findMyBooking).reduce((acc, [lang, url]) => {
acc[lang as Lang] = { url }
return urls
}, urls),
}
}
if (pathName.startsWith(hotelreservation(lang))) {
return { lang, urls: baseUrls }
}
return { lang, urls: { [lang]: { url: pathName } } }
} }
const urls = await getUrlsOfAllLanguages(lang, uid, contentType)
return { return {
lang, lang,
urls, urls,

View File

@@ -1,5 +1,10 @@
import { z } from "zod" import { z } from "zod"
import { baseUrls } from "@/constants/routes/baseUrls"
import { findMyBooking } from "@/constants/routes/findMyBooking"
import { hotelreservation } from "@/constants/routes/hotelReservation"
import { env } from "@/env/server"
import { attributesSchema as hotelAttributesSchema } from "../../hotels/schemas/hotel" import { attributesSchema as hotelAttributesSchema } from "../../hotels/schemas/hotel"
import { tempImageVaultAssetSchema } from "../schemas/imageVault" import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { systemSchema } from "../schemas/system" import { systemSchema } from "../schemas/system"
@@ -8,7 +13,9 @@ import { getDescription, getImage, getTitle } from "./utils"
import type { Metadata } from "next" import type { Metadata } from "next"
import { Country } from "@/types/enums/country" import { Country } from "@/types/enums/country"
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
import { RTETypeEnum } from "@/types/rte/enums" import { RTETypeEnum } from "@/types/rte/enums"
import type { Lang } from "@/constants/languages"
const metaDataJsonSchema = z.object({ const metaDataJsonSchema = z.object({
children: z.array( children: z.array(
@@ -101,6 +108,7 @@ export const metadataSchema = rawMetadataSchema.transform(async (data) => {
const noIndex = !!data.web?.seo_metadata?.noindex const noIndex = !!data.web?.seo_metadata?.noindex
const metadata: Metadata = { const metadata: Metadata = {
metadataBase: new URL(env.PUBLIC_URL),
title: await getTitle(data), title: await getTitle(data),
description: getDescription(data), description: getDescription(data),
openGraph: { openGraph: {
@@ -116,3 +124,21 @@ export const metadataSchema = rawMetadataSchema.transform(async (data) => {
} }
return metadata return metadata
}) })
// Several pages are not currently routed within contentstack context.
// This function is used to generate the urls for these pages.
export function getNonContentstackUrls(lang: Lang, pathName: string) {
if (Object.values(findMyBooking).includes(pathName)) {
const urls: LanguageSwitcherData = {}
return Object.entries(findMyBooking).reduce((acc, [lang, url]) => {
acc[lang as Lang] = { url }
return urls
}, urls)
}
if (pathName.startsWith(hotelreservation(lang))) {
return baseUrls
}
return { [lang]: { url: pathName } }
}

View File

@@ -16,11 +16,15 @@ import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag" import { generateTag } from "@/utils/generateTag"
import { getHotel } from "../../hotels/query" import { getHotel } from "../../hotels/query"
import { getUrlsOfAllLanguages } from "../languageSwitcher/utils"
import { getMetadataInput } from "./input" import { getMetadataInput } from "./input"
import { metadataSchema } from "./output" import { getNonContentstackUrls, metadataSchema } from "./output"
import { affix, getCityData, getCountryData } from "./utils" import { affix, getCityData, getCountryData } from "./utils"
import type { Metadata } from "next"
import { PageContentTypeEnum } from "@/types/requests/contentType" import { PageContentTypeEnum } from "@/types/requests/contentType"
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata" import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
@@ -92,7 +96,10 @@ const fetchMetadata = cache(async function fetchMemoizedMetadata<T>(
return response.data return response.data
}) })
async function getTransformedMetadata(data: unknown) { async function getTransformedMetadata(
data: unknown,
alternates: Metadata["alternates"]
) {
transformMetadataCounter.add(1) transformMetadataCounter.add(1)
console.info("contentstack.metadata transform start") console.info("contentstack.metadata transform start")
const validatedMetadata = await metadataSchema.safeParseAsync(data) const validatedMetadata = await metadataSchema.safeParseAsync(data)
@@ -114,6 +121,9 @@ async function getTransformedMetadata(data: unknown) {
transformMetadataSuccessCounter.add(1) transformMetadataSuccessCounter.add(1)
console.info("contentstack.metadata transform success") console.info("contentstack.metadata transform success")
if (alternates) {
validatedMetadata.data.alternates = alternates
}
return validatedMetadata.data return validatedMetadata.data
} }
@@ -126,28 +136,66 @@ export const metadataQueryRouter = router({
uid: ctx.uid, uid: ctx.uid,
} }
let urls: LanguageSwitcherData | null = null
if (
input.subpage ||
input.filterFromUrl ||
!ctx.uid ||
!ctx.contentType
) {
urls = getNonContentstackUrls(ctx.lang, `${ctx.lang}/${ctx.pathname}`)
} else {
urls = await getUrlsOfAllLanguages(ctx.lang, ctx.uid, ctx.contentType)
}
let alternates: Metadata["alternates"] = null
if (urls) {
const languages: Record<string, string> = {}
Object.entries(urls)
.filter(([lang]) => lang !== ctx.lang)
.forEach(([lang, { url }]) => {
languages[lang] = url
})
const canonical = urls[ctx.lang]?.url
alternates = {
canonical,
languages,
}
}
switch (ctx.contentType) { switch (ctx.contentType) {
case PageContentTypeEnum.accountPage: case PageContentTypeEnum.accountPage:
const accountPageResponse = await fetchMetadata<{ const accountPageResponse = await fetchMetadata<{
account_page: RawMetadataSchema account_page: RawMetadataSchema
}>(GetAccountPageMetadata, variables) }>(GetAccountPageMetadata, variables)
return getTransformedMetadata(accountPageResponse.account_page) return getTransformedMetadata(
accountPageResponse.account_page,
alternates
)
case PageContentTypeEnum.collectionPage: case PageContentTypeEnum.collectionPage:
const collectionPageResponse = await fetchMetadata<{ const collectionPageResponse = await fetchMetadata<{
collection_page: RawMetadataSchema collection_page: RawMetadataSchema
}>(GetCollectionPageMetadata, variables) }>(GetCollectionPageMetadata, variables)
return getTransformedMetadata(collectionPageResponse.collection_page) return getTransformedMetadata(
collectionPageResponse.collection_page,
alternates
)
case PageContentTypeEnum.contentPage: case PageContentTypeEnum.contentPage:
const contentPageResponse = await fetchMetadata<{ const contentPageResponse = await fetchMetadata<{
content_page: RawMetadataSchema content_page: RawMetadataSchema
}>(GetContentPageMetadata, variables) }>(GetContentPageMetadata, variables)
return getTransformedMetadata(contentPageResponse.content_page) return getTransformedMetadata(
contentPageResponse.content_page,
alternates
)
case PageContentTypeEnum.destinationOverviewPage: case PageContentTypeEnum.destinationOverviewPage:
const destinationOverviewPageResponse = await fetchMetadata<{ const destinationOverviewPageResponse = await fetchMetadata<{
destination_overview_page: RawMetadataSchema destination_overview_page: RawMetadataSchema
}>(GetDestinationOverviewPageMetadata, variables) }>(GetDestinationOverviewPageMetadata, variables)
return getTransformedMetadata( return getTransformedMetadata(
destinationOverviewPageResponse.destination_overview_page destinationOverviewPageResponse.destination_overview_page,
alternates
) )
case PageContentTypeEnum.destinationCountryPage: case PageContentTypeEnum.destinationCountryPage:
const destinationCountryPageResponse = await fetchMetadata<{ const destinationCountryPageResponse = await fetchMetadata<{
@@ -159,10 +207,13 @@ export const metadataQueryRouter = router({
ctx.serviceToken, ctx.serviceToken,
ctx.lang ctx.lang
) )
return getTransformedMetadata({ return getTransformedMetadata(
...destinationCountryPageResponse.destination_country_page, {
...countryData, ...destinationCountryPageResponse.destination_country_page,
}) ...countryData,
},
alternates
)
case PageContentTypeEnum.destinationCityPage: case PageContentTypeEnum.destinationCityPage:
const destinationCityPageResponse = await fetchMetadata<{ const destinationCityPageResponse = await fetchMetadata<{
destination_city_page: RawMetadataSchema destination_city_page: RawMetadataSchema
@@ -173,15 +224,21 @@ export const metadataQueryRouter = router({
ctx.serviceToken, ctx.serviceToken,
ctx.lang ctx.lang
) )
return getTransformedMetadata({ return getTransformedMetadata(
...destinationCityPageResponse.destination_city_page, {
...cityData, ...destinationCityPageResponse.destination_city_page,
}) ...cityData,
},
alternates
)
case PageContentTypeEnum.loyaltyPage: case PageContentTypeEnum.loyaltyPage:
const loyaltyPageResponse = await fetchMetadata<{ const loyaltyPageResponse = await fetchMetadata<{
loyalty_page: RawMetadataSchema loyalty_page: RawMetadataSchema
}>(GetLoyaltyPageMetadata, variables) }>(GetLoyaltyPageMetadata, variables)
return getTransformedMetadata(loyaltyPageResponse.loyalty_page) return getTransformedMetadata(
loyaltyPageResponse.loyalty_page,
alternates
)
case PageContentTypeEnum.hotelPage: case PageContentTypeEnum.hotelPage:
const hotelPageResponse = await fetchMetadata<{ const hotelPageResponse = await fetchMetadata<{
hotel_page: RawMetadataSchema hotel_page: RawMetadataSchema
@@ -198,10 +255,13 @@ export const metadataQueryRouter = router({
) )
: null : null
return getTransformedMetadata({ return getTransformedMetadata(
...hotelPageData, {
hotelData: hotelData?.hotel, ...hotelPageData,
}) hotelData: hotelData?.hotel,
},
alternates
)
default: default:
return null return null
} }