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, + }, + } +}