feat(SW-201): Added structured data for hotel pages
This commit is contained in:
@@ -12,42 +12,43 @@ import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
|||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
import { getRestaurantHeading } from "@/utils/facilityCards"
|
import { getRestaurantHeading } from "@/utils/facilityCards"
|
||||||
|
import { generateHotelSchema } from "@/utils/jsonSchemas"
|
||||||
|
|
||||||
import DynamicMap from "./Map/DynamicMap"
|
import DynamicMap from "./Map/DynamicMap"
|
||||||
import MapCard from "./Map/MapCard"
|
import MapCard from "./Map/MapCard"
|
||||||
import MapWithCardWrapper from "./Map/MapWithCard"
|
import MapWithCardWrapper from "./Map/MapWithCard"
|
||||||
import MobileMapToggle from "./Map/MobileMapToggle"
|
import MobileMapToggle from "./Map/MobileMapToggle"
|
||||||
import StaticMap from "./Map/StaticMap"
|
import StaticMap from "./Map/StaticMap"
|
||||||
import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise"
|
|
||||||
import AmenitiesList from "./AmenitiesList"
|
import AmenitiesList from "./AmenitiesList"
|
||||||
import Facilities from "./Facilities"
|
import Facilities from "./Facilities"
|
||||||
import IntroSection from "./IntroSection"
|
import IntroSection from "./IntroSection"
|
||||||
import PreviewImages from "./PreviewImages"
|
import PreviewImages from "./PreviewImages"
|
||||||
import { Rooms } from "./Rooms"
|
import { Rooms } from "./Rooms"
|
||||||
import { AboutTheHotelSidePeek } from "./SidePeeks"
|
import { AboutTheHotelSidePeek, WellnessAndExerciseSidePeek } from "./SidePeeks"
|
||||||
import TabNavigation from "./TabNavigation"
|
import TabNavigation from "./TabNavigation"
|
||||||
|
|
||||||
import styles from "./hotelPage.module.css"
|
import styles from "./hotelPage.module.css"
|
||||||
|
|
||||||
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
|
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 { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
|
||||||
import { Facility } from "@/types/hotel"
|
import type { Facility } from "@/types/hotel"
|
||||||
|
|
||||||
export default async function HotelPage({ hotelId }: HotelPageProps) {
|
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 lang = getLang()
|
||||||
const [intl, hotelPageData, hotelData] = await Promise.all([
|
const [intl, hotelPageData, hotelData] = await Promise.all([
|
||||||
getIntl(),
|
getIntl(),
|
||||||
getHotelPage(),
|
getHotelPage(),
|
||||||
getHotelData({ hotelId, language: lang }),
|
getHotelData({ hotelId, language: lang }),
|
||||||
])
|
])
|
||||||
|
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
||||||
|
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
||||||
|
|
||||||
if (!hotelData?.data || !hotelPageData) {
|
if (!hotelData?.data || !hotelPageData) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jsonSchema = generateHotelSchema(hotelData.data.attributes)
|
||||||
const { faq, content } = hotelPageData
|
const { faq, content } = hotelPageData
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
@@ -103,6 +104,12 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.pageContainer}>
|
<div className={styles.pageContainer}>
|
||||||
|
<script
|
||||||
|
type={jsonSchema.type}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: JSON.stringify(jsonSchema.jsonLd),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div className={styles.hotelImages}>
|
<div className={styles.hotelImages}>
|
||||||
{images?.length && <PreviewImages images={images} hotelName={name} />}
|
{images?.length && <PreviewImages images={images} hotelName={name} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -351,6 +351,7 @@
|
|||||||
"Sort by": "Sorter efter",
|
"Sort by": "Sorter efter",
|
||||||
"Sports": "Sport",
|
"Sports": "Sport",
|
||||||
"Standard price": "Standardpris",
|
"Standard price": "Standardpris",
|
||||||
|
"Stay at HOTEL_NAME | Hotel in DESTINATION": "Bo på {hotelName} | Hotel i {destination}",
|
||||||
"Street": "Gade",
|
"Street": "Gade",
|
||||||
"Successfully updated profile!": "Profilen er opdateret med succes!",
|
"Successfully updated profile!": "Profilen er opdateret med succes!",
|
||||||
"Summary": "Opsummering",
|
"Summary": "Opsummering",
|
||||||
|
|||||||
@@ -351,6 +351,7 @@
|
|||||||
"Sort by": "Sortieren nach",
|
"Sort by": "Sortieren nach",
|
||||||
"Sports": "Sport",
|
"Sports": "Sport",
|
||||||
"Standard price": "Standardpreis",
|
"Standard price": "Standardpreis",
|
||||||
|
"Stay at HOTEL_NAME | Hotel in DESTINATION": "Übernachten Sie im {hotelName} | Hotel in {destination}",
|
||||||
"Street": "Straße",
|
"Street": "Straße",
|
||||||
"Successfully updated profile!": "Profil erfolgreich aktualisiert!",
|
"Successfully updated profile!": "Profil erfolgreich aktualisiert!",
|
||||||
"Summary": "Zusammenfassung",
|
"Summary": "Zusammenfassung",
|
||||||
|
|||||||
@@ -380,6 +380,7 @@
|
|||||||
"Sort by": "Sort by",
|
"Sort by": "Sort by",
|
||||||
"Sports": "Sports",
|
"Sports": "Sports",
|
||||||
"Standard price": "Standard price",
|
"Standard price": "Standard price",
|
||||||
|
"Stay at HOTEL_NAME | Hotel in DESTINATION": "Stay at {hotelName} | Hotel in {destination}",
|
||||||
"Street": "Street",
|
"Street": "Street",
|
||||||
"Successfully updated profile!": "Successfully updated profile!",
|
"Successfully updated profile!": "Successfully updated profile!",
|
||||||
"Summary": "Summary",
|
"Summary": "Summary",
|
||||||
@@ -510,6 +511,5 @@
|
|||||||
"uppercase letter": "uppercase letter",
|
"uppercase letter": "uppercase letter",
|
||||||
"{amount} out of {total}": "{amount} out of {total}",
|
"{amount} out of {total}": "{amount} out of {total}",
|
||||||
"{card} ending with {cardno}": "{card} ending with {cardno}",
|
"{card} ending with {cardno}": "{card} ending with {cardno}",
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
||||||
"Stay at HOTEL_NAME | Hotel in DESTINATION": "Stay at {hotelName} | Hotel in {destination}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,6 +352,7 @@
|
|||||||
"Sort by": "Lajitteluperuste",
|
"Sort by": "Lajitteluperuste",
|
||||||
"Sports": "Urheilu",
|
"Sports": "Urheilu",
|
||||||
"Standard price": "Normaali hinta",
|
"Standard price": "Normaali hinta",
|
||||||
|
"Stay at HOTEL_NAME | Hotel in DESTINATION": "Majoitu kohteessa {hotelName} | Hotelli kohteessa {destination}",
|
||||||
"Street": "Katu",
|
"Street": "Katu",
|
||||||
"Successfully updated profile!": "Profiilin päivitys onnistui!",
|
"Successfully updated profile!": "Profiilin päivitys onnistui!",
|
||||||
"Summary": "Yhteenveto",
|
"Summary": "Yhteenveto",
|
||||||
|
|||||||
@@ -349,6 +349,7 @@
|
|||||||
"Sort by": "Sorter etter",
|
"Sort by": "Sorter etter",
|
||||||
"Sports": "Sport",
|
"Sports": "Sport",
|
||||||
"Standard price": "Standardpris",
|
"Standard price": "Standardpris",
|
||||||
|
"Stay at HOTEL_NAME | Hotel in DESTINATION": "Bo på {hotelName} | Hotell i {destination}",
|
||||||
"Street": "Gate",
|
"Street": "Gate",
|
||||||
"Successfully updated profile!": "Vellykket oppdatert profil!",
|
"Successfully updated profile!": "Vellykket oppdatert profil!",
|
||||||
"Summary": "Sammendrag",
|
"Summary": "Sammendrag",
|
||||||
|
|||||||
@@ -349,6 +349,7 @@
|
|||||||
"Sort by": "Sortera efter",
|
"Sort by": "Sortera efter",
|
||||||
"Sports": "Sport",
|
"Sports": "Sport",
|
||||||
"Standard price": "Standardpris",
|
"Standard price": "Standardpris",
|
||||||
|
"Stay at HOTEL_NAME | Hotel in DESTINATION": "Bo på {hotelName} | Hotell i {destination}",
|
||||||
"Street": "Gata",
|
"Street": "Gata",
|
||||||
"Successfully updated profile!": "Profilen har uppdaterats framgångsrikt!",
|
"Successfully updated profile!": "Profilen har uppdaterats framgångsrikt!",
|
||||||
"Summary": "Sammanfattning",
|
"Summary": "Sammanfattning",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { hotelAttributesSchema } from "../../hotels/output"
|
||||||
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
|
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
|
||||||
import { getDescription, getImage, getTitle } from "./utils"
|
import { getDescription, getImage, getTitle } from "./utils"
|
||||||
|
|
||||||
@@ -72,13 +73,8 @@ export const rawMetadataSchema = z.object({
|
|||||||
hero_image: tempImageVaultAssetSchema.nullable(),
|
hero_image: tempImageVaultAssetSchema.nullable(),
|
||||||
blocks: metaDataBlocksSchema,
|
blocks: metaDataBlocksSchema,
|
||||||
hotel_page_id: z.string().optional().nullable(),
|
hotel_page_id: z.string().optional().nullable(),
|
||||||
hotelData: z
|
hotelData: hotelAttributesSchema
|
||||||
.object({
|
.pick({ name: true, address: true, hotelContent: true, gallery: true })
|
||||||
name: z.string(),
|
|
||||||
city: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
image: z.object({ url: z.string(), alt: z.string() }).nullable(),
|
|
||||||
})
|
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
})
|
})
|
||||||
@@ -88,7 +84,7 @@ export const metadataSchema = rawMetadataSchema.transform(async (data) => {
|
|||||||
|
|
||||||
const metadata: Metadata = {
|
const metadata: Metadata = {
|
||||||
title: await getTitle(data),
|
title: await getTitle(data),
|
||||||
description: await getDescription(data),
|
description: getDescription(data),
|
||||||
openGraph: {
|
openGraph: {
|
||||||
images: getImage(data),
|
images: getImage(data),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -153,26 +153,10 @@ export const metadataQueryRouter = router({
|
|||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const rawHotelData = hotelPageData
|
return getTransformedMetadata({
|
||||||
|
...hotelPageData,
|
||||||
if (hotelData?.data.attributes) {
|
hotelData: 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)
|
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ export async function getTitle(data: RawMetadataSchema) {
|
|||||||
if (data.hotelData) {
|
if (data.hotelData) {
|
||||||
return intl.formatMessage(
|
return intl.formatMessage(
|
||||||
{ id: "Stay at HOTEL_NAME | Hotel in DESTINATION" },
|
{ 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) {
|
if (data.web?.breadcrumbs?.title) {
|
||||||
@@ -84,14 +87,13 @@ export async function getTitle(data: RawMetadataSchema) {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDescription(data: RawMetadataSchema) {
|
export function getDescription(data: RawMetadataSchema) {
|
||||||
const intl = await getIntl()
|
|
||||||
const metadata = data.web?.seo_metadata
|
const metadata = data.web?.seo_metadata
|
||||||
if (metadata?.description) {
|
if (metadata?.description) {
|
||||||
return metadata.description
|
return metadata.description
|
||||||
}
|
}
|
||||||
if (data.hotelData) {
|
if (data.hotelData) {
|
||||||
return data.hotelData.description
|
return data.hotelData.hotelContent.texts.descriptions.short
|
||||||
}
|
}
|
||||||
if (data.preamble) {
|
if (data.preamble) {
|
||||||
return truncateTextAfterLastPeriod(data.preamble)
|
return truncateTextAfterLastPeriod(data.preamble)
|
||||||
@@ -118,6 +120,9 @@ export async function getDescription(data: RawMetadataSchema) {
|
|||||||
export function getImage(data: RawMetadataSchema) {
|
export function getImage(data: RawMetadataSchema) {
|
||||||
const metadataImage = data.web?.seo_metadata?.seo_image
|
const metadataImage = data.web?.seo_metadata?.seo_image
|
||||||
const heroImage = data.hero_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)
|
// Currently we don't have the possibility to get smaller images from ImageVault (2024-11-15)
|
||||||
if (metadataImage) {
|
if (metadataImage) {
|
||||||
@@ -128,8 +133,11 @@ export function getImage(data: RawMetadataSchema) {
|
|||||||
height: metadataImage.dimensions.height,
|
height: metadataImage.dimensions.height,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.hotelData?.image) {
|
if (hotelImage) {
|
||||||
return data.hotelData.image
|
return {
|
||||||
|
url: hotelImage.imageSizes.small,
|
||||||
|
alt: hotelImage.metaData.altText || undefined,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (heroImage) {
|
if (heroImage) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -423,6 +423,47 @@ const hotelFactsSchema = z.object({
|
|||||||
yearBuilt: z.string(),
|
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
|
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
||||||
export const getHotelDataSchema = z.object({
|
export const getHotelDataSchema = z.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
@@ -435,46 +476,7 @@ export const getHotelDataSchema = z.object({
|
|||||||
}
|
}
|
||||||
return lang
|
return lang
|
||||||
}),
|
}),
|
||||||
attributes: z.object({
|
attributes: hotelAttributesSchema,
|
||||||
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),
|
|
||||||
}),
|
|
||||||
relationships: relationshipsSchema,
|
relationships: relationshipsSchema,
|
||||||
}),
|
}),
|
||||||
// NOTE: We can pass an "include" param to the hotel API to retrieve
|
// NOTE: We can pass an "include" param to the hotel API to retrieve
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { env } from "@/env/server"
|
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"
|
import type { Breadcrumbs } from "@/types/trpc/routers/contentstack/breadcrumbs"
|
||||||
|
|
||||||
export function generateBreadcrumbsSchema(breadcrumbs: Breadcrumbs) {
|
export function generateBreadcrumbsSchema(breadcrumbs: Breadcrumbs) {
|
||||||
@@ -25,3 +31,49 @@ export function generateBreadcrumbsSchema(breadcrumbs: Breadcrumbs) {
|
|||||||
jsonLd,
|
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<HotelSchema> = {
|
||||||
|
"@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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user