Merged in feat/SW-1381-new-startpage-page (pull request #1197)

feat(SW-1381): add initial start page

* feat(SW-1381): add initial start page

* fix: remove unused startpage template

remove translation key for middleware error page

* fix(SW-1381): add tracking SDK and feature flag to hide start page


Approved-by: Erik Tiekstra
Approved-by: Matilda Landström
Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
Christian Andolf
2025-01-23 10:18:21 +00:00
parent cf8a8a2810
commit 127bb6a0a7
20 changed files with 356 additions and 11 deletions

View File

@@ -10,6 +10,7 @@ import DestinationCountryPage from "@/components/ContentType/DestinationCountryP
import DestinationOverviewPage from "@/components/ContentType/DestinationOverviewPage" import DestinationOverviewPage from "@/components/ContentType/DestinationOverviewPage"
import HotelPage from "@/components/ContentType/HotelPage" import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage" import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
import StartPage from "@/components/ContentType/StartPage"
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage" import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
import ContentPage from "@/components/ContentType/StaticPages/ContentPage" import ContentPage from "@/components/ContentType/StaticPages/ContentPage"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
@@ -74,6 +75,11 @@ export default async function ContentTypePage({
) : ( ) : (
notFound() notFound()
) )
case PageContentTypeEnum.startPage:
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <StartPage />
default: default:
const type: never = params.contentType const type: never = params.contentType
console.error(`Unsupported content type given: ${type}`) console.error(`Unsupported content type given: ${type}`)

View File

@@ -1,4 +1,3 @@
import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css" import styles from "./page.module.css"
@@ -10,17 +9,9 @@ export default async function MiddlewareError({
}: LayoutArgs<LangParams & StatusParams>) { }: LayoutArgs<LangParams & StatusParams>) {
setLang(params.lang) setLang(params.lang)
const intl = await getIntl()
return ( return (
<div className={styles.layout}> <div className={styles.layout}>
{intl.formatMessage( Middleware error {params.lang}: {params.status}
{ id: "Middleware error {lang}: {status}" },
{
lang: params.lang,
status: params.status,
}
)}
</div> </div>
) )
} }

View File

@@ -0,0 +1,21 @@
import { Suspense } from "react"
import { getStartPage } from "@/lib/trpc/memoizedRequests"
import TrackingSDK from "@/components/TrackingSDK"
export default async function StartPage() {
const content = await getStartPage()
if (!content) {
return null
}
return (
<p>
<code>{JSON.stringify(content, null, 2)}</code>
<Suspense fallback={null}>
<TrackingSDK pageData={content.tracking} />
</Suspense>
</p>
)
}

View File

@@ -15,6 +15,7 @@ export const breadcrumbsVariants = cva(styles.breadcrumbs, {
[PageContentTypeEnum.destinationCityPage]: styles.contentWidth, [PageContentTypeEnum.destinationCityPage]: styles.contentWidth,
[PageContentTypeEnum.hotelPage]: styles.hotelHeaderWidth, [PageContentTypeEnum.hotelPage]: styles.hotelHeaderWidth,
[PageContentTypeEnum.loyaltyPage]: styles.fullWidth, [PageContentTypeEnum.loyaltyPage]: styles.fullWidth,
[PageContentTypeEnum.startPage]: styles.contentWidth,
default: styles.fullWidth, default: styles.fullWidth,
}, },
}, },

View File

@@ -76,4 +76,12 @@ query EntryByUrlBatch2($locale: String!, $url: String!) {
} }
total total
} }
all_start_page(where: { url: $url }, locale: $locale) {
items {
system {
...System
}
}
total
}
} }

View File

