feat(SW-200): refactoring SEO metadata handling and added functionality for static pages

This commit is contained in:
Erik Tiekstra
2024-11-14 07:22:38 +01:00
parent 92ad7192b1
commit 28738d7161
17 changed files with 278 additions and 217 deletions

View File

@@ -11,7 +11,6 @@ import {
shortcutsSchema,
} from "../schemas/blocks/shortcuts"
import { textContentSchema } from "../schemas/blocks/textContent"
import { page } from "../schemas/metadata"
import { systemSchema } from "../schemas/system"
import { AccountPageEnum } from "@/types/enums/accountPage"
@@ -81,7 +80,3 @@ export const accountPageRefsSchema = z.object({
system: systemSchema,
}),
})
export const accountPageMetadataSchema = z.object({
account_page: page,
})

View File

@@ -5,7 +5,6 @@ import {
GetAccountPage,
GetAccountPageRefs,
} from "@/lib/graphql/Query/AccountPage/AccountPage.graphql"
import { GetMyPagesMetaData } from "@/lib/graphql/Query/AccountPage/MetaData.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
@@ -16,13 +15,7 @@ import {
generateTagsFromSystem,
} from "@/utils/generateTag"
import { removeEmptyObjects } from "../../utils"
import { getMetaData, getResponse } from "../metadata/utils"
import {
accountPageMetadataSchema,
accountPageRefsSchema,
accountPageSchema,
} from "./output"
import { accountPageRefsSchema, accountPageSchema } from "./output"
import { getConnections } from "./utils"
import {
@@ -30,7 +23,6 @@ import {
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type {
GetAccountpageMetadata,
GetAccountPageRefsSchema,
GetAccountPageSchema,
} from "@/types/trpc/routers/contentstack/accountPage"
@@ -203,30 +195,4 @@ export const accountPageQueryRouter = router({
tracking,
}
}),
metadata: router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
const variables = {
locale: ctx.lang,
uid: ctx.uid,
}
const response = await getResponse<GetAccountpageMetadata>(
GetMyPagesMetaData,
variables
)
const validatedMetadata = accountPageMetadataSchema.safeParse(
response.data
)
if (!validatedMetadata.success) {
console.error(
`Failed to validate My Page MetaData Data - (uid: ${variables.uid})`
)
console.error(validatedMetadata.error)
return null
}
return getMetaData(validatedMetadata.data.account_page)
}),
}),
})

View File

