From 9f02870647b6e7fa454f5e5ac5a7d1e62f218d8e Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 25 Sep 2025 13:26:00 +0000 Subject: [PATCH] feat(BOOK-57): Adjusted metadata for destination pages with active seo filter Approved-by: Chuma Mcphoy (We Ahead) --- .../CityListing/CityListingSkeleton.tsx | 2 +- .../HotelListing/HotelListingSkeleton.tsx | 2 +- .../description/destinationCityPage.ts | 43 ---- .../description/destinationCountryPage.ts | 52 ----- .../metadata/description/destinationPage.ts | 110 +++++++++ .../utils/metadata/description/index.ts | 20 +- .../utils/metadata/generateMetadata.ts | 27 ++- apps/scandic-web/utils/metadata/image.ts | 105 --------- .../utils/metadata/image/destinationPage.ts | 27 +++ .../utils/metadata/image/hotelPage.ts | 88 +++++++ .../scandic-web/utils/metadata/image/index.ts | 59 +++++ apps/scandic-web/utils/metadata/title.ts | 214 ------------------ .../utils/metadata/title/destinationPage.ts | 61 +++++ .../utils/metadata/title/hotelPage.ts | 148 ++++++++++++ .../scandic-web/utils/metadata/title/index.ts | 79 +++++++ .../DestinationCityPage/Metadata.graphql | 5 + .../DestinationCountryPage/Metadata.graphql | 15 ++ .../routers/contentstack/metadata/output.ts | 28 ++- .../routers/contentstack/metadata/utils.ts | 9 +- .../schemas/destinationFilters.ts | 42 ++-- 20 files changed, 678 insertions(+), 458 deletions(-) delete mode 100644 apps/scandic-web/utils/metadata/description/destinationCityPage.ts delete mode 100644 apps/scandic-web/utils/metadata/description/destinationCountryPage.ts create mode 100644 apps/scandic-web/utils/metadata/description/destinationPage.ts delete mode 100644 apps/scandic-web/utils/metadata/image.ts create mode 100644 apps/scandic-web/utils/metadata/image/destinationPage.ts create mode 100644 apps/scandic-web/utils/metadata/image/hotelPage.ts create mode 100644 apps/scandic-web/utils/metadata/image/index.ts delete mode 100644 apps/scandic-web/utils/metadata/title.ts create mode 100644 apps/scandic-web/utils/metadata/title/destinationPage.ts create mode 100644 apps/scandic-web/utils/metadata/title/hotelPage.ts create mode 100644 apps/scandic-web/utils/metadata/title/index.ts diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationListing/CityListing/CityListingSkeleton.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationListing/CityListing/CityListingSkeleton.tsx index 11e5670e5..76be2ca68 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationListing/CityListing/CityListingSkeleton.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationListing/CityListing/CityListingSkeleton.tsx @@ -13,7 +13,7 @@ export default function CityListingSkeleton() { -
    +
      {Array.from({ length: 3 }).map((_, index) => (
    • diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationListing/HotelListing/HotelListingSkeleton.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationListing/HotelListing/HotelListingSkeleton.tsx index 327c4fba6..9dbdab293 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationListing/HotelListing/HotelListingSkeleton.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationListing/HotelListing/HotelListingSkeleton.tsx @@ -13,7 +13,7 @@ export default function HotelListingSkeleton() { -
        +
          {Array.from({ length: 3 }).map((_, index) => (
        • diff --git a/apps/scandic-web/utils/metadata/description/destinationCityPage.ts b/apps/scandic-web/utils/metadata/description/destinationCityPage.ts deleted file mode 100644 index b00396d70..000000000 --- a/apps/scandic-web/utils/metadata/description/destinationCityPage.ts +++ /dev/null @@ -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) -} diff --git a/apps/scandic-web/utils/metadata/description/destinationCountryPage.ts b/apps/scandic-web/utils/metadata/description/destinationCountryPage.ts deleted file mode 100644 index ff6c817e1..000000000 --- a/apps/scandic-web/utils/metadata/description/destinationCountryPage.ts +++ /dev/null @@ -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) -} diff --git a/apps/scandic-web/utils/metadata/description/destinationPage.ts b/apps/scandic-web/utils/metadata/description/destinationPage.ts new file mode 100644 index 000000000..7a62c2ba2 --- /dev/null +++ b/apps/scandic-web/utils/metadata/description/destinationPage.ts @@ -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 +} diff --git a/apps/scandic-web/utils/metadata/description/index.ts b/apps/scandic-web/utils/metadata/description/index.ts index 67b42a628..8411f5691 100644 --- a/apps/scandic-web/utils/metadata/description/index.ts +++ b/apps/scandic-web/utils/metadata/description/index.ts @@ -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 diff --git a/apps/scandic-web/utils/metadata/generateMetadata.ts b/apps/scandic-web/utils/metadata/generateMetadata.ts index a7b952866..1c3577a85 100644 --- a/apps/scandic-web/utils/metadata/generateMetadata.ts +++ b/apps/scandic-web/utils/metadata/generateMetadata.ts @@ -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, diff --git a/apps/scandic-web/utils/metadata/image.ts b/apps/scandic-web/utils/metadata/image.ts deleted file mode 100644 index 2207e748f..000000000 --- a/apps/scandic-web/utils/metadata/image.ts +++ /dev/null @@ -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 -} diff --git a/apps/scandic-web/utils/metadata/image/destinationPage.ts b/apps/scandic-web/utils/metadata/image/destinationPage.ts new file mode 100644 index 000000000..b1018fe77 --- /dev/null +++ b/apps/scandic-web/utils/metadata/image/destinationPage.ts @@ -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 +} diff --git a/apps/scandic-web/utils/metadata/image/hotelPage.ts b/apps/scandic-web/utils/metadata/image/hotelPage.ts new file mode 100644 index 000000000..352f316da --- /dev/null +++ b/apps/scandic-web/utils/metadata/image/hotelPage.ts @@ -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 +} diff --git a/apps/scandic-web/utils/metadata/image/index.ts b/apps/scandic-web/utils/metadata/image/index.ts new file mode 100644 index 000000000..e71b14138 --- /dev/null +++ b/apps/scandic-web/utils/metadata/image/index.ts @@ -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 +} diff --git a/apps/scandic-web/utils/metadata/title.ts b/apps/scandic-web/utils/metadata/title.ts deleted file mode 100644 index b1d16fdce..000000000 --- a/apps/scandic-web/utils/metadata/title.ts +++ /dev/null @@ -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 "" -} diff --git a/apps/scandic-web/utils/metadata/title/destinationPage.ts b/apps/scandic-web/utils/metadata/title/destinationPage.ts new file mode 100644 index 000000000..a736513a5 --- /dev/null +++ b/apps/scandic-web/utils/metadata/title/destinationPage.ts @@ -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 +} diff --git a/apps/scandic-web/utils/metadata/title/hotelPage.ts b/apps/scandic-web/utils/metadata/title/hotelPage.ts new file mode 100644 index 000000000..33f0096e4 --- /dev/null +++ b/apps/scandic-web/utils/metadata/title/hotelPage.ts @@ -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 } + ) +} diff --git a/apps/scandic-web/utils/metadata/title/index.ts b/apps/scandic-web/utils/metadata/title/index.ts new file mode 100644 index 000000000..67f5adadf --- /dev/null +++ b/apps/scandic-web/utils/metadata/title/index.ts @@ -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 "" +} diff --git a/packages/trpc/lib/graphql/Query/DestinationCityPage/Metadata.graphql b/packages/trpc/lib/graphql/Query/DestinationCityPage/Metadata.graphql index 02495e2b3..64e3cd808 100644 --- a/packages/trpc/lib/graphql/Query/DestinationCityPage/Metadata.graphql +++ b/packages/trpc/lib/graphql/Query/DestinationCityPage/Metadata.graphql @@ -24,6 +24,11 @@ query GetDestinationCityPageMetadata($locale: String!, $uid: String!) { image } seo_filters { + heading + preamble + seo_metadata { + ...Metadata + } filterConnection { edges { node { diff --git a/packages/trpc/lib/graphql/Query/DestinationCountryPage/Metadata.graphql b/packages/trpc/lib/graphql/Query/DestinationCountryPage/Metadata.graphql index e1837d3b3..0c12482a4 100644 --- a/packages/trpc/lib/graphql/Query/DestinationCountryPage/Metadata.graphql +++ b/packages/trpc/lib/graphql/Query/DestinationCountryPage/Metadata.graphql @@ -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 } diff --git a/packages/trpc/lib/routers/contentstack/metadata/output.ts b/packages/trpc/lib/routers/contentstack/metadata/output.ts index 3d390aded..11846ddbb 100644 --- a/packages/trpc/lib/routers/contentstack/metadata/output.ts +++ b/packages/trpc/lib/routers/contentstack/metadata/output.ts @@ -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, }) diff --git a/packages/trpc/lib/routers/contentstack/metadata/utils.ts b/packages/trpc/lib/routers/contentstack/metadata/utils.ts index 8ddb46f1e..66952e292 100644 --- a/packages/trpc/lib/routers/contentstack/metadata/utils.ts +++ b/packages/trpc/lib/routers/contentstack/metadata/utils.ts @@ -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" } } diff --git a/packages/trpc/lib/routers/contentstack/schemas/destinationFilters.ts b/packages/trpc/lib/routers/contentstack/schemas/destinationFilters.ts index 512123d46..efd0c3973 100644 --- a/packages/trpc/lib/routers/contentstack/schemas/destinationFilters.ts +++ b/packages/trpc/lib/routers/contentstack/schemas/destinationFilters.ts @@ -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) =>