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

@@ -10,7 +10,7 @@ import styles from "./page.module.css"
import type { LangParams, PageArgs } from "@/types/params"
export { generateMetadataAccountPage as generateMetadata } from "@/utils/generateMetadata"
export { generateMetadata } from "@/utils/generateMetadata"
export default async function MyPages({
params,

View File

@@ -1,5 +1,5 @@
import ProfilePage from "../page"
export { generateMetadataAccountPage as generateMetadata } from "@/utils/generateMetadata"
export { generateMetadata } from "@/utils/generateMetadata"
export default ProfilePage

View File

@@ -7,7 +7,7 @@ import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
export { generateMetadataAccountPage as generateMetadata } from "@/utils/generateMetadata"
export { generateMetadata } from "@/utils/generateMetadata"
export default async function ProfilePage({ params }: PageArgs<LangParams>) {
setLang(params.lang)

View File

@@ -1,7 +1,7 @@
#import "../../Fragments/Image.graphql"
#import "../../Fragments/System.graphql"
query GetMyPagesMetaData($locale: String!, $uid: String!) {
query GetAccountPageMetaData($locale: String!, $uid: String!) {
account_page(locale: $locale, uid: $uid) {
system {
...System

View File

@@ -0,0 +1,30 @@
#import "../../Fragments/Image.graphql"
#import "../../Fragments/System.graphql"
query GetCollectionPageMetaData($locale: String!, $uid: String!) {
collection_page(locale: $locale, uid: $uid) {
header {
heading
preamble
}
system {
...System
}
web {
breadcrumbs {
title
}
seo_metadata {
description
title
imageConnection {
edges {
node {
...Image
}
}
}
}
}
}
}

View File

@@ -0,0 +1,30 @@
#import "../../Fragments/Image.graphql"
#import "../../Fragments/System.graphql"
query GetContentPageMetaData($locale: String!, $uid: String!) {
content_page(locale: $locale, uid: $uid) {
header {
heading
preamble
}
system {
...System
}
web {
breadcrumbs {
title
}
seo_metadata {
description
title
imageConnection {
edges {
node {
...Image
}
}
}
}
}
}
}

View File

@@ -3,6 +3,8 @@
query GetLoyaltyPageMetaData($locale: String!, $uid: String!) {
loyalty_page(locale: $locale, uid: $uid) {
heading
preamble
system {
...System
}

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>

View File

@@ -1,5 +0,0 @@
import { z } from "zod"
import { getMetaDataSchema } from "@/server/routers/contentstack/schemas/metadata"
export interface MetaData extends z.infer<typeof getMetaDataSchema> {}

View File

@@ -1,7 +1,6 @@
import { z } from "zod"
import {
accountPageMetadataSchema,
accountPageRefsSchema,
accountPageSchema,
blocksSchema,
@@ -18,10 +17,4 @@ export interface GetAccountPageSchema
export interface AccountPage extends z.output<typeof accountPageSchema> {}
export interface GetAccountpageMetadata
extends z.output<typeof accountPageMetadataSchema> {}
export interface AccountPageMetadata
extends z.output<typeof accountPageMetadataSchema> {}
export type Block = z.output<typeof blocksSchema>

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
import {
metaDataSchema,
rawMetaDataDataSchema,
} from "@/server/routers/contentstack/metadata/output"
export interface RawMetaDataSchema
extends z.input<typeof rawMetaDataDataSchema> {}
export interface MetaDataSchema extends z.output<typeof metaDataSchema> {}

View File

@@ -1,59 +1,5 @@
import { serverClient } from "@/lib/trpc/server"
export async function generateMetadata() {
const metaData = await serverClient().contentstack.metaData.get()
if (!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,
},
}
}
export async function generateMetadataAccountPage() {
const metaData = await serverClient().contentstack.accountPage.metadata.get()
if (!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,
},
}
return await serverClient().contentstack.metaData.get()
}