@@ -0,0 +1,47 @@
#import "../../Fragments/System.graphql"
query GetStartPage($locale: String!, $uid: String!) {
start_page(uid: $uid, locale: $locale) {
title
url
system {
...System
created_at
updated_at
}
}
trackingProps: start_page(locale: "en", uid: $uid) {
url
}
}
query GetStartPageRefs($locale: String!, $uid: String!) {
start_page(locale: $locale, uid: $uid) {
system {
...System
}
}
}
query GetDaDeEnUrlsStartPage($uid: String!) {
de: start_page(locale: "de", uid: $uid) {
url
}
en: start_page(locale: "en", uid: $uid) {
url
}
da: start_page(locale: "da", uid: $uid) {
url
}
}
query GetFiNoSvUrlsStartPage($uid: String!) {
fi: start_page(locale: "fi", uid: $uid) {
url
}
no: start_page(locale: "no", uid: $uid) {
url
}
sv: start_page(locale: "sv", uid: $uid) {
url
}
}

View File

@@ -186,3 +186,7 @@ export const getDestinationCityPage = cache(
return serverClient().contentstack.destinationCityPage.get() return serverClient().contentstack.destinationCityPage.get()
} }
) )
export const getStartPage = cache(async function getMemoizedStartPage() {
return serverClient().contentstack.startPage.get()
})

View File

@@ -18,6 +18,7 @@ export const validateBookingWidgetToggleSchema = z.object({
destination_city_page: bookingWidgetToggleSchema, destination_city_page: bookingWidgetToggleSchema,
hotel_page: bookingWidgetToggleSchema, hotel_page: bookingWidgetToggleSchema,
loyalty_page: bookingWidgetToggleSchema, loyalty_page: bookingWidgetToggleSchema,
start_page: bookingWidgetToggleSchema,
}) })
export type ValidateBookingWidgetToggleType = z.infer< export type ValidateBookingWidgetToggleType = z.infer<

View File

