Files
web/apps/scandic-web/server/routers/contentstack/metadata/output.ts
Anton Gunnarsson 846fd904a6 Merged in feat/sw-2859-set-up-shared-trpc-package (pull request #2319)
feat(SW-2859): Create trpc package

* Add isEdge, safeTry and dataCache to new common package

* Add eslint and move prettier config

* Clean up tests

* Create trpc package and move initialization

* Move errors and a few procedures

* Move telemetry to common package

* Move tokenManager to common package

* Add Sentry to procedures

* Clean up procedures

* Fix self-referencing imports

* Add exports to packages and lint rule to prevent relative imports

* Add env to trpc package

* Add eslint to trpc package

* Apply lint rules

* Use direct imports from trpc package

* Add lint-staged config to trpc

* Move lang enum to common

* Restructure trpc package folder structure

* Fix lang imports


Approved-by: Linus Flood
2025-06-18 12:14:20 +00:00

215 lines
5.7 KiB
TypeScript

import { z } from "zod"
import { baseUrls } from "@/constants/routes/baseUrls"
import { findMyBooking } from "@/constants/routes/findMyBooking"
import { hotelreservation } from "@/constants/routes/hotelReservation"
import { myStay } from "@/constants/routes/myStay"
import { env } from "@/env/server"
import { attributesSchema as hotelAttributesSchema } from "../../hotels/schemas/hotel"
import { additionalDataAttributesSchema } from "../../hotels/schemas/hotel/include/additionalData"
import { imageSchema } from "../../hotels/schemas/image"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { systemSchema } from "../schemas/system"
import { getDescription } from "./utils/description"
import { getImage } from "./utils/image"
import { getTitle } from "./utils/title"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { Metadata } from "next"
import type { ImageVaultAsset } from "@/types/components/imageVault"
import { Country } from "@/types/enums/country"
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
import { RTETypeEnum } from "@/types/rte/enums"
const metaDataJsonSchema = z.object({
children: z.array(
z.object({
type: z.nativeEnum(RTETypeEnum),
children: z.array(
z.object({
text: z.string().optional(),
})
),
})
),
})
const metaDataBlocksSchema = z
.array(
z.object({
content: z
.object({
content: z
.object({
json: metaDataJsonSchema,
})
.optional()
.nullable(),
})
.optional()
.nullable(),
})
)
.optional()
.nullable()
export const rawMetadataSchema = z.object({
web: z
.object({
seo_metadata: z
.object({
title: z.string().nullish(),
description: z.string().nullish(),
noindex: z.boolean().nullish(),
seo_image: tempImageVaultAssetSchema.nullable(),
})
.nullish(),
breadcrumbs: z
.object({
title: z.string().nullish(),
})
.nullish(),
})
.nullish(),
destination_settings: z
.object({
city_denmark: z.string().nullish(),
city_finland: z.string().nullish(),
city_germany: z.string().nullish(),
city_poland: z.string().nullish(),
city_norway: z.string().nullish(),
city_sweden: z.string().nullish(),
country: z.nativeEnum(Country).nullish(),
})
.nullish(),
heading: z.string().nullish(),
preamble: z
.union([
z.string(),
z.object({
first_column: z.string(),
}),
])
.transform((preamble) => {
if (typeof preamble === "string") {
return preamble
}
return preamble?.first_column || null
})
.nullish(),
header: z
.object({
heading: z.string().nullish(),
preamble: z.string().nullish(),
hero_image: tempImageVaultAssetSchema.nullable(),
})
.nullish(),
hero_image: tempImageVaultAssetSchema.nullable(),
images: z
.array(z.object({ image: tempImageVaultAssetSchema }).nullish())
.transform((images) =>
images
.map((image) => image?.image)
.filter((image): image is ImageVaultAsset => !!image)
)
.nullish(),
blocks: metaDataBlocksSchema,
hotel_page_id: z.string().nullish(),
hotelData: hotelAttributesSchema
.pick({
name: true,
address: true,
detailedFacilities: true,
hotelContent: true,
healthFacilities: true,
})
.nullish(),
additionalHotelData: additionalDataAttributesSchema
.pick({
gallery: true,
hotelParking: true,
healthAndFitness: true,
hotelSpecialNeeds: true,
meetingRooms: true,
parkingImages: true,
accessibility: true,
conferencesAndMeetings: true,
})
.nullish(),
hotelRestaurants: z
.array(
z.object({
nameInUrl: z.string().nullish(),
elevatorPitch: z.string().nullish(),
name: z.string().nullish(),
content: z
.object({
images: z.array(imageSchema).nullish(),
})
.nullish(),
})
)
.nullish(),
subpageUrl: z.string().nullish(),
destinationData: z
.object({
location: z.string().nullish(),
filter: z.string().nullish(),
filterType: z.enum(["facility", "surroundings"]).nullish(),
hotelCount: z.number().nullish(),
cities: z.array(z.string()).nullish(),
})
.nullish(),
system: systemSchema,
})
export const metadataSchema = rawMetadataSchema.transform(async (data) => {
const noIndex = !!data.web?.seo_metadata?.noindex
const metadata: Metadata = {
metadataBase: env.PUBLIC_URL ? new URL(env.PUBLIC_URL) : undefined,
title: await getTitle(data),
description: await getDescription(data),
openGraph: {
images: getImage(data),
},
}
if (noIndex) {
metadata.robots = {
index: false,
follow: false,
}
}
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 (Object.values(myStay).includes(pathName)) {
const urls: LanguageSwitcherData = {}
return Object.entries(myStay).reduce((acc, [lang, url]) => {
acc[lang as Lang] = { url }
return urls
}, urls)
}
if (pathName.startsWith(hotelreservation(lang))) {
return baseUrls
}
return { [lang]: { url: pathName } }
}