Feat/SW-2152 seo descriptions
* feat(SW-2152): Added improved meta descriptions for hotel pages * feat(SW-2152): Added improved meta descriptions for destination pages * feat(SW-2152): Refactoring metadata description functionality * feat(SW-2152): Improved truncate function and added cities check to country page description Approved-by: Michael Zetterberg Approved-by: Matilda Landström
This commit is contained in:
@@ -19,6 +19,9 @@ query GetDestinationCityPageMetadata($locale: String!, $uid: String!) {
|
||||
city_poland
|
||||
city_sweden
|
||||
}
|
||||
images {
|
||||
image
|
||||
}
|
||||
system {
|
||||
...System
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ query GetDestinationCountryPageMetadata($locale: String!, $uid: String!) {
|
||||
destination_settings {
|
||||
country
|
||||
}
|
||||
images {
|
||||
image
|
||||
}
|
||||
system {
|
||||
...System
|
||||
}
|
||||
|
||||
@@ -11,10 +11,13 @@ import { additionalDataAttributesSchema } from "../../hotels/schemas/hotel/inclu
|
||||
import { imageSchema } from "../../hotels/schemas/image"
|
||||
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
|
||||
import { systemSchema } from "../schemas/system"
|
||||
import { getDescription, getImage, getTitle } from "./utils"
|
||||
import { getDescription } from "./utils/description"
|
||||
import { getImage } from "./utils/image"
|
||||
import { getTitle } from "./utils/title"
|
||||
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import type { ImageVaultAsset } from "@/types/components/imageVault"
|
||||
import { Country } from "@/types/enums/country"
|
||||
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
|
||||
import { RTETypeEnum } from "@/types/rte/enums"
|
||||
@@ -57,56 +60,59 @@ export const rawMetadataSchema = z.object({
|
||||
.object({
|
||||
seo_metadata: z
|
||||
.object({
|
||||
title: z.string().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
noindex: z.boolean().optional().nullable(),
|
||||
title: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
noindex: z.boolean().nullish(),
|
||||
seo_image: tempImageVaultAssetSchema.nullable(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
.nullish(),
|
||||
breadcrumbs: z
|
||||
.object({
|
||||
title: z.string().optional().nullable(),
|
||||
title: z.string().nullish(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
.nullish(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
.nullish(),
|
||||
destination_settings: z
|
||||
.object({
|
||||
city_denmark: z.string().optional().nullable(),
|
||||
city_finland: z.string().optional().nullable(),
|
||||
city_germany: z.string().optional().nullable(),
|
||||
city_poland: z.string().optional().nullable(),
|
||||
city_norway: z.string().optional().nullable(),
|
||||
city_sweden: z.string().optional().nullable(),
|
||||
country: z.nativeEnum(Country).optional().nullable(),
|
||||
city_denmark: z.string().nullish(),
|
||||
city_finland: z.string().nullish(),
|
||||
city_germany: z.string().nullish(),
|
||||
city_poland: z.string().nullish(),
|
||||
city_norway: z.string().nullish(),
|
||||
city_sweden: z.string().nullish(),
|
||||
country: z.nativeEnum(Country).nullish(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
heading: z.string().optional().nullable(),
|
||||
preamble: z.string().optional().nullable(),
|
||||
.nullish(),
|
||||
heading: z.string().nullish(),
|
||||
preamble: z.string().nullish(),
|
||||
header: z
|
||||
.object({
|
||||
heading: z.string().optional().nullable(),
|
||||
preamble: z.string().optional().nullable(),
|
||||
heading: z.string().nullish(),
|
||||
preamble: z.string().nullish(),
|
||||
hero_image: tempImageVaultAssetSchema.nullable(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
.nullish(),
|
||||
hero_image: tempImageVaultAssetSchema.nullable(),
|
||||
images: z
|
||||
.array(z.object({ image: tempImageVaultAssetSchema }).nullish())
|
||||
.transform((images) =>
|
||||
images
|
||||
.map((image) => image?.image)
|
||||
.filter((image): image is ImageVaultAsset => !!image)
|
||||
)
|
||||
.nullish(),
|
||||
blocks: metaDataBlocksSchema,
|
||||
hotel_page_id: z.string().optional().nullable(),
|
||||
hotel_page_id: z.string().nullish(),
|
||||
hotelData: hotelAttributesSchema
|
||||
.pick({
|
||||
name: true,
|
||||
address: true,
|
||||
detailedFacilities: true,
|
||||
hotelContent: true,
|
||||
healthFacilities: true,
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
.nullish(),
|
||||
additionalHotelData: additionalDataAttributesSchema
|
||||
.pick({
|
||||
gallery: true,
|
||||
@@ -118,28 +124,31 @@ export const rawMetadataSchema = z.object({
|
||||
accessibility: true,
|
||||
conferencesAndMeetings: true,
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
.nullish(),
|
||||
hotelRestaurants: z
|
||||
.array(
|
||||
z.object({
|
||||
nameInUrl: z.string().optional().nullable(),
|
||||
elevatorPitch: z.string().optional().nullable(),
|
||||
name: z.string().optional().nullable(),
|
||||
nameInUrl: z.string().nullish(),
|
||||
elevatorPitch: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
content: z
|
||||
.object({
|
||||
images: z.array(imageSchema).optional().nullable(),
|
||||
images: z.array(imageSchema).nullish(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
.nullish(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.nullable(),
|
||||
subpageUrl: z.string().optional().nullable(),
|
||||
location: z.string().optional().nullable(),
|
||||
filter: z.string().optional().nullable(),
|
||||
filterType: z.enum(["facility", "surroundings"]).optional().nullable(),
|
||||
.nullish(),
|
||||
subpageUrl: z.string().nullish(),
|
||||
destinationData: z
|
||||
.object({
|
||||
location: z.string().nullish(),
|
||||
filter: z.string().nullish(),
|
||||
filterType: z.enum(["facility", "surroundings"]).nullish(),
|
||||
hotelCount: z.number().nullish(),
|
||||
cities: z.array(z.string()).nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
system: systemSchema,
|
||||
})
|
||||
|
||||
@@ -149,7 +158,7 @@ export const metadataSchema = rawMetadataSchema.transform(async (data) => {
|
||||
const metadata: Metadata = {
|
||||
metadataBase: env.PUBLIC_URL ? new URL(env.PUBLIC_URL) : undefined,
|
||||
title: await getTitle(data),
|
||||
description: getDescription(data),
|
||||
description: await getDescription(data),
|
||||
openGraph: {
|
||||
images: getImage(data),
|
||||
},
|
||||
|
||||
@@ -163,7 +163,7 @@ export const metadataQueryRouter = router({
|
||||
)
|
||||
metadata = await getTransformedMetadata({
|
||||
...destinationCountryPageResponse.destination_country_page,
|
||||
...countryData,
|
||||
destinationData: countryData,
|
||||
})
|
||||
break
|
||||
case PageContentTypeEnum.destinationCityPage:
|
||||
@@ -178,7 +178,7 @@ export const metadataQueryRouter = router({
|
||||
)
|
||||
metadata = await getTransformedMetadata({
|
||||
...destinationCityPageResponse.destination_city_page,
|
||||
...cityData,
|
||||
destinationData: cityData,
|
||||
})
|
||||
break
|
||||
case PageContentTypeEnum.loyaltyPage:
|
||||
|
||||
@@ -1,485 +0,0 @@
|
||||
import { type Lang } from "@/constants/languages"
|
||||
import { getFiltersFromHotels } from "@/stores/destination-data/helper"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import {
|
||||
getCityByCityIdentifier,
|
||||
getHotelIdsByCityIdentifier,
|
||||
getHotelIdsByCountry,
|
||||
getHotelsByHotelIds,
|
||||
} from "../../hotels/utils"
|
||||
|
||||
import { ApiCountry } from "@/types/enums/country"
|
||||
import { RTETypeEnum } from "@/types/rte/enums"
|
||||
import type {
|
||||
MetadataInputSchema,
|
||||
RawMetadataSchema,
|
||||
} from "@/types/trpc/routers/contentstack/metadata"
|
||||
|
||||
export const affix = "metadata"
|
||||
|
||||
/**
|
||||
* Truncates the given text "intelligently" based on the last period found near the max length.
|
||||
*
|
||||
* - If a period exists within the extended range (`maxLength` to `maxLength + maxExtension`),
|
||||
* the function truncates after the closest period to `maxLength`.
|
||||
* - If no period is found in the range, it truncates the text after the last period found in the max length of the text.
|
||||
* - If no periods exist at all, it truncates at `maxLength` and appends ellipsis (`...`).
|
||||
*
|
||||
* @param {string} text - The input text to be truncated.
|
||||
* @param {number} [maxLength=150] - The desired maximum length of the truncated text.
|
||||
* @param {number} [minLength=120] - The minimum allowable length for the truncated text.
|
||||
* @param {number} [maxExtension=10] - The maximum number of characters to extend beyond `maxLength` to find a period.
|
||||
* @returns {string} - The truncated text.
|
||||
*/
|
||||
function truncateTextAfterLastPeriod(
|
||||
text: string,
|
||||
maxLength: number = 150,
|
||||
minLength: number = 120,
|
||||
maxExtension: number = 10
|
||||
): string {
|
||||
if (text.length <= maxLength) {
|
||||
return text
|
||||
}
|
||||
|
||||
// Define the extended range
|
||||
const extendedEnd = Math.min(text.length, maxLength + maxExtension)
|
||||
const extendedText = text.slice(0, extendedEnd)
|
||||
|
||||
// Find all periods within the extended range and filter after minLength to get valid periods
|
||||
const periodsInRange = [...extendedText.matchAll(/\./g)].map(
|
||||
({ index }) => index
|
||||
)
|
||||
const validPeriods = periodsInRange.filter((index) => index + 1 >= minLength)
|
||||
|
||||
if (validPeriods.length > 0) {
|
||||
// Find the period closest to maxLength
|
||||
const closestPeriod = validPeriods.reduce((closest, index) =>
|
||||
Math.abs(index + 1 - maxLength) < Math.abs(closest + 1 - maxLength)
|
||||
? index
|
||||
: closest
|
||||
)
|
||||
return extendedText.slice(0, closestPeriod + 1)
|
||||
}
|
||||
|
||||
// Fallback: If no period is found within the valid range, look for the last period in the truncated text
|
||||
const maxLengthText = text.slice(0, maxLength)
|
||||
const lastPeriodIndex = maxLengthText.lastIndexOf(".")
|
||||
if (lastPeriodIndex !== -1) {
|
||||
return text.slice(0, lastPeriodIndex + 1)
|
||||
}
|
||||
|
||||
// Final fallback: Return maxLength text including ellipsis
|
||||
return `${maxLengthText}...`
|
||||
}
|
||||
|
||||
export async function getTitle(data: RawMetadataSchema) {
|
||||
const intl = await getIntl()
|
||||
const metadata = data.web?.seo_metadata
|
||||
if (metadata?.title) {
|
||||
return metadata.title
|
||||
}
|
||||
if (data.hotelData) {
|
||||
if (data.subpageUrl) {
|
||||
const restaurantSubPage = data.hotelRestaurants?.find(
|
||||
(restaurant) => restaurant.nameInUrl === data.subpageUrl
|
||||
)
|
||||
if (restaurantSubPage) {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"Explore {restaurantName} at {hotelName} in {destination}",
|
||||
},
|
||||
{
|
||||
restaurantName: restaurantSubPage.name,
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
switch (data.subpageUrl) {
|
||||
case data.additionalHotelData?.hotelParking.nameInUrl:
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"Parking information for {hotelName} in {destination}",
|
||||
},
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
case data.additionalHotelData?.healthAndFitness.nameInUrl:
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"Gym & Health Facilities at {hotelName} in {destination}",
|
||||
},
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
case data.additionalHotelData?.hotelSpecialNeeds.nameInUrl:
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"Accessibility information for {hotelName} in {destination}",
|
||||
},
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
case data.additionalHotelData?.meetingRooms.nameInUrl:
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"Meetings, Conferences & Events at {hotelName} in {destination}",
|
||||
},
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Stay at {hotelName} | Hotel in {destination}",
|
||||
},
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
}
|
||||
if (
|
||||
data.system.content_type_uid === "destination_city_page" ||
|
||||
data.system.content_type_uid === "destination_country_page"
|
||||
) {
|
||||
const { location, filter, filterType } = data
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Hotels in {location}",
|
||||
},
|
||||
{ location }
|
||||
)
|
||||
}
|
||||
}
|
||||
if (data.web?.breadcrumbs?.title) {
|
||||
return data.web.breadcrumbs.title
|
||||
}
|
||||
if (data.heading) {
|
||||
return data.heading
|
||||
}
|
||||
if (data.header?.heading) {
|
||||
return data.header.heading
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
export function getDescription(data: RawMetadataSchema) {
|
||||
const metadata = data.web?.seo_metadata
|
||||
if (metadata?.description) {
|
||||
return metadata.description
|
||||
}
|
||||
if (data.hotelData) {
|
||||
if (data.subpageUrl) {
|
||||
let subpageDescription: string | undefined
|
||||
const restaurantSubPage = data.hotelRestaurants?.find(
|
||||
(restaurant) => restaurant.nameInUrl === data.subpageUrl
|
||||
)
|
||||
if (restaurantSubPage?.elevatorPitch) {
|
||||
subpageDescription = restaurantSubPage.elevatorPitch
|
||||
}
|
||||
|
||||
switch (data.subpageUrl) {
|
||||
case data.additionalHotelData?.hotelParking.nameInUrl:
|
||||
subpageDescription =
|
||||
data.additionalHotelData?.hotelParking.elevatorPitch
|
||||
break
|
||||
case data.additionalHotelData?.healthAndFitness.nameInUrl:
|
||||
subpageDescription =
|
||||
data.additionalHotelData?.healthAndFitness.elevatorPitch
|
||||
break
|
||||
case data.additionalHotelData?.hotelSpecialNeeds.nameInUrl:
|
||||
subpageDescription =
|
||||
data.additionalHotelData?.hotelSpecialNeeds.elevatorPitch
|
||||
break
|
||||
case data.additionalHotelData?.meetingRooms.nameInUrl:
|
||||
subpageDescription =
|
||||
data.additionalHotelData?.meetingRooms.elevatorPitch
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (subpageDescription) {
|
||||
return truncateTextAfterLastPeriod(subpageDescription)
|
||||
}
|
||||
}
|
||||
return data.hotelData.hotelContent.texts.descriptions?.short
|
||||
}
|
||||
if (data.preamble) {
|
||||
return truncateTextAfterLastPeriod(data.preamble)
|
||||
}
|
||||
if (data.header?.preamble) {
|
||||
return truncateTextAfterLastPeriod(data.header.preamble)
|
||||
}
|
||||
if (data.blocks?.length) {
|
||||
const jsonData = data.blocks[0].content?.content?.json
|
||||
// Finding the first paragraph with text
|
||||
const firstParagraph = jsonData?.children?.find(
|
||||
(child) => child.type === RTETypeEnum.p && child.children[0].text
|
||||
)
|
||||
|
||||
if (firstParagraph?.children?.length) {
|
||||
return firstParagraph.children[0].text
|
||||
? truncateTextAfterLastPeriod(firstParagraph.children[0].text)
|
||||
: ""
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
export function getImage(data: RawMetadataSchema) {
|
||||
const metadataImage = data.web?.seo_metadata?.seo_image
|
||||
const heroImage = data.hero_image || data.header?.hero_image
|
||||
|
||||
// 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.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.imageSizes.small,
|
||||
alt:
|
||||
restaurantImage.metaData.altText ||
|
||||
restaurantImage.metaData.altText_En ||
|
||||
"",
|
||||
}
|
||||
}
|
||||
|
||||
switch (data.subpageUrl) {
|
||||
case data.additionalHotelData?.hotelParking.nameInUrl:
|
||||
const parkingImage =
|
||||
data.additionalHotelData?.parkingImages?.heroImages[0]
|
||||
if (parkingImage) {
|
||||
subpageImage = {
|
||||
url: parkingImage.imageSizes.small,
|
||||
alt:
|
||||
parkingImage.metaData.altText ||
|
||||
parkingImage.metaData.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.imageSizes.small,
|
||||
alt:
|
||||
wellnessImage.metaData.altText ||
|
||||
wellnessImage.metaData.altText_En ||
|
||||
"",
|
||||
}
|
||||
}
|
||||
break
|
||||
case data.additionalHotelData?.hotelSpecialNeeds.nameInUrl:
|
||||
const accessibilityImage =
|
||||
data.additionalHotelData?.accessibility?.heroImages[0]
|
||||
if (accessibilityImage) {
|
||||
subpageImage = {
|
||||
url: accessibilityImage.imageSizes.small,
|
||||
alt:
|
||||
accessibilityImage.metaData.altText ||
|
||||
accessibilityImage.metaData.altText_En ||
|
||||
"",
|
||||
}
|
||||
}
|
||||
break
|
||||
case data.additionalHotelData?.meetingRooms.nameInUrl:
|
||||
const meetingImage =
|
||||
data.additionalHotelData?.conferencesAndMeetings?.heroImages[0]
|
||||
if (meetingImage) {
|
||||
subpageImage = {
|
||||
url: meetingImage.imageSizes.small,
|
||||
alt:
|
||||
meetingImage.metaData.altText ||
|
||||
meetingImage.metaData.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.imageSizes.small,
|
||||
alt: hotelImage.metaData.altText || undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (heroImage) {
|
||||
return {
|
||||
url: heroImage.url,
|
||||
alt: heroImage.meta.alt || undefined,
|
||||
width: heroImage.dimensions.width,
|
||||
height: heroImage.dimensions.height,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function getCityData(
|
||||
data: RawMetadataSchema,
|
||||
input: MetadataInputSchema,
|
||||
serviceToken: string,
|
||||
lang: Lang
|
||||
) {
|
||||
const destinationSettings = data.destination_settings
|
||||
const filter = input.filterFromUrl
|
||||
|
||||
if (destinationSettings) {
|
||||
const {
|
||||
city_sweden,
|
||||
city_norway,
|
||||
city_denmark,
|
||||
city_finland,
|
||||
city_germany,
|
||||
city_poland,
|
||||
} = destinationSettings
|
||||
const cities = [
|
||||
city_denmark,
|
||||
city_finland,
|
||||
city_germany,
|
||||
city_poland,
|
||||
city_norway,
|
||||
city_sweden,
|
||||
].filter((city): city is string => Boolean(city))
|
||||
|
||||
const cityIdentifier = cities[0]
|
||||
|
||||
if (cityIdentifier) {
|
||||
const cityData = await getCityByCityIdentifier({
|
||||
cityIdentifier,
|
||||
serviceToken,
|
||||
lang,
|
||||
})
|
||||
const hotelIds = await getHotelIdsByCityIdentifier(
|
||||
cityIdentifier,
|
||||
serviceToken
|
||||
)
|
||||
|
||||
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
||||
|
||||
let filterType
|
||||
if (filter) {
|
||||
const allFilters = getFiltersFromHotels(hotels)
|
||||
const facilityFilter = allFilters.facilityFilters.find(
|
||||
(f) => f.slug === filter
|
||||
)
|
||||
const surroudingsFilter = allFilters.surroundingsFilters.find(
|
||||
(f) => f.slug === filter
|
||||
)
|
||||
|
||||
if (facilityFilter) {
|
||||
filterType = "facility"
|
||||
} else if (surroudingsFilter) {
|
||||
filterType = "surroundings"
|
||||
}
|
||||
}
|
||||
|
||||
return { location: cityData?.name, filter, filterType }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getCountryData(
|
||||
data: RawMetadataSchema,
|
||||
input: MetadataInputSchema,
|
||||
serviceToken: string,
|
||||
lang: Lang
|
||||
) {
|
||||
const country = data.destination_settings?.country
|
||||
const filter = input.filterFromUrl
|
||||
|
||||
if (country) {
|
||||
const translatedCountry = ApiCountry[lang][country]
|
||||
let filterType
|
||||
|
||||
const hotelIds = await getHotelIdsByCountry({
|
||||
country,
|
||||
serviceToken,
|
||||
})
|
||||
|
||||
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
||||
|
||||
if (filter) {
|
||||
const allFilters = getFiltersFromHotels(hotels)
|
||||
const facilityFilter = allFilters.facilityFilters.find(
|
||||
(f) => f.slug === filter
|
||||
)
|
||||
const surroudingsFilter = allFilters.surroundingsFilters.find(
|
||||
(f) => f.slug === filter
|
||||
)
|
||||
|
||||
if (facilityFilter) {
|
||||
filterType = "facility"
|
||||
} else if (surroudingsFilter) {
|
||||
filterType = "surroundings"
|
||||
}
|
||||
}
|
||||
return { location: translatedCountry, filter, filterType }
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { truncateTextAfterLastPeriod } from "../truncate"
|
||||
|
||||
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { truncateTextAfterLastPeriod } from "../truncate"
|
||||
|
||||
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { truncateTextAfterLastPeriod } from "../truncate"
|
||||
|
||||
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
|
||||
|
||||
function getSubpageDescription(
|
||||
subpageUrl: string,
|
||||
additionalHotelData: RawMetadataSchema["additionalHotelData"],
|
||||
hotelRestaurants: RawMetadataSchema["hotelRestaurants"]
|
||||
) {
|
||||
const restaurantSubPage = hotelRestaurants?.find(
|
||||
(restaurant) => restaurant.nameInUrl === subpageUrl
|
||||
)
|
||||
if (restaurantSubPage?.elevatorPitch) {
|
||||
return restaurantSubPage.elevatorPitch
|
||||
}
|
||||
|
||||
if (!additionalHotelData) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (subpageUrl) {
|
||||
case additionalHotelData.hotelParking.nameInUrl:
|
||||
return additionalHotelData.hotelParking.elevatorPitch
|
||||
case additionalHotelData.healthAndFitness.nameInUrl:
|
||||
return additionalHotelData.healthAndFitness.elevatorPitch
|
||||
case additionalHotelData.hotelSpecialNeeds.nameInUrl:
|
||||
return additionalHotelData.hotelSpecialNeeds.elevatorPitch
|
||||
case additionalHotelData.meetingRooms.nameInUrl:
|
||||
return additionalHotelData.meetingRooms.elevatorPitch
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHotelPageDescription(data: RawMetadataSchema) {
|
||||
const intl = await getIntl()
|
||||
const { subpageUrl, hotelData, additionalHotelData, hotelRestaurants } = data
|
||||
if (!hotelData) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (subpageUrl) {
|
||||
const subpageDescription = getSubpageDescription(
|
||||
subpageUrl,
|
||||
additionalHotelData,
|
||||
hotelRestaurants
|
||||
)
|
||||
|
||||
if (subpageDescription) {
|
||||
return truncateTextAfterLastPeriod(subpageDescription)
|
||||
}
|
||||
}
|
||||
|
||||
const hotelName = hotelData.name
|
||||
const location = hotelData.address.city
|
||||
const amenities = hotelData.detailedFacilities
|
||||
|
||||
if (amenities.length < 4) {
|
||||
return intl.formatMessage(
|
||||
{ defaultMessage: "{hotelName} in {location}. Book your stay now!" },
|
||||
{ hotelName, location }
|
||||
)
|
||||
}
|
||||
|
||||
const hotelDescription = intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"{hotelName} in {location} offers {amenity1} and {amenity2}. Guests can also enjoy {amenity3} and {amenity4}. Book your stay at {hotelName} today!",
|
||||
},
|
||||
{
|
||||
hotelName,
|
||||
location,
|
||||
amenity1: amenities[0].name,
|
||||
amenity2: amenities[1].name,
|
||||
amenity3: amenities[2].name,
|
||||
amenity4: amenities[3].name,
|
||||
}
|
||||
)
|
||||
const shortHotelDescription = intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"{hotelName} in {location} offers {amenity1} and {amenity2}. Guests can also enjoy {amenity3} and {amenity4}.",
|
||||
},
|
||||
{
|
||||
hotelName,
|
||||
location,
|
||||
amenity1: amenities[0].name,
|
||||
amenity2: amenities[1].name,
|
||||
amenity3: amenities[2].name,
|
||||
amenity4: amenities[3].name,
|
||||
}
|
||||
)
|
||||
|
||||
if (hotelDescription.length > 160) {
|
||||
if (shortHotelDescription.length > 160) {
|
||||
return truncateTextAfterLastPeriod(shortHotelDescription)
|
||||
}
|
||||
return shortHotelDescription
|
||||
} else {
|
||||
return hotelDescription
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { truncateTextAfterLastPeriod } from "../truncate"
|
||||
import { getDestinationCityPageDescription } from "./destinationCityPage"
|
||||
import { getDestinationCountryPageDescription } from "./destinationCountryPage"
|
||||
import { getHotelPageDescription } from "./hotelPage"
|
||||
|
||||
import { PageContentTypeEnum } from "@/types/requests/contentType"
|
||||
import { RTETypeEnum } from "@/types/rte/enums"
|
||||
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
|
||||
|
||||
export async function getDescription(data: RawMetadataSchema) {
|
||||
const metadata = data.web?.seo_metadata
|
||||
|
||||
if (metadata?.description) {
|
||||
return metadata.description
|
||||
}
|
||||
|
||||
let description: string | null = null
|
||||
switch (data.system.content_type_uid) {
|
||||
case PageContentTypeEnum.hotelPage:
|
||||
description = await getHotelPageDescription(data)
|
||||
break
|
||||
case PageContentTypeEnum.destinationCityPage:
|
||||
description = await getDestinationCityPageDescription(data)
|
||||
break
|
||||
case PageContentTypeEnum.destinationCountryPage:
|
||||
description = await getDestinationCountryPageDescription(data)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (description) {
|
||||
return description
|
||||
}
|
||||
|
||||
// Fallback descriptions from contentstack content
|
||||
if (data.preamble) {
|
||||
return truncateTextAfterLastPeriod(data.preamble)
|
||||
}
|
||||
if (data.header?.preamble) {
|
||||
return truncateTextAfterLastPeriod(data.header.preamble)
|
||||
}
|
||||
if (data.blocks?.length) {
|
||||
const jsonData = data.blocks[0].content?.content?.json
|
||||
// Finding the first paragraph with text
|
||||
const firstParagraph = jsonData?.children?.find(
|
||||
(child) => child.type === RTETypeEnum.p && child.children[0].text
|
||||
)
|
||||
|
||||
if (firstParagraph?.children?.length) {
|
||||
return firstParagraph.children[0].text
|
||||
? truncateTextAfterLastPeriod(firstParagraph.children[0].text)
|
||||
: ""
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
|
||||
|
||||
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.imageSizes.small,
|
||||
alt:
|
||||
restaurantImage.metaData.altText ||
|
||||
restaurantImage.metaData.altText_En ||
|
||||
"",
|
||||
}
|
||||
}
|
||||
|
||||
switch (data.subpageUrl) {
|
||||
case data.additionalHotelData?.hotelParking.nameInUrl:
|
||||
const parkingImage =
|
||||
data.additionalHotelData?.parkingImages?.heroImages[0]
|
||||
if (parkingImage) {
|
||||
subpageImage = {
|
||||
url: parkingImage.imageSizes.small,
|
||||
alt:
|
||||
parkingImage.metaData.altText ||
|
||||
parkingImage.metaData.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.imageSizes.small,
|
||||
alt:
|
||||
wellnessImage.metaData.altText ||
|
||||
wellnessImage.metaData.altText_En ||
|
||||
"",
|
||||
}
|
||||
}
|
||||
break
|
||||
case data.additionalHotelData?.hotelSpecialNeeds.nameInUrl:
|
||||
const accessibilityImage =
|
||||
data.additionalHotelData?.accessibility?.heroImages[0]
|
||||
if (accessibilityImage) {
|
||||
subpageImage = {
|
||||
url: accessibilityImage.imageSizes.small,
|
||||
alt:
|
||||
accessibilityImage.metaData.altText ||
|
||||
accessibilityImage.metaData.altText_En ||
|
||||
"",
|
||||
}
|
||||
}
|
||||
break
|
||||
case data.additionalHotelData?.meetingRooms.nameInUrl:
|
||||
const meetingImage =
|
||||
data.additionalHotelData?.conferencesAndMeetings?.heroImages[0]
|
||||
if (meetingImage) {
|
||||
subpageImage = {
|
||||
url: meetingImage.imageSizes.small,
|
||||
alt:
|
||||
meetingImage.metaData.altText ||
|
||||
meetingImage.metaData.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.imageSizes.small,
|
||||
alt: hotelImage.metaData.altText || undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (heroImage) {
|
||||
return {
|
||||
url: heroImage.url,
|
||||
alt: heroImage.meta.alt || undefined,
|
||||
width: heroImage.dimensions.width,
|
||||
height: heroImage.dimensions.height,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { type Lang } from "@/constants/languages"
|
||||
import {
|
||||
getFiltersFromHotels,
|
||||
getSortedCities,
|
||||
} from "@/stores/destination-data/helper"
|
||||
|
||||
import {
|
||||
getCityByCityIdentifier,
|
||||
getHotelIdsByCityIdentifier,
|
||||
getHotelIdsByCountry,
|
||||
getHotelsByHotelIds,
|
||||
} from "../../../hotels/utils"
|
||||
import { getCityPages } from "../../destinationCountryPage/utils"
|
||||
|
||||
import { ApiCountry } from "@/types/enums/country"
|
||||
import { SortOption } from "@/types/enums/destinationFilterAndSort"
|
||||
import type {
|
||||
MetadataInputSchema,
|
||||
RawMetadataSchema,
|
||||
} from "@/types/trpc/routers/contentstack/metadata"
|
||||
|
||||
export const affix = "metadata"
|
||||
|
||||
export async function getCityData(
|
||||
data: RawMetadataSchema,
|
||||
input: MetadataInputSchema,
|
||||
serviceToken: string,
|
||||
lang: Lang
|
||||
) {
|
||||
const destinationSettings = data.destination_settings
|
||||
const filter = input.filterFromUrl
|
||||
|
||||
if (destinationSettings) {
|
||||
const {
|
||||
city_sweden,
|
||||
city_norway,
|
||||
city_denmark,
|
||||
city_finland,
|
||||
city_germany,
|
||||
city_poland,
|
||||
} = destinationSettings
|
||||
const cities = [
|
||||
city_denmark,
|
||||
city_finland,
|
||||
city_germany,
|
||||
city_poland,
|
||||
city_norway,
|
||||
city_sweden,
|
||||
].filter((city): city is string => Boolean(city))
|
||||
|
||||
const cityIdentifier = cities[0]
|
||||
|
||||
if (cityIdentifier) {
|
||||
const cityData = await getCityByCityIdentifier({
|
||||
cityIdentifier,
|
||||
serviceToken,
|
||||
lang,
|
||||
})
|
||||
const hotelIds = await getHotelIdsByCityIdentifier(
|
||||
cityIdentifier,
|
||||
serviceToken
|
||||
)
|
||||
|
||||
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
||||
|
||||
let filterType
|
||||
if (filter) {
|
||||
const allFilters = getFiltersFromHotels(hotels)
|
||||
const facilityFilter = allFilters.facilityFilters.find(
|
||||
(f) => f.slug === filter
|
||||
)
|
||||
const surroudingsFilter = allFilters.surroundingsFilters.find(
|
||||
(f) => f.slug === filter
|
||||
)
|
||||
|
||||
if (facilityFilter) {
|
||||
filterType = "facility"
|
||||
} else if (surroudingsFilter) {
|
||||
filterType = "surroundings"
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
location: cityData?.name,
|
||||
filter,
|
||||
filterType,
|
||||
hotelCount: hotelIds.length,
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getCountryData(
|
||||
data: RawMetadataSchema,
|
||||
input: MetadataInputSchema,
|
||||
serviceToken: string,
|
||||
lang: Lang
|
||||
) {
|
||||
const country = data.destination_settings?.country
|
||||
const filter = input.filterFromUrl
|
||||
|
||||
if (country) {
|
||||
const translatedCountry = ApiCountry[lang][country]
|
||||
let filterType
|
||||
|
||||
const cities = await getCityPages(lang, serviceToken, country)
|
||||
const sortedCities = getSortedCities(cities, SortOption.Recommended)
|
||||
const hotelIds = await getHotelIdsByCountry({
|
||||
country,
|
||||
serviceToken,
|
||||
})
|
||||
|
||||
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
||||
|
||||
if (filter) {
|
||||
const allFilters = getFiltersFromHotels(hotels)
|
||||
const facilityFilter = allFilters.facilityFilters.find(
|
||||
(f) => f.slug === filter
|
||||
)
|
||||
const surroudingsFilter = allFilters.surroundingsFilters.find(
|
||||
(f) => f.slug === filter
|
||||
)
|
||||
|
||||
if (facilityFilter) {
|
||||
filterType = "facility"
|
||||
} else if (surroudingsFilter) {
|
||||
filterType = "surroundings"
|
||||
}
|
||||
}
|
||||
return {
|
||||
location: translatedCountry,
|
||||
filter,
|
||||
filterType,
|
||||
cities: sortedCities.slice(0, 2).map(({ cityName }) => cityName),
|
||||
hotelCount: hotelIds.length,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
|
||||
|
||||
export async function getTitle(data: RawMetadataSchema) {
|
||||
const intl = await getIntl()
|
||||
const metadata = data.web?.seo_metadata
|
||||
if (metadata?.title) {
|
||||
return metadata.title
|
||||
}
|
||||
|
||||
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) {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"Explore {restaurantName} at {hotelName} in {destination}",
|
||||
},
|
||||
{
|
||||
restaurantName: restaurantSubPage.name,
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
switch (data.subpageUrl) {
|
||||
case data.additionalHotelData?.hotelParking.nameInUrl:
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"Parking information for {hotelName} in {destination}",
|
||||
},
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
case data.additionalHotelData?.healthAndFitness.nameInUrl:
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"Gym & Health Facilities at {hotelName} in {destination}",
|
||||
},
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
case data.additionalHotelData?.hotelSpecialNeeds.nameInUrl:
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"Accessibility information for {hotelName} in {destination}",
|
||||
},
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
case data.additionalHotelData?.meetingRooms.nameInUrl:
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"Meetings, Conferences & Events at {hotelName} in {destination}",
|
||||
},
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Stay at {hotelName} | Hotel in {destination}",
|
||||
},
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
}
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Hotels in {location}",
|
||||
},
|
||||
{ location }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.web?.breadcrumbs?.title) {
|
||||
return data.web.breadcrumbs.title
|
||||
}
|
||||
if (data.heading) {
|
||||
return data.heading
|
||||
}
|
||||
if (data.header?.heading) {
|
||||
return data.header.heading
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Truncates the given text "intelligently" based on the last period found near the max length.
|
||||
*
|
||||
* - If a period exists within the extended range (`maxLength` to `maxLength + maxExtension`),
|
||||
* the function truncates after the closest period to `maxLength`.
|
||||
* - If no period is found in the range, it truncates the text after the last period found in the max length of the text.
|
||||
* - If no periods exist at all, it truncates at `maxLength` and appends ellipsis (`...`).
|
||||
*
|
||||
* @param {string} text - The input text to be truncated.
|
||||
* @param {number} [maxLength=150] - The desired maximum length of the truncated text.
|
||||
* @param {number} [minLength=120] - The minimum allowable length for the truncated text.
|
||||
* @param {number} [maxExtension=10] - The maximum number of characters to extend beyond `maxLength` to find a period.
|
||||
* @returns {string} - The truncated text.
|
||||
*/
|
||||
export function truncateTextAfterLastPeriod(
|
||||
text: string,
|
||||
maxLength: number = 160,
|
||||
minLength: number = 120,
|
||||
maxExtension: number = 10
|
||||
): string {
|
||||
if (text.length <= maxLength) {
|
||||
return text
|
||||
}
|
||||
|
||||
// Define the extended range
|
||||
const extendedEnd = Math.min(text.length, maxLength + maxExtension)
|
||||
const extendedText = text.slice(0, extendedEnd)
|
||||
|
||||
// Find all periods within the extended range and filter after minLength to get valid periods
|
||||
const periodsInRange = [...extendedText.matchAll(/\./g)].map(
|
||||
({ index }) => index
|
||||
)
|
||||
const validPeriods = periodsInRange.filter((index) => index + 1 >= minLength)
|
||||
|
||||
if (validPeriods.length > 0) {
|
||||
// Find the period closest to maxLength
|
||||
const closestPeriod = validPeriods.reduce((closest, currentIndex) => {
|
||||
const distanceFromCurrentToMaxLength = Math.abs(
|
||||
currentIndex + 1 - maxLength
|
||||
)
|
||||
const distanceFromClosestToMaxLength = Math.abs(closest + 1 - maxLength)
|
||||
|
||||
return distanceFromCurrentToMaxLength < distanceFromClosestToMaxLength
|
||||
? currentIndex
|
||||
: closest
|
||||
}, validPeriods[0])
|
||||
|
||||
return extendedText.slice(0, closestPeriod + 1)
|
||||
}
|
||||
|
||||
// Fallback: If no period is found within the valid range, look for the last period in the truncated text
|
||||
const maxLengthText = text.slice(0, maxLength)
|
||||
const lastPeriodIndex = maxLengthText.lastIndexOf(".")
|
||||
if (lastPeriodIndex !== -1) {
|
||||
return text.slice(0, lastPeriodIndex + 1)
|
||||
}
|
||||
|
||||
// Final fallback: Return maxLength text including ellipsis
|
||||
return `${maxLengthText}...`
|
||||
}
|
||||
Reference in New Issue
Block a user