@@ -16,6 +16,7 @@ import { loyaltyPageRouter } from "./loyaltyPage"
import { metadataRouter } from "./metadata" import { metadataRouter } from "./metadata"
import { myPagesRouter } from "./myPages" import { myPagesRouter } from "./myPages"
import { rewardRouter } from "./reward" import { rewardRouter } from "./reward"
import { startPageRouter } from "./startPage"
export const contentstackRouter = router({ export const contentstackRouter = router({
accountPage: accountPageRouter, accountPage: accountPageRouter,
@@ -34,4 +35,5 @@ export const contentstackRouter = router({
metadata: metadataRouter, metadata: metadataRouter,
rewards: rewardRouter, rewards: rewardRouter,
loyaltyLevels: loyaltyLevelRouter, loyaltyLevels: loyaltyLevelRouter,
startPage: startPageRouter,
}) })

View File

@@ -39,6 +39,10 @@ import {
GetDaDeEnUrlsLoyaltyPage, GetDaDeEnUrlsLoyaltyPage,
GetFiNoSvUrlsLoyaltyPage, GetFiNoSvUrlsLoyaltyPage,
} from "@/lib/graphql/Query/LoyaltyPage/LoyaltyPage.graphql" } from "@/lib/graphql/Query/LoyaltyPage/LoyaltyPage.graphql"
import {
GetDaDeEnUrlsStartPage,
GetFiNoSvUrlsStartPage,
} from "@/lib/graphql/Query/StartPage/StartPage.graphql"
import { internalServerError } from "@/server/errors/trpc" import { internalServerError } from "@/server/errors/trpc"
import { publicProcedure, router } from "@/server/trpc" import { publicProcedure, router } from "@/server/trpc"
@@ -120,6 +124,10 @@ async function getLanguageSwitcher(options: LanguageSwitcherVariables) {
daDeEnDocument = GetDaDeEnUrlsDestinationCityPage daDeEnDocument = GetDaDeEnUrlsDestinationCityPage
fiNoSvDocument = GetFiNoSvUrlsDestinationCityPage fiNoSvDocument = GetFiNoSvUrlsDestinationCityPage
break break
case PageContentTypeEnum.startPage:
daDeEnDocument = GetDaDeEnUrlsStartPage
fiNoSvDocument = GetFiNoSvUrlsStartPage
break
default: default:
console.error(`type: [${options.contentType}]`) console.error(`type: [${options.contentType}]`)
console.error(`Trying to get a content type that is not supported`) console.error(`Trying to get a content type that is not supported`)

View File

@@ -0,0 +1,5 @@
import { mergeRouters } from "@/server/trpc"
import { startPageQueryRouter } from "./query"
export const startPageRouter = mergeRouters(startPageQueryRouter)

View File

@@ -0,0 +1,25 @@
import { z } from "zod"
import { systemSchema } from "../schemas/system"
export const startPageSchema = z.object({
start_page: z.object({
title: z.string(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
/** REFS */
export const startPageRefsSchema = z.object({
start_page: z.object({
system: systemSchema,
}),
})

View File

@@ -0,0 +1,182 @@
import {
GetStartPage,
GetStartPageRefs,
} from "@/lib/graphql/Query/StartPage/StartPage.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag"
import { startPageRefsSchema, startPageSchema } from "./output"
import {
getStartPageCounter,
getStartPageFailCounter,
getStartPageRefsCounter,
getStartPageRefsFailCounter,
getStartPageRefsSuccessCounter,
getStartPageSuccessCounter,
} from "./telemetry"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type {
GetStartPageData,
GetStartPageRefsSchema,
} from "@/types/trpc/routers/contentstack/startPage"
export const startPageQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
const { lang, uid } = ctx
getStartPageRefsCounter.add(1, { lang, uid: `${uid}` })
console.info(
"contentstack.startPage.refs start",
JSON.stringify({
query: { lang, uid },
})
)
const refsResponse = await request<GetStartPageRefsSchema>(
GetStartPageRefs,
{
locale: lang,
uid,
},
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
},
}
)
if (!refsResponse.data) {
const notFoundError = notFound(refsResponse)
getStartPageRefsFailCounter.add(1, {
lang,
uid,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.startPage.refs not found error",
JSON.stringify({
query: { lang, uid },
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const validatedRefsData = startPageRefsSchema.safeParse(refsResponse.data)
if (!validatedRefsData.success) {
getStartPageRefsFailCounter.add(1, {
lang,
uid,
error_type: "validation_error",
error: JSON.stringify(validatedRefsData.error),
})
console.error(
"contentstack.startPage.refs validation error",
JSON.stringify({
query: { lang, uid },
error: validatedRefsData.error,
})
)
return null
}
getStartPageRefsSuccessCounter.add(1, { lang, uid: `${uid}` })
console.info(
"contentstack.startPage.refs success",
JSON.stringify({
query: { lang, uid },
})
)
getStartPageCounter.add(1, { lang, uid: `${uid}` })
console.info(
"contentstack.startPage start",
JSON.stringify({
query: { lang, uid },
})
)
const response = await request<GetStartPageData>(
GetStartPage,
{
locale: lang,
uid,
},
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
},
}
)
if (!response.data) {
const notFoundError = notFound(response)
getStartPageFailCounter.add(1, {
lang,
uid: `${uid}`,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.startPage not found error",
JSON.stringify({
query: { lang, uid },
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const startPage = startPageSchema.safeParse(response.data)
if (!startPage.success) {
getStartPageFailCounter.add(1, {
lang,
uid: `${uid}`,
error_type: "validation_error",
error: JSON.stringify(startPage.error),
})
console.error(
"contentstack.startPage validation error",
JSON.stringify({
query: { lang, uid },
error: startPage.error,
})
)
return null
}
getStartPageSuccessCounter.add(1, { lang, uid: `${uid}` })
console.info(
"contentstack.startPage success",
JSON.stringify({
query: { lang, uid },
})
)
const system = startPage.data.start_page.system
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: lang,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum["start-page"],
pageType: "start-page",
pageName: startPage.data.trackingProps.url,
siteSections: startPage.data.trackingProps.url,
siteVersion: "new-web",
}
return {
startPage: startPage.data.start_page,
tracking,
}
}),
})

View File

@@ -0,0 +1,23 @@
import { metrics } from "@opentelemetry/api"
const meter = metrics.getMeter("trpc.contentstack.startPage")
export const getStartPageRefsCounter = meter.createCounter(
"trpc.contentstack.startPage.get"
)
export const getStartPageRefsFailCounter = meter.createCounter(
"trpc.contentstack.startPage.get-fail"
)
export const getStartPageRefsSuccessCounter = meter.createCounter(
"trpc.contentstack.startPage.get-success"
)
export const getStartPageCounter = meter.createCounter(
"trpc.contentstack.startPage.get"
)
export const getStartPageSuccessCounter = meter.createCounter(
"trpc.contentstack.startPage.get-success"
)
export const getStartPageFailCounter = meter.createCounter(
"trpc.contentstack.startPage.get-fail"
)

View File

@@ -9,6 +9,7 @@ export enum TrackingChannelEnum {
"destination-overview-page" = "destination-overview-page", "destination-overview-page" = "destination-overview-page",
"destination-page" = "destination-page", "destination-page" = "destination-page",
"hotels" = "hotels", "hotels" = "hotels",
"start-page" = "start-page",
} }
export type TrackingChannel = keyof typeof TrackingChannelEnum export type TrackingChannel = keyof typeof TrackingChannelEnum

View File

@@ -26,6 +26,7 @@ export type ContentTypeParams = {
| PageContentTypeEnum.destinationOverviewPage | PageContentTypeEnum.destinationOverviewPage
| PageContentTypeEnum.destinationCountryPage | PageContentTypeEnum.destinationCountryPage
| PageContentTypeEnum.destinationCityPage | PageContentTypeEnum.destinationCityPage
| PageContentTypeEnum.startPage
} }
export type ContentTypeWebviewParams = { export type ContentTypeWebviewParams = {

View File

@@ -8,4 +8,5 @@ export enum PageContentTypeEnum {
destinationCityPage = "destination_city_page", destinationCityPage = "destination_city_page",
hotelPage = "hotel_page", hotelPage = "hotel_page",
loyaltyPage = "loyalty_page", loyaltyPage = "loyalty_page",
startPage = "start_page",
} }

View File

@@ -22,4 +22,5 @@ export const validateEntryResolveSchema = z.object({
all_destination_overview_page: entryResolveSchema, all_destination_overview_page: entryResolveSchema,
all_destination_country_page: entryResolveSchema, all_destination_country_page: entryResolveSchema,
all_destination_city_page: entryResolveSchema, all_destination_city_page: entryResolveSchema,
all_start_page: entryResolveSchema,
}) })

View File

@@ -0,0 +1,14 @@
import type { z } from "zod"
import type {
startPageRefsSchema,
startPageSchema,
} from "@/server/routers/contentstack/startPage/output"
export interface GetStartPageData extends z.input<typeof startPageSchema> {}
export interface StartPage extends z.output<typeof startPageSchema> {}
export interface GetStartPageRefsSchema
extends z.input<typeof startPageRefsSchema> {}
export interface StartPageRefs extends z.output<typeof startPageRefsSchema> {}

View File

@@ -9,7 +9,10 @@ import { internalServerError } from "@/server/errors/next"
import { validateEntryResolveSchema } from "@/types/requests/entry" import { validateEntryResolveSchema } from "@/types/requests/entry"
export async function resolve(url: string, lang = Lang.en) { export async function resolve(url: string, lang = Lang.en) {
const variables = { locale: lang, url } const variables = { locale: lang, url: url || "/" }
// The maximum amount of content types you can query is 6, therefor more
// than that is being batched
const response = await batchEdgeRequest([ const response = await batchEdgeRequest([
{ {
document: EntryByUrlBatch1, document: EntryByUrlBatch1,