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) =>