From c730fa703511d7f55039c45c7676b09125905fe3 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 19 Aug 2024 13:03:46 +0200 Subject: [PATCH] feat(SW-266): Replacing static metadata with data from Contentstack on Loyalty pages and Account Pages --- .../(protected)/my-pages/[...path]/page.tsx | 10 ++- .../(public)/[contentType]/[uid]/page.tsx | 19 +++++ app/[lang]/(live)/layout.tsx | 10 +-- .../Fragments/LoyaltyPage/MetaData.graphql | 17 ++++ .../Fragments/MyPages/MetaData.graphql | 17 ++++ lib/graphql/Query/MetaDataLoyaltyPage.graphql | 12 +++ lib/graphql/Query/MetaDataMyPages.graphql | 12 +++ server/routers/contentstack/index.ts | 2 + .../contentstack/loyaltyPage/output.ts | 1 + .../routers/contentstack/loyaltyPage/query.ts | 1 + server/routers/contentstack/metadata/index.ts | 5 ++ .../routers/contentstack/metadata/output.ts | 62 +++++++++++++++ server/routers/contentstack/metadata/query.ts | 78 +++++++++++++++++++ server/routers/contentstack/metadata/utils.ts | 34 ++++++++ types/components/metadata/index.ts | 5 ++ utils/generateMetadata.ts | 39 ++++++++++ 16 files changed, 316 insertions(+), 8 deletions(-) create mode 100644 lib/graphql/Fragments/LoyaltyPage/MetaData.graphql create mode 100644 lib/graphql/Fragments/MyPages/MetaData.graphql create mode 100644 lib/graphql/Query/MetaDataLoyaltyPage.graphql create mode 100644 lib/graphql/Query/MetaDataMyPages.graphql create mode 100644 server/routers/contentstack/metadata/index.ts create mode 100644 server/routers/contentstack/metadata/output.ts create mode 100644 server/routers/contentstack/metadata/query.ts create mode 100644 server/routers/contentstack/metadata/utils.ts create mode 100644 types/components/metadata/index.ts create mode 100644 utils/generateMetadata.ts diff --git a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx index b35861357..584e0922b 100644 --- a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx @@ -5,11 +5,20 @@ import Title from "@/components/TempDesignSystem/Text/Title" import TrackingSDK from "@/components/TrackingSDK" import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" +import { generateMetadata as generateBaseMetadata } from "@/utils/generateMetadata" import styles from "./page.module.css" import type { LangParams, PageArgs } from "@/types/params" +export async function generateMetadata({ params }: PageArgs) { + const accountPageRes = await serverClient().contentstack.accountPage.get() + return generateBaseMetadata({ + params, + pageTitle: accountPageRes?.accountPage?.title, + }) +} + export default async function MyPages({ params, }: PageArgs) { @@ -34,7 +43,6 @@ export default async function MyPages({