@@ -1,11 +1,52 @@
import { z } from "zod"
import { page } from "../schemas/metadata"
import { getDescription, getImages, getTitle } from "./utils"
export const getLoyaltyPageMetadataSchema = z.object({
loyalty_page: page,
export const rawMetaDataDataSchema = z.object({
web: z.object({
seo_metadata: z
.object({
title: z.string().optional().nullable(),
description: z.string().optional().nullable(),
imageConnection: z
.object({
edges: z.array(
z.object({
node: z.object({
url: z.string(),
}),
})
),
})
.optional()
.nullable(),
})
.optional()
.nullable(),
breadcrumbs: z
.object({
title: z.string().optional().nullable(),
})
.optional()
.nullable(),
}),
heading: z.string().optional().nullable(),
preamble: z.string().optional().nullable(),
header: z
.object({
heading: z.string().optional().nullable(),
preamble: z.string().optional().nullable(),
})
.optional()
.nullable(),
})
export type GetLoyaltyPageMetaDataData = z.infer<
typeof getLoyaltyPageMetadataSchema
>
export const metaDataSchema = rawMetaDataDataSchema.transform((data) => {
return {
title: getTitle(data),
description: getDescription(data),
openGraph: {
images: getImages(data),
},
}
})

View File

@@ -1,45 +1,144 @@
import { metrics } from "@opentelemetry/api"
import { cache } from "react"
import { Lang } from "@/constants/languages"
import { GetAccountPageMetaData } from "@/lib/graphql/Query/AccountPage/MetaData.graphql"
import { GetCollectionPageMetaData } from "@/lib/graphql/Query/CollectionPage/MetaData.graphql"
import { GetContentPageMetaData } from "@/lib/graphql/Query/ContentPage/MetaData.graphql"
import { GetLoyaltyPageMetaData } from "@/lib/graphql/Query/LoyaltyPage/MetaData.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import {
type GetLoyaltyPageMetaDataData,
getLoyaltyPageMetadataSchema,
} from "./output"
import { getMetaData, getResponse, type Variables } from "./utils"
import { generateTag } from "@/utils/generateTag"
import { metaDataSchema } from "./output"
import { affix } from "./utils"
import { PageTypeEnum } from "@/types/requests/pageType"
import { RawMetaDataSchema } from "@/types/trpc/routers/contentstack/metadata"
async function getLoyaltyPageMetaData(variables: Variables) {
const response = await getResponse<GetLoyaltyPageMetaDataData>(
GetLoyaltyPageMetaData,
variables
const meter = metrics.getMeter("trpc.metaData")
// OpenTelemetry metrics
const fetchMetaDataCounter = meter.createCounter(
"trpc.contentstack.metaData.get"
)
const fetchMetaDataSuccessCounter = meter.createCounter(
"trpc.contentstack.metaData.get-success"
)
const fetchMetaDataFailCounter = meter.createCounter(
"trpc.contentstack.metaData.get-fail"
)
const transformMetaDataCounter = meter.createCounter(
"trpc.contentstack.metaData.transform"
)
const transformMetaDataSuccessCounter = meter.createCounter(
"trpc.contentstack.metaData.transform-success"
)
const transformMetaDataFailCounter = meter.createCounter(
"trpc.contentstack.metaData.transform-fail"
)
const fetchMetaData = cache(async function fetchMemoizedMetaData<T>(
query: string,
{ uid, lang }: { uid: string; lang: Lang }
) {
fetchMetaDataCounter.add(1, { lang, uid })
console.info(
"contentstack.metaData fetch start",
JSON.stringify({ query: { lang, uid } })
)
const validatedMetadata = getLoyaltyPageMetadataSchema.safeParse(
response.data
const response = await request<T>(
query,
{ locale: lang, uid },
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid, affix)],
},
}
)
if (!validatedMetadata.success) {
if (!response.data) {
const notFoundError = notFound(response)
fetchMetaDataFailCounter.add(1, {
lang,
uid,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
`Failed to validate Loyaltypage MetaData Data - (uid: ${variables.uid})`
"contentstack.metaData fetch not found error",
JSON.stringify({
query: { lang, uid },
error: { code: notFoundError.code },
})
)
throw notFoundError
}
fetchMetaDataSuccessCounter.add(1, { lang, uid })
console.info(
"contentstack.metaData fetch success",
JSON.stringify({ query: { lang, uid } })
)
return response.data
})
function getTransformedMetaData(data: unknown) {
transformMetaDataCounter.add(1)
console.info("contentstack.metaData transform start")
const validatedMetaData = metaDataSchema.safeParse(data)
if (!validatedMetaData.success) {
transformMetaDataFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(validatedMetaData.error),
})
console.error(
"contentstack.metaData validation error",
JSON.stringify({
error: validatedMetaData.error,
})
)
console.error(validatedMetadata.error)
return null
}
return getMetaData(validatedMetadata.data.loyalty_page)
transformMetaDataSuccessCounter.add(1)
console.info("contentstack.metaData transform success")
return validatedMetaData.data
}
export const metaDataQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
const variables = {
locale: ctx.lang,
lang: ctx.lang,
uid: ctx.uid,
}
switch (ctx.contentType) {
case PageTypeEnum.accountPage:
const accountPageResponse = await fetchMetaData<{
account_page: RawMetaDataSchema
}>(GetAccountPageMetaData, variables)
return getTransformedMetaData(accountPageResponse.account_page)
case PageTypeEnum.collectionPage:
const collectionPageResponse = await fetchMetaData<{
collection_page: RawMetaDataSchema
}>(GetCollectionPageMetaData, variables)
return getTransformedMetaData(collectionPageResponse.collection_page)
case PageTypeEnum.contentPage:
const contentPageResponse = await fetchMetaData<{
content_page: RawMetaDataSchema
}>(GetContentPageMetaData, variables)
return getTransformedMetaData(contentPageResponse.content_page)
case PageTypeEnum.loyaltyPage:
return await getLoyaltyPageMetaData(variables)
const loyaltyPageResponse = await fetchMetaData<{
loyalty_page: RawMetaDataSchema
}>(GetLoyaltyPageMetaData, variables)
return getTransformedMetaData(loyaltyPageResponse.loyalty_page)
default:
return null
}

View File

@@ -1,44 +1,44 @@
import { Lang } from "@/constants/languages"
import { request } from "@/lib/graphql/request"
import { internalServerError, notFound } from "@/server/errors/trpc"
import { generateTag } from "@/utils/generateTag"
import { getMetaDataSchema, Page } from "../schemas/metadata"
export type Variables = {
locale: Lang
uid: string
}
import { RawMetaDataSchema } from "@/types/trpc/routers/contentstack/metadata"
export const affix = "metadata"
export async function getResponse<T>(query: string, variables: Variables) {
const response = await request<T>(query, variables, {
cache: "force-cache",
next: {
tags: [generateTag(variables.locale, variables.uid, affix)],
},
})
if (!response.data) {
throw notFound(response)
export function getTitle(data: RawMetaDataSchema) {
const metaData = data.web.seo_metadata
if (metaData?.title) {
return metaData.title
}
return response
if (data.web?.breadcrumbs?.title) {
return data.web.breadcrumbs.title
}
if (data.heading) {
return data.heading
}
if (data.header?.heading) {
return data.header.heading
}
return ""
}
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,
export function getDescription(data: RawMetaDataSchema) {
const metaData = data.web.seo_metadata
if (metaData?.description) {
return metaData.description
}
const validatedMetaData = getMetaDataSchema.safeParse(pageMetaData)
if (!validatedMetaData.success) {
throw internalServerError(validatedMetaData.error)
if (data.preamble) {
return data.preamble
}
return validatedMetaData.data
if (data.header?.preamble) {
return data.header.preamble
}
return ""
}
export function getImages(data: RawMetaDataSchema) {
const metaData = data.web.seo_metadata
if (metaData?.imageConnection) {
return metaData.imageConnection.edges.map((edge) => ({
url: edge.node.url,
}))
}
return []
}

View File

@@ -1,46 +0,0 @@
import { z } from "zod"
import { systemSchema } from "./system"
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(),
})
export 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: systemSchema,
})
export type Page = z.infer<typeof page>