diff --git a/.env.test b/.env.test
index 8d70bab9d..801c4336b 100644
--- a/.env.test
+++ b/.env.test
@@ -12,6 +12,7 @@ CURITY_CLIENT_SECRET_SERVICE="test"
CURITY_CLIENT_ID_USER="test"
CURITY_CLIENT_SECRET_USER="test"
CURITY_ISSUER_USER="test"
+CURITY_ISSUER_SERVICE="test"
CYPRESS_API_BASEURL="test"
CYPRESS_CURITY_USERNAME="test"
CYPRESS_CURITY_PASSWORD="test"
@@ -35,3 +36,4 @@ SEAMLESS_LOGOUT_FI="test"
SEAMLESS_LOGOUT_NO="test"
SEAMLESS_LOGOUT_SV="test"
WEBVIEW_ENCRYPTION_KEY="test"
+BOOKING_ENCRYPTION_KEY="test"
diff --git a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx
index b35861357..012c40de2 100644
--- a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx
+++ b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx
@@ -34,7 +34,6 @@ export default async function MyPages({
{formatMessage({ id: "No content published" })}
)}
-
>
)
diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx
index fe70a0371..c159e3926 100644
--- a/app/[lang]/(live)/layout.tsx
+++ b/app/[lang]/(live)/layout.tsx
@@ -15,14 +15,9 @@ import { getIntl } from "@/i18n"
import ServerIntlProvider from "@/i18n/Provider"
import { getLang, setLang } from "@/i18n/serverContext"
-import type { Metadata } from "next"
-
import type { LangParams, LayoutArgs } from "@/types/params"
-export const metadata: Metadata = {
- description: "New web",
- title: "Scandic Hotels",
-}
+export { generateMetadata } from "@/utils/generateMetadata"
export default async function RootLayout({
children,
@@ -35,8 +30,8 @@ export default async function RootLayout({
>) {
setLang(params.lang)
preloadUserTracking()
-
const { defaultLocale, locale, messages } = await getIntl()
+
return (
diff --git a/lib/graphql/Fragments/LoyaltyPage/MetaData.graphql b/lib/graphql/Fragments/LoyaltyPage/MetaData.graphql
new file mode 100644
index 000000000..a169b5182
--- /dev/null
+++ b/lib/graphql/Fragments/LoyaltyPage/MetaData.graphql
@@ -0,0 +1,20 @@
+#import "../Image.graphql"
+
+fragment LoyaltyPageMetaData on LoyaltyPage {
+ web {
+ seo_metadata {
+ title
+ description
+ imageConnection {
+ edges {
+ node {
+ ...Image
+ }
+ }
+ }
+ }
+ breadcrumbs {
+ title
+ }
+ }
+}
diff --git a/lib/graphql/Fragments/MyPages/MetaData.graphql b/lib/graphql/Fragments/MyPages/MetaData.graphql
new file mode 100644
index 000000000..644594401
--- /dev/null
+++ b/lib/graphql/Fragments/MyPages/MetaData.graphql
@@ -0,0 +1,20 @@
+#import "../Image.graphql"
+
+fragment MyPagesMetaData on AccountPage {
+ web {
+ seo_metadata {
+ title
+ description
+ imageConnection {
+ edges {
+ node {
+ ...Image
+ }
+ }
+ }
+ }
+ breadcrumbs {
+ title
+ }
+ }
+}
diff --git a/lib/graphql/Query/MetaDataLoyaltyPage.graphql b/lib/graphql/Query/MetaDataLoyaltyPage.graphql
new file mode 100644
index 000000000..9b6ba5d77
--- /dev/null
+++ b/lib/graphql/Query/MetaDataLoyaltyPage.graphql
@@ -0,0 +1,12 @@
+#import "../Fragments/LoyaltyPage/MetaData.graphql"
+
+query GetLoyaltyPageMetaData($locale: String!, $url: String!) {
+ all_loyalty_page(locale: $locale, where: { url: $url }) {
+ items {
+ ...LoyaltyPageMetaData
+ system {
+ uid
+ }
+ }
+ }
+}
diff --git a/lib/graphql/Query/MetaDataMyPages.graphql b/lib/graphql/Query/MetaDataMyPages.graphql
new file mode 100644
index 000000000..14e255f02
--- /dev/null
+++ b/lib/graphql/Query/MetaDataMyPages.graphql
@@ -0,0 +1,12 @@
+#import "../Fragments/MyPages/MetaData.graphql"
+
+query GetMyPagesMetaData($locale: String!, $url: String!) {
+ all_account_page(locale: $locale, where: { url: $url }) {
+ items {
+ ...MyPagesMetaData
+ system {
+ uid
+ }
+ }
+ }
+}
diff --git a/server/routers/contentstack/index.ts b/server/routers/contentstack/index.ts
index 127673eb0..c04dc867d 100644
--- a/server/routers/contentstack/index.ts
+++ b/server/routers/contentstack/index.ts
@@ -7,6 +7,7 @@ import { contentPageRouter } from "./contentPage"
import { hotelPageRouter } from "./hotelPage"
import { languageSwitcherRouter } from "./languageSwitcher"
import { loyaltyPageRouter } from "./loyaltyPage"
+import { metaDataRouter } from "./metadata"
import { myPagesRouter } from "./myPages"
export const contentstackRouter = router({
@@ -18,4 +19,5 @@ export const contentstackRouter = router({
loyaltyPage: loyaltyPageRouter,
contentPage: contentPageRouter,
myPages: myPagesRouter,
+ metaData: metaDataRouter,
})
diff --git a/server/routers/contentstack/metadata/index.ts b/server/routers/contentstack/metadata/index.ts
new file mode 100644
index 000000000..fa2618123
--- /dev/null
+++ b/server/routers/contentstack/metadata/index.ts
@@ -0,0 +1,5 @@
+import { mergeRouters } from "@/server/trpc"
+
+import { metaDataQueryRouter } from "./query"
+
+export const metaDataRouter = mergeRouters(metaDataQueryRouter)
diff --git a/server/routers/contentstack/metadata/output.ts b/server/routers/contentstack/metadata/output.ts
new file mode 100644
index 000000000..12024b3c6
--- /dev/null
+++ b/server/routers/contentstack/metadata/output.ts
@@ -0,0 +1,66 @@
+import { z } from "zod"
+
+export const getMetaDataSchema = z.object({
+ breadcrumbsTitle: z.string().optional(),
+ title: z.string().optional(),
+ description: z.string().optional(),
+ imageConnection: z
+ .object({
+ edges: z.array(
+ z.object({
+ node: z.object({
+ url: z.string(),
+ }),
+ })
+ ),
+ })
+ .optional(),
+})
+
+const page = z.object({
+ web: z.object({
+ seo_metadata: z.object({
+ title: z.string().optional(),
+ description: z.string().optional(),
+ imageConnection: z
+ .object({
+ edges: z.array(
+ z.object({
+ node: z.object({
+ url: z.string(),
+ }),
+ })
+ ),
+ })
+ .optional(),
+ }),
+ breadcrumbs: z.object({
+ title: z.string(),
+ }),
+ }),
+ system: z.object({
+ uid: z.string(),
+ }),
+})
+
+export type Page = z.infer
+
+const metaDataItems = z.object({
+ items: z.array(page),
+})
+
+export const validateMyPagesMetaDataContentstackSchema = z.object({
+ all_account_page: metaDataItems,
+})
+
+export type GetMyPagesMetaDataData = z.infer<
+ typeof validateMyPagesMetaDataContentstackSchema
+>
+
+export const validateLoyaltyPageMetaDataContentstackSchema = z.object({
+ all_loyalty_page: metaDataItems,
+})
+
+export type GetLoyaltyPageMetaDataData = z.infer<
+ typeof validateLoyaltyPageMetaDataContentstackSchema
+>
diff --git a/server/routers/contentstack/metadata/query.ts b/server/routers/contentstack/metadata/query.ts
new file mode 100644
index 000000000..38d773248
--- /dev/null
+++ b/server/routers/contentstack/metadata/query.ts
@@ -0,0 +1,71 @@
+import { GetLoyaltyPageMetaData } from "@/lib/graphql/Query/MetaDataLoyaltyPage.graphql"
+import { GetMyPagesMetaData } from "@/lib/graphql/Query/MetaDataMyPages.graphql"
+import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
+
+import {
+ type GetLoyaltyPageMetaDataData,
+ type GetMyPagesMetaDataData,
+ validateLoyaltyPageMetaDataContentstackSchema,
+ validateMyPagesMetaDataContentstackSchema,
+} from "./output"
+import { getMetaData, getResponse, Variables } from "./utils"
+
+import { PageTypeEnum } from "@/types/requests/pageType"
+
+async function getLoyaltyPageMetaData(variables: Variables) {
+ const response = await getResponse(
+ GetLoyaltyPageMetaData,
+ variables
+ )
+
+ const validatedMetaDataData =
+ validateLoyaltyPageMetaDataContentstackSchema.safeParse(response.data)
+
+ if (!validatedMetaDataData.success) {
+ console.error(
+ `Failed to validate Loyaltypage MetaData Data - (url: ${variables.url})`
+ )
+ console.error(validatedMetaDataData.error)
+ return null
+ }
+
+ return getMetaData(validatedMetaDataData.data.all_loyalty_page.items[0])
+}
+
+async function getMyPagesMetaData(variables: Variables) {
+ const response = await getResponse(
+ GetMyPagesMetaData,
+ variables
+ )
+
+ const validatedMetaDataData =
+ validateMyPagesMetaDataContentstackSchema.safeParse(response.data)
+
+ if (!validatedMetaDataData.success) {
+ console.error(
+ `Failed to validate My Page MetaData Data - (url: ${variables.url})`
+ )
+ console.error(validatedMetaDataData.error)
+ return null
+ }
+
+ return getMetaData(validatedMetaDataData.data.all_account_page.items[0])
+}
+
+export const metaDataQueryRouter = router({
+ get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
+ const variables = {
+ locale: ctx.lang,
+ url: ctx.pathname,
+ }
+
+ switch (ctx.contentType) {
+ case PageTypeEnum.accountPage:
+ return await getMyPagesMetaData(variables)
+ case PageTypeEnum.loyaltyPage:
+ return await getLoyaltyPageMetaData(variables)
+ default:
+ return []
+ }
+ }),
+})
diff --git a/server/routers/contentstack/metadata/utils.ts b/server/routers/contentstack/metadata/utils.ts
new file mode 100644
index 000000000..661c2b89d
--- /dev/null
+++ b/server/routers/contentstack/metadata/utils.ts
@@ -0,0 +1,35 @@
+import { Lang } from "@/constants/languages"
+import { request } from "@/lib/graphql/request"
+import { internalServerError, notFound } from "@/server/errors/trpc"
+
+import { getMetaDataSchema, Page } from "./output"
+
+export type Variables = {
+ locale: Lang
+ url: string
+}
+
+export async function getResponse(query: string, variables: Variables) {
+ const response = await request(query, variables)
+ if (!response.data) {
+ throw notFound(response)
+ }
+
+ return response
+}
+
+export function getMetaData(page: Page) {
+ const pageMetaData = {
+ breadcrumbsTitle: page.web.breadcrumbs.title,
+ title: page.web.seo_metadata.title,
+ description: page.web.seo_metadata.description,
+ imageConnection: page.web.seo_metadata.imageConnection,
+ uid: page.system.uid,
+ }
+ const validatedMetaData = getMetaDataSchema.safeParse(pageMetaData)
+ if (!validatedMetaData.success) {
+ throw internalServerError(validatedMetaData.error)
+ }
+
+ return validatedMetaData.data
+}
diff --git a/types/components/metadata/index.ts b/types/components/metadata/index.ts
new file mode 100644
index 000000000..bcc71f13f
--- /dev/null
+++ b/types/components/metadata/index.ts
@@ -0,0 +1,5 @@
+import { z } from "zod"
+
+import { getMetaDataSchema } from "@/server/routers/contentstack/metadata/output"
+
+export interface MetaData extends z.infer {}
diff --git a/utils/generateMetadata.ts b/utils/generateMetadata.ts
new file mode 100644
index 000000000..547c5ccd6
--- /dev/null
+++ b/utils/generateMetadata.ts
@@ -0,0 +1,35 @@
+import { Metadata } from "next"
+
+import { serverClient } from "@/lib/trpc/server"
+
+import { MetaData } from "@/types/components/metadata"
+
+export async function generateMetadata(): Promise {
+ const metaData: MetaData | never[] | null =
+ await serverClient().contentstack.metaData.get()
+
+ if (Array.isArray(metaData)) {
+ return {
+ title: "",
+ description: "",
+ openGraph: {
+ images: [],
+ },
+ }
+ }
+
+ const title = metaData?.breadcrumbsTitle ?? metaData?.title ?? ""
+ const description = metaData?.description ?? ""
+ const images =
+ metaData?.imageConnection?.edges?.map((edge) => ({
+ url: edge.node.url,
+ })) || []
+
+ return {
+ title,
+ description,
+ openGraph: {
+ images,
+ },
+ }
+}