{formatMessage({ id: "No content published" })}

)} - ) diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index 0e8e98a05..c5b13354b 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -1,9 +1,12 @@ import { notFound } from "next/navigation" +import { serverClient } from "@/lib/trpc/server" + import ContentPage from "@/components/ContentType/ContentPage" import HotelPage from "@/components/ContentType/HotelPage/HotelPage" import LoyaltyPage from "@/components/ContentType/LoyaltyPage/LoyaltyPage" import { setLang } from "@/i18n/serverContext" +import { generateMetadata as generateBaseMetadata } from "@/utils/generateMetadata" import { ContentTypeParams, @@ -12,6 +15,22 @@ import { UIDParams, } from "@/types/params" +export async function generateMetadata({ + params, +}: PageArgs) { + switch (params.contentType) { + case "loyalty-page": + const loyaltyPageRes = await serverClient().contentstack.loyaltyPage.get() + return generateBaseMetadata({ + params, + pageTitle: loyaltyPageRes?.loyaltyPage?.title, + }) + // Add case "hotel-pages" etc when needed + default: + return + } +} + export default async function ContentTypePage({ params, }: PageArgs) { diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index 2da54eb0a..9f9a9263c 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -12,15 +12,11 @@ import { preloadUserTracking } from "@/components/TrackingSDK" import { getIntl } from "@/i18n" import ServerIntlProvider from "@/i18n/Provider" import { getLang, setLang } from "@/i18n/serverContext" - -import type { Metadata } from "next" +import { generateMetadata } from "@/utils/generateMetadata" import type { LangParams, LayoutArgs } from "@/types/params" -export const metadata: Metadata = { - description: "New web", - title: "Scandic Hotels", -} +export { generateMetadata } export default async function RootLayout({ children, @@ -33,8 +29,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..dcb0c704d --- /dev/null +++ b/lib/graphql/Fragments/LoyaltyPage/MetaData.graphql @@ -0,0 +1,17 @@ +#import "../Image.graphql" + +fragment LoyaltyPageMetaData on LoyaltyPage { + web { + seo_metadata { + title + description + imageConnection { + edges { + node { + ...Image + } + } + } + } + } +} diff --git a/lib/graphql/Fragments/MyPages/MetaData.graphql b/lib/graphql/Fragments/MyPages/MetaData.graphql new file mode 100644 index 000000000..1971efe3f --- /dev/null +++ b/lib/graphql/Fragments/MyPages/MetaData.graphql @@ -0,0 +1,17 @@ +#import "../Image.graphql" + +fragment MyPagesMetaData on AccountPage { + web { + seo_metadata { + title + description + imageConnection { + edges { + node { + ...Image + } + } + } + } + } +} 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 be1ef1ed3..2394251b3 100644 --- a/server/routers/contentstack/index.ts +++ b/server/routers/contentstack/index.ts @@ -6,6 +6,7 @@ import { breadcrumbsRouter } from "./breadcrumbs" import { hotelPageRouter } from "./hotelPage" import { languageSwitcherRouter } from "./languageSwitcher" import { loyaltyPageRouter } from "./loyaltyPage" +import { metaDataRouter } from "./metadata" import { myPagesRouter } from "./myPages" export const contentstackRouter = router({ @@ -16,4 +17,5 @@ export const contentstackRouter = router({ languageSwitcher: languageSwitcherRouter, loyaltyPage: loyaltyPageRouter, myPages: myPagesRouter, + metaData: metaDataRouter, }) diff --git a/server/routers/contentstack/loyaltyPage/output.ts b/server/routers/contentstack/loyaltyPage/output.ts index 205509a4c..a95b2e341 100644 --- a/server/routers/contentstack/loyaltyPage/output.ts +++ b/server/routers/contentstack/loyaltyPage/output.ts @@ -190,6 +190,7 @@ const loyaltyPageSidebarItem = z.discriminatedUnion("__typename", [ ]) export const validateLoyaltyPageSchema = z.object({ + title: z.string(), heading: z.string().nullable(), blocks: z.array(loyaltyPageBlockItem).nullable(), sidebar: z.array(loyaltyPageSidebarItem).nullable(), diff --git a/server/routers/contentstack/loyaltyPage/query.ts b/server/routers/contentstack/loyaltyPage/query.ts index c23422c4a..7f4fee2e9 100644 --- a/server/routers/contentstack/loyaltyPage/query.ts +++ b/server/routers/contentstack/loyaltyPage/query.ts @@ -209,6 +209,7 @@ export const loyaltyPageQueryRouter = router({ : null const loyaltyPage = { + title: response.data.loyalty_page.title, heading: response.data.loyalty_page.heading, system: response.data.loyalty_page.system, blocks, 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..ae0367a3c --- /dev/null +++ b/server/routers/contentstack/metadata/output.ts @@ -0,0 +1,62 @@ +import { z } from "zod" + +export const getMetaDataSchema = 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(), +}) + +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(), + }), + }), + 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..3dbc2f2a2 --- /dev/null +++ b/server/routers/contentstack/metadata/query.ts @@ -0,0 +1,78 @@ +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 + ) + + if (!response.data.all_loyalty_page.items[0].web?.seo_metadata?.title) { + return null + } + 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 + ) + + if (!response.data.all_account_page.items[0].web?.seo_metadata?.title) { + return [] + } + + 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..f2e80b604 --- /dev/null +++ b/server/routers/contentstack/metadata/utils.ts @@ -0,0 +1,34 @@ +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 = { + 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..e44e14354 --- /dev/null +++ b/utils/generateMetadata.ts @@ -0,0 +1,39 @@ +import { Metadata } from "next" + +import { serverClient } from "@/lib/trpc/server" + +import { MetaData } from "@/types/components/metadata" +import { LangParams, PageArgs } from "@/types/params" + +export async function generateMetadata({ + params, + pageTitle, +}: PageArgs & { pageTitle?: string }): Promise { + console.log("PARAMS", params) + const metaData: MetaData | never[] | null = + await serverClient().contentstack.metaData.get() + + if (Array.isArray(metaData)) { + return { + title: pageTitle ?? "", + description: "", + openGraph: { + images: [], + }, + } + } + const title = metaData?.title ?? pageTitle ?? "" + const description = metaData?.description ?? "" + const images = + metaData?.imageConnection?.edges?.map((edge) => ({ + url: edge.node.url, + })) || [] + + return { + title, + description, + openGraph: { + images, + }, + } +}