Merged in SW-3490-set-metadata-for-routes (pull request #2881)

SW-3490 set metadata for routes
* feat(SW-3490): Set metadata title for hotelreservation paths

Approved-by: Anton Gunnarsson
This commit is contained in:
Joakim Jäderberg
2025-10-01 11:34:52 +00:00
parent 4f151b143e
commit df8e223d23
25 changed files with 440 additions and 37 deletions

View File

@@ -1,9 +1,50 @@
import { AlternativeHotelsMapPage as AlternativeHotelsMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsMapPage"
import { getHotel } from "@/lib/trpc/memoizedRequests/getHotel"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import type { LangParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams>): Promise<Metadata> {
const intl = await getIntl()
const { hotel } = await searchParams
const { lang } = await params
if (!hotel || Array.isArray(hotel)) {
return {}
}
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
const hotelName = hotelData?.additionalData?.name
if (!hotelName) {
return {}
}
const title = intl.formatMessage(
{
defaultMessage: "Alternatives for {value}",
},
{
value: hotelName,
}
)
return { title }
}
export default async function AlternativeHotelsMapPage(
props: PageArgs<LangParams>
) {

View File

@@ -1,9 +1,50 @@
import { AlternativeHotelsPage as AlternativeHotelsPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsPage"
import { getHotel } from "@/lib/trpc/memoizedRequests/getHotel"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import { type LangParams, type PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams>): Promise<Metadata> {
const intl = await getIntl()
const { hotel } = await searchParams
const { lang } = await params
if (!hotel || Array.isArray(hotel)) {
return {}
}
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
const hotelName = hotelData?.additionalData?.name
if (!hotelName) {
return {}
}
const title = intl.formatMessage(
{
defaultMessage: "Alternatives for {value}",
},
{
value: hotelName,
}
)
return { title }
}
export default async function AlternativeHotelsPage(
props: PageArgs<LangParams>
) {

View File

@@ -1,9 +1,26 @@
import { SelectHotelMapPage as SelectHotelMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelMapPage"
import { toCapitalCase } from "@scandic-hotels/common/utils/toCapitalCase"
import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import type { LangParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
}: PageArgs<LangParams>): Promise<Metadata> {
const { city } = await searchParams
if (!city || Array.isArray(city)) {
return {}
}
return {
title: `${toCapitalCase(city)}`,
}
}
export default async function SelectHotelMapPage(props: PageArgs<LangParams>) {
const searchParams = await props.searchParams
const lang = await getLang()

View File

@@ -1,9 +1,26 @@
import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage"
import { toCapitalCase } from "@scandic-hotels/common/utils/toCapitalCase"
import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import type { LangParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
}: PageArgs<LangParams>): Promise<Metadata> {
const { city } = await searchParams
if (!city || Array.isArray(city)) {
return {}
}
return {
title: `${toCapitalCase(city)}`,
}
}
export default async function SelectHotelPage(props: PageArgs<LangParams>) {
const searchParams = await props.searchParams
const lang = await getLang()

View File

@@ -1,9 +1,39 @@
import { SelectRatePage as SelectRatePagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectRatePage"
import { getHotel } from "@/lib/trpc/memoizedRequests/getHotel"
import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import { type LangParams, type PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams>): Promise<Metadata> {
const { hotel } = await searchParams
const { lang } = await params
if (!hotel || Array.isArray(hotel)) {
return {}
}
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
if (!hotelData?.additionalData?.name) {
return {}
}
return {
title: hotelData.additionalData.name,
}
}
export default async function SelectRatePage(props: PageArgs<LangParams>) {
const searchParams = await props.searchParams
const lang = await getLang()

View File

@@ -38,7 +38,6 @@ import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRout
import type { Metadata } from "next"
export const metadata: Metadata = {
title: "SAS by Scandic Hotels",
description: "TODO This text should be updated.",
}

View File

@@ -2,8 +2,25 @@ import "@scandic-hotels/common/polyfills"
import { configureTrpc } from "@/lib/trpc"
import { getTitlePrefix } from "@/util/metadata/getTitlePrfiex"
import type { Metadata } from "next"
configureTrpc()
export async function generateMetadata(): Promise<Metadata> {
return {
title: {
template: combineSegments([
getTitlePrefix(),
"%s",
"SAS by Scandic Hotels",
]),
default: combineSegments([getTitlePrefix(), "SAS by Scandic Hotels"]),
},
}
}
export default function RootLayout({
children,
}: {
@@ -11,3 +28,10 @@ export default function RootLayout({
}) {
return <>{children}</>
}
function combineSegments(
segments: (string | null | undefined)[],
delimiter = " | "
) {
return segments.filter(Boolean).join(delimiter).trim()
}

View File

@@ -0,0 +1,14 @@
import { cache } from "react"
import { serverClient } from ".."
import type { HotelInput } from "@scandic-hotels/trpc/types/hotel"
export const getHotel = cache(async function getMemoizedHotelData(
input: HotelInput
) {
input.isCardOnlyPayment ??= false
const caller = await serverClient()
return caller.hotel.get(input)
})

View File

@@ -0,0 +1,11 @@
import { env } from "@/env/server"
export function getTitlePrefix() {
if (env.SENTRY_ENVIRONMENT === "production") {
return ""
}
const environmentShortName =
env.SENTRY_ENVIRONMENT === "development" ? "local" : env.SENTRY_ENVIRONMENT
return `${environmentShortName}`
}

View File

@@ -1,11 +1,52 @@
import { AlternativeHotelsMapPage as AlternativeHotelsMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsMapPage"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import type { Metadata } from "next"
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams, { hotel: string }>): Promise<Metadata> {
const intl = await getIntl()
const { hotel } = await searchParams
const { lang } = await params
if (!hotel || Array.isArray(hotel)) {
return {}
}
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
const hotelName = hotelData?.additionalData?.name
if (!hotelName) {
return {}
}
const title = intl.formatMessage(
{
defaultMessage: "Alternatives for {value}",
},
{
value: hotelName,
}
)
return { title }
}
export default async function AlternativeHotelsMapPage(
props: PageArgs<LangParams, NextSearchParams>
) {

View File

@@ -1,13 +1,54 @@
import { AlternativeHotelsPage as AlternativeHotelsPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsPage"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import {
type LangParams,
type NextSearchParams,
type PageArgs,
} from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams, { hotel: string }>): Promise<Metadata> {
const intl = await getIntl()
const { hotel } = await searchParams
const { lang } = await params
if (!hotel || Array.isArray(hotel)) {
return {}
}
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
const hotelName = hotelData?.additionalData?.name
if (!hotelName) {
return {}
}
const title = intl.formatMessage(
{
defaultMessage: "Alternatives for {value}",
},
{
value: hotelName,
}
)
return { title }
}
export default async function AlternativeHotelsPage(
props: PageArgs<LangParams, NextSearchParams>
) {

View File

@@ -2,6 +2,8 @@ import { EnterDetailsPage as EnterDetailsPagePrimitive } from "@scandic-hotels/b
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export { generateMetadata } from "@/utils/metadata/generateMetadata"
export default async function DetailsPage(
props: PageArgs<LangParams, NextSearchParams>
) {

View File

@@ -8,6 +8,8 @@ import styles from "./page.module.css"
import type { LangParams, PageArgs } from "@/types/params"
export { generateMetadata } from "@/utils/metadata/generateMetadata"
export default async function HotelReservationPage(
props: PageArgs<LangParams>
) {

View File

@@ -1,11 +1,24 @@
import { SelectHotelMapPage as SelectHotelMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelMapPage"
import { toCapitalCase } from "@scandic-hotels/common/utils/toCapitalCase"
import { getLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import type { Metadata } from "next"
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
}: PageArgs<LangParams, { city: string }>): Promise<Metadata> {
const { city } = await searchParams
return {
title: `${toCapitalCase(city)}`,
}
}
export default async function SelectHotelMapPage(
props: PageArgs<LangParams, NextSearchParams>
) {

View File

@@ -1,9 +1,22 @@
import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage"
import { toCapitalCase } from "@scandic-hotels/common/utils/toCapitalCase"
import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
}: PageArgs<LangParams, { city: string }>): Promise<Metadata> {
const { city } = await searchParams
return {
title: `${toCapitalCase(city)}`,
}
}
export default async function SelectHotelPage(
props: PageArgs<LangParams, NextSearchParams>
) {

View File

@@ -1,13 +1,39 @@
import { SelectRatePage as SelectRatePagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectRatePage"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import {
type LangParams,
type NextSearchParams,
type PageArgs,
} from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams, { hotel: string }>): Promise<Metadata> {
const { hotel } = await searchParams
const { lang } = await params
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
if (!hotelData?.additionalData?.name) {
return {}
}
return {
title: hotelData.additionalData.name,
}
}
export default async function SelectRatePage(
props: PageArgs<LangParams, NextSearchParams>
) {

View File

@@ -0,0 +1,27 @@
import { getTitlePrefix } from "@/utils/metadata/title/getTitlePrefix"
import type { Metadata } from "next"
export async function generateMetadata(): Promise<Metadata> {
return {
title: {
template: combineSegments([getTitlePrefix(), "%s", "Scandic Hotels"]),
default: combineSegments([getTitlePrefix(), "Scandic Hotels"]),
},
}
}
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return <>{children}</>
}
function combineSegments(
segments: (string | null | undefined)[],
delimiter = " | "
) {
return segments.filter(Boolean).join(delimiter).trim()
}

View File

@@ -27,6 +27,7 @@ export async function generateMetadata({
const { subpage, filterFromUrl, ...otherSearchParams } = await searchParams
// If there are other (real) search params, we don't want to index the page as this will
// cause duplicate content issues.
const noIndexOnSearchParams = !!Object.keys(otherSearchParams).length
const caller = await serverClient()
const { rawMetadata, alternates, robots } =
@@ -129,7 +130,7 @@ async function getTransformedMetadata(
) {
const metadata: Metadata = {
metadataBase: env.PUBLIC_URL ? new URL(env.PUBLIC_URL) : undefined,
title: await getTitle(data),
title: { absolute: await getTitle(data) },
description: await getDescription(data),
openGraph: {
images: getImage(data),

View File

@@ -4,8 +4,7 @@ import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstac
export async function getDestinationPageTitle(
data: RawMetadataSchema,
pageType: "city" | "country",
suffix: string
pageType: "city" | "country"
) {
const intl = await getIntl()
const { destinationData } = data
@@ -33,13 +32,10 @@ export async function getDestinationPageTitle(
{ location }
)
return `${destinationTitle}${suffix}`
return destinationTitle
}
export function getDestinationFilterSeoMetaTitle(
data: RawMetadataSchema,
suffix: string
) {
export function getDestinationFilterSeoMetaTitle(data: RawMetadataSchema) {
const filter = data.destinationData?.filter
if (!filter) {
@@ -51,10 +47,10 @@ export function getDestinationFilterSeoMetaTitle(
if (foundSeoFilter) {
if (foundSeoFilter.seo_metadata?.title) {
return `${foundSeoFilter.seo_metadata.title}${suffix}`
return foundSeoFilter.seo_metadata.title
}
return `${foundSeoFilter.heading}${suffix}`
return foundSeoFilter.heading
}
return null

View File

@@ -0,0 +1,11 @@
import { env } from "@/env/server"
export function getTitlePrefix() {
if (env.SENTRY_ENVIRONMENT === "production") {
return ""
}
const environmentShortName =
env.SENTRY_ENVIRONMENT === "development" ? "local" : env.SENTRY_ENVIRONMENT
return `${environmentShortName}`
}

View File

@@ -4,6 +4,7 @@ import {
getDestinationFilterSeoMetaTitle,
getDestinationPageTitle,
} from "./destinationPage"
import { getTitlePrefix } from "./getTitlePrefix"
import { getHotelPageTitle } from "./hotelPage"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
@@ -17,13 +18,14 @@ function getTitleSuffix(contentType: string) {
case PageContentTypeEnum.destinationOverviewPage:
case PageContentTypeEnum.destinationCityPage:
case PageContentTypeEnum.destinationCountryPage:
return " | Scandic Hotels"
return "Scandic Hotels"
default:
return ""
}
}
export async function getTitle(data: RawMetadataSchema) {
const prefix = getTitlePrefix()
const suffix = getTitleSuffix(data.system.content_type_uid)
const metadata = data.web?.seo_metadata
const isDestinationPage = [
@@ -32,48 +34,41 @@ export async function getTitle(data: RawMetadataSchema) {
].includes(data.system.content_type_uid as PageContentTypeEnum)
if (isDestinationPage) {
const destinationFilterSeoMetaTitle = getDestinationFilterSeoMetaTitle(
data,
suffix
)
const destinationFilterSeoMetaTitle = getDestinationFilterSeoMetaTitle(data)
if (destinationFilterSeoMetaTitle) {
return destinationFilterSeoMetaTitle
}
}
if (metadata?.title) {
return `${metadata.title}${suffix}`
return combineSegments([prefix, metadata.title, suffix])
}
let title: string | null = null
let title: string | null | undefined = null
switch (data.system.content_type_uid) {
case PageContentTypeEnum.hotelPage:
title = await getHotelPageTitle(data)
break
case PageContentTypeEnum.destinationCityPage:
title = await getDestinationPageTitle(data, "city", suffix)
title = await getDestinationPageTitle(data, "city")
break
case PageContentTypeEnum.destinationCountryPage:
title = await getDestinationPageTitle(data, "country", suffix)
title = await getDestinationPageTitle(data, "country")
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 ""
title ||= data.web?.breadcrumbs?.title || data.heading || data.header?.heading
return combineSegments([prefix, title, suffix])
}
function combineSegments(
segments: (string | null | undefined)[],
delimiter = " | "
) {
return segments.filter(Boolean).join(delimiter).trim()
}