feat(BOOK-57): Adjusted metadata for destination pages with active seo filter

Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
Erik Tiekstra
2025-09-25 13:26:00 +00:00
parent 7714761c77
commit 9f02870647
20 changed files with 678 additions and 458 deletions

View File

@@ -13,7 +13,7 @@ export default function CityListingSkeleton() {
<SkeletonShimmer height="30px" width="200px" />
<SkeletonShimmer height="30px" width="120px" />
</div>
<ul className={styles.cityList}>
<ul className={styles.list}>
{Array.from({ length: 3 }).map((_, index) => (
<li key={index}>
<CityListingItemSkeleton />

View File

@@ -13,7 +13,7 @@ export default function HotelListingSkeleton() {
<SkeletonShimmer height="30px" width="300px" />
<SkeletonShimmer height="30px" width="100px" />
</div>
<ul className={styles.hotelList}>
<ul className={styles.list}>
{Array.from({ length: 3 }).map((_, index) => (
<li key={index}>
<HotelListingItemSkeleton />

View File

@@ -1,43 +0,0 @@
import { getIntl } from "@/i18n"
import { truncateTextAfterLastPeriod } from "../truncate"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
export async function getDestinationCityPageDescription(
data: RawMetadataSchema
) {
const intl = await getIntl()
if (!data.destinationData || !data.destinationData.hotelCount) {
return null
}
const { hotelCount, location } = data.destinationData
if (hotelCount === 1) {
const destinationCitySingleHotelDescription = intl.formatMessage(
{
defaultMessage:
"Discover our Scandic hotel in {location}. Start your day with a delicious breakfast before exploring {location}. Book your stay at a Scandic hotel now!",
},
{
location: location,
}
)
return truncateTextAfterLastPeriod(destinationCitySingleHotelDescription)
}
const destinationCityMultipleHotelDescription = intl.formatMessage(
{
defaultMessage:
"Discover all our {hotelCount} Scandic hotels in {location}. Start your day with a delicious breakfast before exploring {location}. Book your stay at a Scandic hotel now!",
},
{
hotelCount: hotelCount,
location: location,
}
)
return truncateTextAfterLastPeriod(destinationCityMultipleHotelDescription)
}

View File

@@ -1,52 +0,0 @@
import { getIntl } from "@/i18n"
import { truncateTextAfterLastPeriod } from "../truncate"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
export async function getDestinationCountryPageDescription(
data: RawMetadataSchema
) {
const intl = await getIntl()
if (!data.destinationData?.location) {
return null
}
const { hotelCount, location, cities } = data.destinationData
let destinationCountryDescription: string | null = null
if (!hotelCount) {
destinationCountryDescription = intl.formatMessage(
{
defaultMessage:
"Discover {location}. Enjoy your stay at a Scandic hotel. Book now!",
},
{ location }
)
} else if (!cities || cities.length < 2) {
destinationCountryDescription = intl.formatMessage(
{
defaultMessage:
"Discover all our {hotelCount} Scandic hotels in {location}. Enjoy your stay at a Scandic hotel. Book now!",
},
{ hotelCount, location }
)
} else {
destinationCountryDescription = intl.formatMessage(
{
defaultMessage:
"Discover all our {hotelCount} Scandic hotels in {location}. Explore {city1}, {city2}, and more! All while enjoying your stay at a Scandic hotel. Book now!",
},
{
hotelCount: hotelCount,
location: location,
city1: cities[0],
city2: cities[1],
}
)
}
return truncateTextAfterLastPeriod(destinationCountryDescription)
}

View File

@@ -0,0 +1,110 @@
import { getIntl } from "@/i18n"
import { truncateTextAfterLastPeriod } from "../truncate"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
export async function getDestinationCityPageDescription(
data: RawMetadataSchema
) {
const intl = await getIntl()
if (!data.destinationData?.location) {
return null
}
const { hotelCount, location } = data.destinationData
if (hotelCount === 1) {
const destinationCitySingleHotelDescription = intl.formatMessage(
{
defaultMessage:
"Discover our Scandic hotel in {location}. Start your day with a delicious breakfast before exploring {location}. Book your stay at a Scandic hotel now!",
},
{ location }
)
return truncateTextAfterLastPeriod(destinationCitySingleHotelDescription)
}
const destinationCityMultipleHotelDescription = intl.formatMessage(
{
defaultMessage:
"Discover all our {hotelCount} Scandic hotels in {location}. Start your day with a delicious breakfast before exploring {location}. Book your stay at a Scandic hotel now!",
},
{ hotelCount, location }
)
return truncateTextAfterLastPeriod(destinationCityMultipleHotelDescription)
}
export async function getDestinationCountryPageDescription(
data: RawMetadataSchema
) {
const intl = await getIntl()
if (!data.destinationData?.location) {
return null
}
const { hotelCount, location, cities } = data.destinationData
let destinationCountryDescription: string | null = null
if (!hotelCount) {
destinationCountryDescription = intl.formatMessage(
{
defaultMessage:
"Discover {location}. Enjoy your stay at a Scandic hotel. Book now!",
},
{ location }
)
} else if (!cities || cities.length < 2) {
destinationCountryDescription = intl.formatMessage(
{
defaultMessage:
"Discover all our {hotelCount} Scandic hotels in {location}. Enjoy your stay at a Scandic hotel. Book now!",
},
{ hotelCount, location }
)
} else {
destinationCountryDescription = intl.formatMessage(
{
defaultMessage:
"Discover all our {hotelCount} Scandic hotels in {location}. Explore {city1}, {city2}, and more! All while enjoying your stay at a Scandic hotel. Book now!",
},
{
hotelCount: hotelCount,
location: location,
city1: cities[0],
city2: cities[1],
}
)
}
return truncateTextAfterLastPeriod(destinationCountryDescription)
}
export function getDestinationFilterSeoMetaDescription(
data: RawMetadataSchema
) {
const filter = data.destinationData?.filter
if (!filter) {
return null
}
const foundSeoFilter = data.seo_filters?.find(
(f) => f.filterConnection.edges[0]?.node?.slug === filter
)
if (foundSeoFilter) {
if (foundSeoFilter.seo_metadata?.description) {
return foundSeoFilter.seo_metadata.description
}
if (foundSeoFilter.preamble) {
return truncateTextAfterLastPeriod(foundSeoFilter.preamble)
}
}
return null
}

View File

@@ -1,15 +1,31 @@
import { PageContentTypeEnum } from "@scandic-hotels/trpc/enums/contentType"
import { RTETypeEnum } from "@scandic-hotels/trpc/types/RTEenums"
import {
getDestinationCityPageDescription,
getDestinationCountryPageDescription,
getDestinationFilterSeoMetaDescription,
} from "@/utils/metadata/description/destinationPage"
import { truncateTextAfterLastPeriod } from "../truncate"
import { getDestinationCityPageDescription } from "./destinationCityPage"
import { getDestinationCountryPageDescription } from "./destinationCountryPage"
import { getHotelPageDescription } from "./hotelPage"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
export async function getDescription(data: RawMetadataSchema) {
const metadata = data.web?.seo_metadata
const isDestinationPage = [
PageContentTypeEnum.destinationCityPage,
PageContentTypeEnum.destinationCountryPage,
].includes(data.system.content_type_uid as PageContentTypeEnum)
if (isDestinationPage) {
const destinationFilterSeoMetaDescription =
getDestinationFilterSeoMetaDescription(data)
if (destinationFilterSeoMetaDescription) {
return destinationFilterSeoMetaDescription
}
}
if (metadata?.description) {
return metadata.description

View File

@@ -1,3 +1,4 @@
import { PageContentTypeEnum } from "@scandic-hotels/trpc/enums/contentType"
import { type RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
import { env } from "@/env/server"
@@ -99,13 +100,33 @@ function getUrl(alternates: AlternateURLs | null): string | null {
}
}
function isNoIndexFromMetadata(data: RawMetadataSchema) {
const isDestinationPage = [
PageContentTypeEnum.destinationCityPage,
PageContentTypeEnum.destinationCountryPage,
].includes(data.system.content_type_uid as PageContentTypeEnum)
if (isDestinationPage) {
const filter = data.destinationData?.filter
if (filter) {
const foundSeoFilter = data.seo_filters?.find(
(f) => f.filterConnection.edges[0]?.node?.slug === filter
)
if (foundSeoFilter) {
return !!foundSeoFilter.seo_metadata?.noindex
}
}
}
return !!data.web?.seo_metadata?.noindex
}
async function getTransformedMetadata(
data: RawMetadataSchema,
alternates: Metadata["alternates"] | null,
robots: Metadata["robots"] | null = null
) {
const noIndex = !!data.web?.seo_metadata?.noindex
const metadata: Metadata = {
metadataBase: env.PUBLIC_URL ? new URL(env.PUBLIC_URL) : undefined,
title: await getTitle(data),
@@ -117,7 +138,7 @@ async function getTransformedMetadata(
robots,
}
if (noIndex) {
if (isNoIndexFromMetadata(data)) {
metadata.robots = {
index: false,
follow: false,

View File

@@ -1,105 +0,0 @@
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
export function getImage(data: RawMetadataSchema) {
const metadataImage = data.web?.seo_metadata?.seo_image
const heroImage =
data.hero_image || data.header?.hero_image || data.images?.[0]
// Currently we don't have the possibility to get smaller images from ImageVault (2024-11-15)
if (metadataImage) {
return {
url: metadataImage.url,
alt: metadataImage.meta.alt || undefined,
width: metadataImage.dimensions.width,
height: metadataImage.dimensions.height,
}
}
if (data.system.content_type_uid === "hotel_page" && data.hotelData) {
if (data.subpageUrl) {
let subpageImage: { url: string; alt: string } | undefined
const restaurantSubPage = data.hotelRestaurants?.find(
(restaurant) => restaurant.nameInUrl === data.subpageUrl
)
const restaurantImage = restaurantSubPage?.content?.images?.[0]
if (restaurantImage) {
subpageImage = {
url: restaurantImage.src,
alt: restaurantImage.altText || restaurantImage.altText_En || "",
}
}
switch (data.subpageUrl) {
case data.additionalHotelData?.hotelParking.nameInUrl:
const parkingImage =
data.additionalHotelData?.parkingImages?.heroImages[0]
if (parkingImage) {
subpageImage = {
url: parkingImage.src,
alt: parkingImage.altText || parkingImage.altText_En || "",
}
}
break
case data.additionalHotelData?.healthAndFitness.nameInUrl:
const wellnessImage = data.hotelData.healthFacilities.find(
(fac) => fac.content.images.length
)?.content.images[0]
if (wellnessImage) {
subpageImage = {
url: wellnessImage.src,
alt: wellnessImage.altText || wellnessImage.altText_En || "",
}
}
break
case data.additionalHotelData?.hotelSpecialNeeds.nameInUrl:
const accessibilityImage =
data.additionalHotelData?.accessibility?.heroImages[0]
if (accessibilityImage) {
subpageImage = {
url: accessibilityImage.src,
alt:
accessibilityImage.altText ||
accessibilityImage.altText_En ||
"",
}
}
break
case data.additionalHotelData?.meetingRooms.nameInUrl:
const meetingImage =
data.additionalHotelData?.conferencesAndMeetings?.heroImages[0]
if (meetingImage) {
subpageImage = {
url: meetingImage.src,
alt: meetingImage.altText || meetingImage.altText_En || "",
}
}
break
default:
break
}
if (subpageImage) {
return subpageImage
}
}
const hotelImage =
data.additionalHotelData?.gallery?.heroImages?.[0] ||
data.additionalHotelData?.gallery?.smallerImages?.[0]
if (hotelImage) {
return {
url: hotelImage.src,
alt: hotelImage.altText || undefined,
}
}
}
if (heroImage) {
return {
url: heroImage.url,
alt: heroImage.meta.alt || undefined,
width: heroImage.dimensions.width,
height: heroImage.dimensions.height,
}
}
return undefined
}

View File

@@ -0,0 +1,27 @@
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
export function getDestinationFilterSeoMetaImage(data: RawMetadataSchema) {
const filter = data.destinationData?.filter
if (!filter) {
return null
}
const foundSeoFilter = data.seo_filters?.find(
(f) => f.filterConnection.edges[0]?.node?.slug === filter
)
if (foundSeoFilter) {
const metaDataImage = foundSeoFilter.seo_metadata?.seo_image
if (metaDataImage) {
return {
url: metaDataImage.url,
alt: metaDataImage.meta.alt || undefined,
width: metaDataImage.dimensions.width,
height: metaDataImage.dimensions.height,
}
}
}
return null
}

View File

@@ -0,0 +1,88 @@
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
export function getHotelPageImage(data: RawMetadataSchema) {
const { subpageUrl, hotelData, additionalHotelData, hotelRestaurants } = data
if (!hotelData) {
return null
}
if (subpageUrl) {
let subpageImage: { url: string; alt?: string } | null = null
const restaurantSubPage = hotelRestaurants?.find(
(restaurant) => restaurant.nameInUrl === subpageUrl
)
const restaurantImage = restaurantSubPage?.content?.images?.[0]
if (restaurantImage) {
subpageImage = {
url: restaurantImage.src,
alt: restaurantImage.altText || restaurantImage.altText_En || undefined,
}
} else {
switch (subpageUrl) {
case additionalHotelData?.hotelParking.nameInUrl:
const parkingImage = additionalHotelData?.parkingImages?.heroImages[0]
if (parkingImage) {
subpageImage = {
url: parkingImage.src,
alt: parkingImage.altText || parkingImage.altText_En || undefined,
}
}
break
case additionalHotelData?.healthAndFitness.nameInUrl:
const wellnessImage = hotelData.healthFacilities.find(
(fac) => fac.content.images.length
)?.content.images[0]
if (wellnessImage) {
subpageImage = {
url: wellnessImage.src,
alt:
wellnessImage.altText || wellnessImage.altText_En || undefined,
}
}
break
case additionalHotelData?.hotelSpecialNeeds.nameInUrl:
const accessibilityImage =
additionalHotelData?.accessibility?.heroImages[0]
if (accessibilityImage) {
subpageImage = {
url: accessibilityImage.src,
alt:
accessibilityImage.altText ||
accessibilityImage.altText_En ||
undefined,
}
}
break
case additionalHotelData?.meetingRooms.nameInUrl:
const meetingImage =
additionalHotelData?.conferencesAndMeetings?.heroImages[0]
if (meetingImage) {
subpageImage = {
url: meetingImage.src,
alt: meetingImage.altText || meetingImage.altText_En || undefined,
}
}
break
default:
break
}
}
if (subpageImage) {
return subpageImage
}
}
const hotelImage =
additionalHotelData?.gallery?.heroImages?.[0] ||
additionalHotelData?.gallery?.smallerImages?.[0]
if (hotelImage) {
return {
url: hotelImage.src,
alt: hotelImage.altText || undefined,
}
}
return null
}

View File

@@ -0,0 +1,59 @@
import { PageContentTypeEnum } from "@scandic-hotels/trpc/enums/contentType"
import { getDestinationFilterSeoMetaImage } from "@/utils/metadata/image/destinationPage"
import { getHotelPageImage } from "@/utils/metadata/image/hotelPage"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
export function getImage(data: RawMetadataSchema) {
const metadataImage = data.web?.seo_metadata?.seo_image
const isDestinationPage = [
PageContentTypeEnum.destinationCityPage,
PageContentTypeEnum.destinationCountryPage,
].includes(data.system.content_type_uid as PageContentTypeEnum)
if (isDestinationPage) {
const destinationFilterSeoMetaImage = getDestinationFilterSeoMetaImage(data)
if (destinationFilterSeoMetaImage) {
return destinationFilterSeoMetaImage
}
}
// Currently we don't have the possibility to get smaller images from ImageVault (2024-11-15)
if (metadataImage) {
return {
url: metadataImage.url,
alt: metadataImage.meta.alt || undefined,
width: metadataImage.dimensions.width,
height: metadataImage.dimensions.height,
}
}
let contentTypeImage: { url: string; alt?: string } | null = null
switch (data.system.content_type_uid) {
case PageContentTypeEnum.hotelPage:
contentTypeImage = getHotelPageImage(data)
break
default:
break
}
if (contentTypeImage) {
return contentTypeImage
}
// Fallback to hero image if no other image is found
const heroImage =
data.hero_image || data.header?.hero_image || data.images?.[0]
if (heroImage) {
return {
url: heroImage.url,
alt: heroImage.meta.alt || undefined,
width: heroImage.dimensions.width,
height: heroImage.dimensions.height,
}
}
return undefined
}

View File

@@ -1,214 +0,0 @@
import { getIntl } from "@/i18n"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
function getTitleSuffix(contentType: string) {
switch (contentType) {
case "content_page":
case "collection_page":
case "campaign_page":
case "campaign_overview_page":
case "destination_overview_page":
case "destination_city_page":
case "destination_country_page":
return " | Scandic Hotels"
default:
return ""
}
}
export async function getTitle(data: RawMetadataSchema) {
const intl = await getIntl()
const suffix = getTitleSuffix(data.system.content_type_uid)
const metadata = data.web?.seo_metadata
if (metadata?.title) {
return `${metadata.title}${suffix}`
}
if (data.system.content_type_uid === "hotel_page" && data.hotelData) {
if (data.subpageUrl) {
const restaurantSubPage = data.hotelRestaurants?.find(
(restaurant) => restaurant.nameInUrl === data.subpageUrl
)
if (restaurantSubPage) {
const restaurantTitleLong = intl.formatMessage(
{
defaultMessage:
"Explore {restaurantName} at {hotelName} in {destination}",
},
{
restaurantName: restaurantSubPage.name,
hotelName: data.hotelData.name,
destination: data.hotelData.translatedCityName,
}
)
const restaurantTitleShort = intl.formatMessage(
{
defaultMessage: "Explore {restaurantName} at {hotelName}",
},
{
restaurantName: restaurantSubPage.name,
hotelName: data.hotelData.name,
}
)
if (restaurantTitleLong.length > 60) {
return restaurantTitleShort
}
return restaurantTitleLong
}
switch (data.subpageUrl) {
case data.additionalHotelData?.hotelParking.nameInUrl:
const parkingTitleLong = intl.formatMessage(
{
defaultMessage:
"Parking information for {hotelName} in {destination}",
},
{
hotelName: data.hotelData.name,
destination: data.hotelData.translatedCityName,
}
)
const parkingTitleShort = intl.formatMessage(
{ defaultMessage: "Parking information for {hotelName}" },
{ hotelName: data.hotelData.name }
)
if (parkingTitleLong.length > 60) {
return parkingTitleShort
}
return parkingTitleLong
case data.additionalHotelData?.healthAndFitness.nameInUrl:
const wellnessTitleLong = intl.formatMessage(
{
defaultMessage:
"Gym & health facilities at {hotelName} in {destination}",
},
{
hotelName: data.hotelData.name,
destination: data.hotelData.translatedCityName,
}
)
const wellnessTitleShort = intl.formatMessage(
{
defaultMessage: "Gym & health facilities at {hotelName}",
},
{
hotelName: data.hotelData.name,
}
)
if (wellnessTitleLong.length > 60) {
return wellnessTitleShort
}
return wellnessTitleLong
case data.additionalHotelData?.hotelSpecialNeeds.nameInUrl:
const accessibilityTitleLong = intl.formatMessage(
{
defaultMessage:
"Accessibility information for {hotelName} in {destination}",
},
{
hotelName: data.hotelData.name,
destination: data.hotelData.translatedCityName,
}
)
const accessibilityTitleShort = intl.formatMessage(
{
defaultMessage: "Accessibility information for {hotelName}",
},
{
hotelName: data.hotelData.name,
}
)
if (accessibilityTitleLong.length > 60) {
return accessibilityTitleShort
}
return accessibilityTitleLong
case data.additionalHotelData?.meetingRooms.nameInUrl:
const meetingsTitleLong = intl.formatMessage(
{
defaultMessage:
"Meetings & conferences at {hotelName} in {destination}",
},
{
hotelName: data.hotelData.name,
destination: data.hotelData.translatedCityName,
}
)
const meetingsTitleShort = intl.formatMessage(
{
defaultMessage: "Meetings & conferences at {hotelName}",
},
{
hotelName: data.hotelData.name,
}
)
if (meetingsTitleLong.length > 60) {
return meetingsTitleShort
}
return meetingsTitleLong
default:
break
}
}
return intl.formatMessage(
{
defaultMessage: "Stay at {hotelName} | Hotel in {destination}",
},
{
hotelName: data.hotelData.name,
destination: data.hotelData.translatedCityName,
}
)
}
if (
data.system.content_type_uid === "destination_city_page" ||
data.system.content_type_uid === "destination_country_page"
) {
if (data.destinationData) {
const { location, filter, filterType } = data.destinationData
if (location) {
if (filter) {
if (filterType === "facility") {
return intl.formatMessage(
{
defaultMessage: "Hotels with {filter} in {location}",
},
{ location, filter }
)
} else if (filterType === "surroundings") {
return intl.formatMessage(
{
defaultMessage: "Hotels near {filter} in {location}",
},
{ location, filter }
)
}
}
const destinationTitle = intl.formatMessage(
{
defaultMessage: "Hotels in {location}",
},
{ location }
)
return `${destinationTitle}${suffix}`
}
}
}
if (data.web?.breadcrumbs?.title) {
return `${data.web.breadcrumbs.title}${suffix}`
}
if (data.heading) {
return `${data.heading}${suffix}`
}
if (data.header?.heading) {
return `${data.header.heading}${suffix}`
}
return ""
}

View File

@@ -0,0 +1,61 @@
import { getIntl } from "@/i18n"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
export async function getDestinationPageTitle(
data: RawMetadataSchema,
pageType: "city" | "country",
suffix: string
) {
const intl = await getIntl()
const { destinationData } = data
if (!destinationData) {
return null
}
const location = destinationData.location
if (!location) {
return null
}
const destinationTitle =
pageType === "country"
? intl.formatMessage(
{
defaultMessage: "Destinations in {location}",
},
{ location }
)
: intl.formatMessage(
{
defaultMessage: "Hotels in {location}",
},
{ location }
)
return `${destinationTitle}${suffix}`
}
export function getDestinationFilterSeoMetaTitle(
data: RawMetadataSchema,
suffix: string
) {
const filter = data.destinationData?.filter
if (!filter) {
return null
}
const foundSeoFilter = data.seo_filters?.find(
(f) => f.filterConnection.edges[0]?.node?.slug === filter
)
if (foundSeoFilter) {
if (foundSeoFilter.seo_metadata?.title) {
return `${foundSeoFilter.seo_metadata.title}${suffix}`
}
return `${foundSeoFilter.heading}${suffix}`
}
return null
}

View File

@@ -0,0 +1,148 @@
import { getIntl } from "@/i18n"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
async function getSubpageTitle(
subpageUrl: string,
additionalHotelData: RawMetadataSchema["additionalHotelData"],
hotelRestaurants: RawMetadataSchema["hotelRestaurants"],
hotelName: string,
destination: string
) {
const intl = await getIntl()
const restaurantSubPage = hotelRestaurants?.find(
(restaurant) => restaurant.nameInUrl === subpageUrl
)
if (restaurantSubPage) {
const restaurantTitleLong = intl.formatMessage(
{
defaultMessage:
"Explore {restaurantName} at {hotelName} in {destination}",
},
{
restaurantName: restaurantSubPage.name,
hotelName,
destination,
}
)
const restaurantTitleShort = intl.formatMessage(
{
defaultMessage: "Explore {restaurantName} at {hotelName}",
},
{
restaurantName: restaurantSubPage.name,
hotelName,
}
)
if (restaurantTitleLong.length > 60) {
return restaurantTitleShort
}
return restaurantTitleLong
}
if (!additionalHotelData) {
return null
}
switch (subpageUrl) {
case additionalHotelData.hotelParking.nameInUrl:
const parkingTitleLong = intl.formatMessage(
{
defaultMessage:
"Parking information for {hotelName} in {destination}",
},
{ hotelName, destination }
)
const parkingTitleShort = intl.formatMessage(
{ defaultMessage: "Parking information for {hotelName}" },
{ hotelName }
)
if (parkingTitleLong.length > 60) {
return parkingTitleShort
}
return parkingTitleLong
case additionalHotelData.healthAndFitness.nameInUrl:
const wellnessTitleLong = intl.formatMessage(
{
defaultMessage:
"Gym & health facilities at {hotelName} in {destination}",
},
{ hotelName, destination }
)
const wellnessTitleShort = intl.formatMessage(
{ defaultMessage: "Gym & health facilities at {hotelName}" },
{ hotelName }
)
if (wellnessTitleLong.length > 60) {
return wellnessTitleShort
}
return wellnessTitleLong
case additionalHotelData.hotelSpecialNeeds.nameInUrl:
const accessibilityTitleLong = intl.formatMessage(
{
defaultMessage:
"Accessibility information for {hotelName} in {destination}",
},
{ hotelName, destination }
)
const accessibilityTitleShort = intl.formatMessage(
{ defaultMessage: "Accessibility information for {hotelName}" },
{ hotelName }
)
if (accessibilityTitleLong.length > 60) {
return accessibilityTitleShort
}
return accessibilityTitleLong
case additionalHotelData.meetingRooms.nameInUrl:
const meetingsTitleLong = intl.formatMessage(
{
defaultMessage:
"Meetings & conferences at {hotelName} in {destination}",
},
{ hotelName, destination }
)
const meetingsTitleShort = intl.formatMessage(
{ defaultMessage: "Meetings & conferences at {hotelName}" },
{ hotelName }
)
if (meetingsTitleLong.length > 60) {
return meetingsTitleShort
}
return meetingsTitleLong
default:
return null
}
}
export async function getHotelPageTitle(data: RawMetadataSchema) {
const intl = await getIntl()
const { subpageUrl, hotelData, additionalHotelData, hotelRestaurants } = data
if (!hotelData) {
return null
}
const hotelName = hotelData.name
const destination = hotelData.translatedCityName
if (subpageUrl) {
const subpageTitle = await getSubpageTitle(
subpageUrl,
additionalHotelData,
hotelRestaurants,
hotelName,
destination
)
return subpageTitle
}
return intl.formatMessage(
{ defaultMessage: "Stay at {hotelName} | Hotel in {destination}" },
{ hotelName, destination }
)
}

View File

@@ -0,0 +1,79 @@
import { PageContentTypeEnum } from "@scandic-hotels/trpc/enums/contentType"
import {
getDestinationFilterSeoMetaTitle,
getDestinationPageTitle,
} from "./destinationPage"
import { getHotelPageTitle } from "./hotelPage"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
function getTitleSuffix(contentType: string) {
switch (contentType) {
case PageContentTypeEnum.contentPage:
case PageContentTypeEnum.collectionPage:
case PageContentTypeEnum.campaignPage:
case PageContentTypeEnum.campaignOverviewPage:
case PageContentTypeEnum.destinationOverviewPage:
case PageContentTypeEnum.destinationCityPage:
case PageContentTypeEnum.destinationCountryPage:
return " | Scandic Hotels"
default:
return ""
}
}
export async function getTitle(data: RawMetadataSchema) {
const suffix = getTitleSuffix(data.system.content_type_uid)
const metadata = data.web?.seo_metadata
const isDestinationPage = [
PageContentTypeEnum.destinationCityPage,
PageContentTypeEnum.destinationCountryPage,
].includes(data.system.content_type_uid as PageContentTypeEnum)
if (isDestinationPage) {
const destinationFilterSeoMetaTitle = getDestinationFilterSeoMetaTitle(
data,
suffix
)
if (destinationFilterSeoMetaTitle) {
return destinationFilterSeoMetaTitle
}
}
if (metadata?.title) {
return `${metadata.title}${suffix}`
}
let title: string | null = null
switch (data.system.content_type_uid) {
case PageContentTypeEnum.hotelPage:
title = await getHotelPageTitle(data)
break
case PageContentTypeEnum.destinationCityPage:
title = await getDestinationPageTitle(data, "city", suffix)
break
case PageContentTypeEnum.destinationCountryPage:
title = await getDestinationPageTitle(data, "country", suffix)
break
default:
break
}
if (title) {
return title
}
// Fallback titles from contentstack content
if (data.web?.breadcrumbs?.title) {
return `${data.web.breadcrumbs.title}${suffix}`
}
if (data.heading) {
return `${data.heading}${suffix}`
}
if (data.header?.heading) {
return `${data.header.heading}${suffix}`
}
return ""
}

View File

@@ -24,6 +24,11 @@ query GetDestinationCityPageMetadata($locale: String!, $uid: String!) {
image
}
seo_filters {
heading
preamble
seo_metadata {
...Metadata
}
filterConnection {
edges {
node {

View File

@@ -1,5 +1,6 @@
#import "../../Fragments/Metadata.graphql"
#import "../../Fragments/System.graphql"
#import "../../Fragments/HotelFilter.graphql"
query GetDestinationCountryPageMetadata($locale: String!, $uid: String!) {
destination_country_page(locale: $locale, uid: $uid) {
@@ -17,6 +18,20 @@ query GetDestinationCountryPageMetadata($locale: String!, $uid: String!) {
images {
image
}
seo_filters {
heading
preamble
seo_metadata {
...Metadata
}
filterConnection {
edges {
node {
...HotelFilter
}
}
}
}
system {
...System
}

View File

@@ -12,7 +12,7 @@ import { Country } from "../../../types/country"
import { RTETypeEnum } from "../../../types/RTEenums"
import { additionalDataAttributesSchema } from "../../hotels/schemas/hotel/include/additionalData"
import { imageSchema } from "../../hotels/schemas/image"
import { destinationFiltersSchema } from "../schemas/destinationFilters"
import { destinationFilterSchema } from "../schemas/destinationFilters"
import { systemSchema } from "../schemas/system"
import type { Lang } from "@scandic-hotels/common/constants/language"
@@ -52,17 +52,19 @@ const metaDataBlocksSchema = z
.optional()
.nullable()
export const seoMetadataSchema = z
.object({
title: z.string().nullish(),
description: z.string().nullish(),
noindex: z.boolean().nullish(),
seo_image: transformedImageVaultAssetSchema,
})
.nullish()
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: transformedImageVaultAssetSchema,
})
.nullish(),
seo_metadata: seoMetadataSchema,
breadcrumbs: z
.object({
title: z.string().nullish(),
@@ -167,7 +169,13 @@ export const rawMetadataSchema = z.object({
cities: z.array(z.string()).nullish(),
})
.nullish(),
seo_filters: destinationFiltersSchema,
seo_filters: z
.array(
destinationFilterSchema.merge(
z.object({ seo_metadata: seoMetadataSchema })
)
)
.nullish(),
system: systemSchema,
})

View File

@@ -71,16 +71,17 @@ export async function getCityData(
hotelFilters,
seoFilters
)
const facilityFilter = allFilters.facilityFilters.find(
(f) => f.slug === filter
)
const surroudingsFilter = allFilters.surroundingsFilters.find(
const surroundingsFilter = allFilters.surroundingsFilters.find(
(f) => f.slug === filter
)
if (facilityFilter) {
filterType = "facility"
} else if (surroudingsFilter) {
} else if (surroundingsFilter) {
filterType = "surroundings"
}
}
@@ -123,13 +124,13 @@ export async function getCountryData(
const facilityFilter = allFilters.facilityFilters.find(
(f) => f.slug === filter
)
const surroudingsFilter = allFilters.surroundingsFilters.find(
const surroundingsFilter = allFilters.surroundingsFilters.find(
(f) => f.slug === filter
)
if (facilityFilter) {
filterType = "facility"
} else if (surroudingsFilter) {
} else if (surroundingsFilter) {
filterType = "surroundings"
}
}

View File

@@ -30,29 +30,25 @@ export const blocksSchema = z.discriminatedUnion("__typename", [
destinationFilterBlockContent,
])
export const destinationFiltersSchema = z
.array(
z.object({
heading: z.string().nullish(),
preamble: z.string().nullish(),
blocks: discriminatedUnionArray(blocksSchema.options).nullish(),
filterConnection: z.object({
edges: z.array(
z.object({
node: z.object({
title: z.string(),
facility_id: z
.nativeEnum(FacilityEnum)
.catch(FacilityEnum.UNKNOWN),
category: z.string(),
slug: z.string(),
}),
})
),
}),
})
)
.nullish()
export const destinationFilterSchema = z.object({
heading: z.string().nullish(),
preamble: z.string().nullish(),
blocks: discriminatedUnionArray(blocksSchema.options).nullish(),
filterConnection: z.object({
edges: z.array(
z.object({
node: z.object({
title: z.string(),
facility_id: z.nativeEnum(FacilityEnum).catch(FacilityEnum.UNKNOWN),
category: z.string(),
slug: z.string(),
}),
})
),
}),
})
const destinationFiltersSchema = z.array(destinationFilterSchema).nullish()
export const transformedDestinationFiltersSchema =
destinationFiltersSchema.transform((data) =>