diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx
index f5acca331..09143ce01 100644
--- a/components/ContentType/HotelPage/index.tsx
+++ b/components/ContentType/HotelPage/index.tsx
@@ -12,42 +12,43 @@ import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import { getRestaurantHeading } from "@/utils/facilityCards"
+import { generateHotelSchema } from "@/utils/jsonSchemas"
import DynamicMap from "./Map/DynamicMap"
import MapCard from "./Map/MapCard"
import MapWithCardWrapper from "./Map/MapWithCard"
import MobileMapToggle from "./Map/MobileMapToggle"
import StaticMap from "./Map/StaticMap"
-import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise"
import AmenitiesList from "./AmenitiesList"
import Facilities from "./Facilities"
import IntroSection from "./IntroSection"
import PreviewImages from "./PreviewImages"
import { Rooms } from "./Rooms"
-import { AboutTheHotelSidePeek } from "./SidePeeks"
+import { AboutTheHotelSidePeek, WellnessAndExerciseSidePeek } from "./SidePeeks"
import TabNavigation from "./TabNavigation"
import styles from "./hotelPage.module.css"
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
-import { HotelPageProps } from "@/types/components/hotelPage/hotelPage"
+import type { HotelPageProps } from "@/types/components/hotelPage/hotelPage"
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
-import { Facility } from "@/types/hotel"
+import type { Facility } from "@/types/hotel"
export default async function HotelPage({ hotelId }: HotelPageProps) {
- const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
- const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const lang = getLang()
const [intl, hotelPageData, hotelData] = await Promise.all([
getIntl(),
getHotelPage(),
getHotelData({ hotelId, language: lang }),
])
+ const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
+ const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
if (!hotelData?.data || !hotelPageData) {
return notFound()
}
+ const jsonSchema = generateHotelSchema(hotelData.data.attributes)
const { faq, content } = hotelPageData
const {
name,
@@ -103,6 +104,12 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
return (
+
diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json
index 3acc7ad46..515e8e806 100644
--- a/i18n/dictionaries/da.json
+++ b/i18n/dictionaries/da.json
@@ -351,6 +351,7 @@
"Sort by": "Sorter efter",
"Sports": "Sport",
"Standard price": "Standardpris",
+ "Stay at HOTEL_NAME | Hotel in DESTINATION": "Bo på {hotelName} | Hotel i {destination}",
"Street": "Gade",
"Successfully updated profile!": "Profilen er opdateret med succes!",
"Summary": "Opsummering",
diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json
index 8c4a26459..ccf322563 100644
--- a/i18n/dictionaries/de.json
+++ b/i18n/dictionaries/de.json
@@ -351,6 +351,7 @@
"Sort by": "Sortieren nach",
"Sports": "Sport",
"Standard price": "Standardpreis",
+ "Stay at HOTEL_NAME | Hotel in DESTINATION": "Übernachten Sie im {hotelName} | Hotel in {destination}",
"Street": "Straße",
"Successfully updated profile!": "Profil erfolgreich aktualisiert!",
"Summary": "Zusammenfassung",
diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json
index 9f14b7f25..0c11069c3 100644
--- a/i18n/dictionaries/en.json
+++ b/i18n/dictionaries/en.json
@@ -380,6 +380,7 @@
"Sort by": "Sort by",
"Sports": "Sports",
"Standard price": "Standard price",
+ "Stay at HOTEL_NAME | Hotel in DESTINATION": "Stay at {hotelName} | Hotel in {destination}",
"Street": "Street",
"Successfully updated profile!": "Successfully updated profile!",
"Summary": "Summary",
@@ -510,6 +511,5 @@
"uppercase letter": "uppercase letter",
"{amount} out of {total}": "{amount} out of {total}",
"{card} ending with {cardno}": "{card} ending with {cardno}",
- "{difference}{amount} {currency}": "{difference}{amount} {currency}",
- "Stay at HOTEL_NAME | Hotel in DESTINATION": "Stay at {hotelName} | Hotel in {destination}"
+ "{difference}{amount} {currency}": "{difference}{amount} {currency}"
}
diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json
index 4f6965532..1623032eb 100644
--- a/i18n/dictionaries/fi.json
+++ b/i18n/dictionaries/fi.json
@@ -352,6 +352,7 @@
"Sort by": "Lajitteluperuste",
"Sports": "Urheilu",
"Standard price": "Normaali hinta",
+ "Stay at HOTEL_NAME | Hotel in DESTINATION": "Majoitu kohteessa {hotelName} | Hotelli kohteessa {destination}",
"Street": "Katu",
"Successfully updated profile!": "Profiilin päivitys onnistui!",
"Summary": "Yhteenveto",
diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json
index 5494b85ef..274e9e79f 100644
--- a/i18n/dictionaries/no.json
+++ b/i18n/dictionaries/no.json
@@ -349,6 +349,7 @@
"Sort by": "Sorter etter",
"Sports": "Sport",
"Standard price": "Standardpris",
+ "Stay at HOTEL_NAME | Hotel in DESTINATION": "Bo på {hotelName} | Hotell i {destination}",
"Street": "Gate",
"Successfully updated profile!": "Vellykket oppdatert profil!",
"Summary": "Sammendrag",
diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json
index e651b14d6..09ed7c72d 100644
--- a/i18n/dictionaries/sv.json
+++ b/i18n/dictionaries/sv.json
@@ -349,6 +349,7 @@
"Sort by": "Sortera efter",
"Sports": "Sport",
"Standard price": "Standardpris",
+ "Stay at HOTEL_NAME | Hotel in DESTINATION": "Bo på {hotelName} | Hotell i {destination}",
"Street": "Gata",
"Successfully updated profile!": "Profilen har uppdaterats framgångsrikt!",
"Summary": "Sammanfattning",
diff --git a/server/routers/contentstack/metadata/output.ts b/server/routers/contentstack/metadata/output.ts
index d9850dc97..dc94a4a56 100644
--- a/server/routers/contentstack/metadata/output.ts
+++ b/server/routers/contentstack/metadata/output.ts
@@ -1,5 +1,6 @@
import { z } from "zod"
+import { hotelAttributesSchema } from "../../hotels/output"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { getDescription, getImage, getTitle } from "./utils"
@@ -72,13 +73,8 @@ export const rawMetadataSchema = z.object({
hero_image: tempImageVaultAssetSchema.nullable(),
blocks: metaDataBlocksSchema,
hotel_page_id: z.string().optional().nullable(),
- hotelData: z
- .object({
- name: z.string(),
- city: z.string(),
- description: z.string(),
- image: z.object({ url: z.string(), alt: z.string() }).nullable(),
- })
+ hotelData: hotelAttributesSchema
+ .pick({ name: true, address: true, hotelContent: true, gallery: true })
.optional()
.nullable(),
})
@@ -88,7 +84,7 @@ export const metadataSchema = rawMetadataSchema.transform(async (data) => {
const metadata: Metadata = {
title: await getTitle(data),
- description: await getDescription(data),
+ description: getDescription(data),
openGraph: {
images: getImage(data),
},
diff --git a/server/routers/contentstack/metadata/query.ts b/server/routers/contentstack/metadata/query.ts
index 4e5d78728..df1f6ee89 100644
--- a/server/routers/contentstack/metadata/query.ts
+++ b/server/routers/contentstack/metadata/query.ts
@@ -153,26 +153,10 @@ export const metadataQueryRouter = router({
)
: null
- const rawHotelData = hotelPageData
-
- if (hotelData?.data.attributes) {
- const attributes = hotelData.data.attributes
- const images = attributes.gallery?.smallerImages
-
- rawHotelData.hotelData = {
- name: attributes.name,
- city: attributes.cityName,
- description: attributes.hotelContent.texts.descriptions.short,
- image: images?.length
- ? {
- url: images[0].imageSizes.small,
- alt: images[0].metaData.altText,
- }
- : null,
- }
- }
-
- return getTransformedMetadata(rawHotelData)
+ return getTransformedMetadata({
+ ...hotelPageData,
+ hotelData: hotelData?.data.attributes,
+ })
default:
return null
}
diff --git a/server/routers/contentstack/metadata/utils.ts b/server/routers/contentstack/metadata/utils.ts
index 393479ec0..1cf9ce80c 100644
--- a/server/routers/contentstack/metadata/utils.ts
+++ b/server/routers/contentstack/metadata/utils.ts
@@ -69,7 +69,10 @@ export async function getTitle(data: RawMetadataSchema) {
if (data.hotelData) {
return intl.formatMessage(
{ id: "Stay at HOTEL_NAME | Hotel in DESTINATION" },
- { hotelName: data.hotelData.name, destination: data.hotelData.city }
+ {
+ hotelName: data.hotelData.name,
+ destination: data.hotelData.address.city,
+ }
)
}
if (data.web?.breadcrumbs?.title) {
@@ -84,14 +87,13 @@ export async function getTitle(data: RawMetadataSchema) {
return ""
}
-export async function getDescription(data: RawMetadataSchema) {
- const intl = await getIntl()
+export function getDescription(data: RawMetadataSchema) {
const metadata = data.web?.seo_metadata
if (metadata?.description) {
return metadata.description
}
if (data.hotelData) {
- return data.hotelData.description
+ return data.hotelData.hotelContent.texts.descriptions.short
}
if (data.preamble) {
return truncateTextAfterLastPeriod(data.preamble)
@@ -118,6 +120,9 @@ export async function getDescription(data: RawMetadataSchema) {
export function getImage(data: RawMetadataSchema) {
const metadataImage = data.web?.seo_metadata?.seo_image
const heroImage = data.hero_image
+ const hotelImage =
+ data.hotelData?.gallery?.heroImages?.[0] ||
+ data.hotelData?.gallery?.smallerImages?.[0]
// Currently we don't have the possibility to get smaller images from ImageVault (2024-11-15)
if (metadataImage) {
@@ -128,8 +133,11 @@ export function getImage(data: RawMetadataSchema) {
height: metadataImage.dimensions.height,
}
}
- if (data.hotelData?.image) {
- return data.hotelData.image
+ if (hotelImage) {
+ return {
+ url: hotelImage.imageSizes.small,
+ alt: hotelImage.metaData.altText || undefined,
+ }
}
if (heroImage) {
return {
diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts
index e6f5f43dc..9a575df46 100644
--- a/server/routers/hotels/output.ts
+++ b/server/routers/hotels/output.ts
@@ -423,6 +423,47 @@ const hotelFactsSchema = z.object({
yearBuilt: z.string(),
})
+export const hotelAttributesSchema = z.object({
+ accessibilityElevatorPitchText: z.string().optional(),
+ address: addressSchema,
+ cityId: z.string(),
+ cityName: z.string(),
+ conferencesAndMeetings: facilitySchema.optional(),
+ contactInformation: contactInformationSchema,
+ detailedFacilities: z
+ .array(detailedFacilitySchema)
+ .transform((facilities) =>
+ facilities.sort((a, b) => b.sortOrder - a.sortOrder)
+ ),
+ gallery: gallerySchema.optional(),
+ galleryImages: z.array(imageSchema).optional(),
+ healthAndWellness: facilitySchema.optional(),
+ healthFacilities: z.array(healthFacilitySchema),
+ hotelContent: hotelContentSchema,
+ hotelFacts: hotelFactsSchema,
+ hotelRoomElevatorPitchText: z.string().optional(),
+ hotelType: z.string().optional(),
+ isActive: z.boolean(),
+ isPublished: z.boolean(),
+ keywords: z.array(z.string()),
+ location: locationSchema,
+ merchantInformationData: merchantInformationSchema,
+ name: z.string(),
+ operaId: z.string(),
+ parking: z.array(parkingSchema),
+ pointsOfInterest: z
+ .array(pointOfInterestSchema)
+ .transform((pois) =>
+ pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
+ ),
+ ratings: ratingsSchema,
+ rewardNight: rewardNightSchema,
+ restaurantImages: facilitySchema.optional(),
+ socialMedia: socialMediaSchema,
+ specialAlerts: specialAlertsSchema,
+ specialNeedGroups: z.array(specialNeedGroupSchema),
+})
+
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
export const getHotelDataSchema = z.object({
data: z.object({
@@ -435,46 +476,7 @@ export const getHotelDataSchema = z.object({
}
return lang
}),
- attributes: z.object({
- accessibilityElevatorPitchText: z.string().optional(),
- address: addressSchema,
- cityId: z.string(),
- cityName: z.string(),
- conferencesAndMeetings: facilitySchema.optional(),
- contactInformation: contactInformationSchema,
- detailedFacilities: z
- .array(detailedFacilitySchema)
- .transform((facilities) =>
- facilities.sort((a, b) => b.sortOrder - a.sortOrder)
- ),
- gallery: gallerySchema.optional(),
- galleryImages: z.array(imageSchema).optional(),
- healthAndWellness: facilitySchema.optional(),
- healthFacilities: z.array(healthFacilitySchema),
- hotelContent: hotelContentSchema,
- hotelFacts: hotelFactsSchema,
- hotelRoomElevatorPitchText: z.string().optional(),
- hotelType: z.string().optional(),
- isActive: z.boolean(),
- isPublished: z.boolean(),
- keywords: z.array(z.string()),
- location: locationSchema,
- merchantInformationData: merchantInformationSchema,
- name: z.string(),
- operaId: z.string(),
- parking: z.array(parkingSchema),
- pointsOfInterest: z
- .array(pointOfInterestSchema)
- .transform((pois) =>
- pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
- ),
- ratings: ratingsSchema,
- rewardNight: rewardNightSchema,
- restaurantImages: facilitySchema.optional(),
- socialMedia: socialMediaSchema,
- specialAlerts: specialAlertsSchema,
- specialNeedGroups: z.array(specialNeedGroupSchema),
- }),
+ attributes: hotelAttributesSchema,
relationships: relationshipsSchema,
}),
// NOTE: We can pass an "include" param to the hotel API to retrieve
diff --git a/utils/jsonSchemas.ts b/utils/jsonSchemas.ts
index 4a1308d0d..7373ddebf 100644
--- a/utils/jsonSchemas.ts
+++ b/utils/jsonSchemas.ts
@@ -1,7 +1,13 @@
import { env } from "@/env/server"
-import type { BreadcrumbList, ListItem, WithContext } from "schema-dts"
+import type {
+ BreadcrumbList,
+ Hotel as HotelSchema,
+ ListItem,
+ WithContext,
+} from "schema-dts"
+import type { Hotel } from "@/types/hotel"
import type { Breadcrumbs } from "@/types/trpc/routers/contentstack/breadcrumbs"
export function generateBreadcrumbsSchema(breadcrumbs: Breadcrumbs) {
@@ -25,3 +31,49 @@ export function generateBreadcrumbsSchema(breadcrumbs: Breadcrumbs) {
jsonLd,
}
}
+
+export function generateHotelSchema(hotel: Hotel) {
+ const ratings = hotel.ratings?.tripAdvisor
+ const checkinData = hotel.hotelFacts.checkin
+ const image = hotel.gallery?.heroImages[0] || hotel.gallery?.smallerImages[0]
+ const facilities = hotel.detailedFacilities
+ const jsonLd: WithContext
= {
+ "@context": "https://schema.org",
+ "@type": "Hotel",
+ name: hotel.name,
+ address: {
+ "@type": "PostalAddress",
+ streetAddress: hotel.address.streetAddress,
+ addressLocality: hotel.address.city,
+ postalCode: hotel.address.zipCode,
+ addressCountry: hotel.address.country,
+ },
+ checkinTime: checkinData.checkInTime,
+ checkoutTime: checkinData.checkOutTime,
+ amenityFeature: facilities.map((facility) => ({
+ "@type": "LocationFeatureSpecification",
+ name: facility.name,
+ })),
+ }
+
+ if (image) {
+ jsonLd.image = {
+ "@type": "ImageObject",
+ url: image.imageSizes.small,
+ caption: image.metaData.title,
+ }
+ }
+
+ if (ratings && ratings.rating && ratings.numberOfReviews) {
+ jsonLd.aggregateRating = {
+ "@type": "AggregateRating",
+ ratingValue: ratings.rating,
+ reviewCount: ratings.numberOfReviews,
+ }
+ }
+
+ return {
+ type: "application/ld+json",
+ jsonLd,
+ }
+}