feat(SW-200): refactoring SEO metadata handling and added functionality for static pages
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ProfilePage from "../page"
|
||||
|
||||
export { generateMetadataAccountPage as generateMetadata } from "@/utils/generateMetadata"
|
||||
export { generateMetadata } from "@/utils/generateMetadata"
|
||||
|
||||
export default ProfilePage
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
30
lib/graphql/Query/CollectionPage/Metadata.graphql
Normal file
30
lib/graphql/Query/CollectionPage/Metadata.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
lib/graphql/Query/ContentPage/Metadata.graphql
Normal file
30
lib/graphql/Query/ContentPage/Metadata.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
query GetLoyaltyPageMetaData($locale: String!, $uid: String!) {
|
||||
loyalty_page(locale: $locale, uid: $uid) {
|
||||
heading
|
||||
preamble
|
||||
system {
|
||||
...System
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -1,5 +0,0 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { getMetaDataSchema } from "@/server/routers/contentstack/schemas/metadata"
|
||||
|
||||
export interface MetaData extends z.infer<typeof getMetaDataSchema> {}
|
||||
@@ -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>
|
||||
|
||||
10
types/trpc/routers/contentstack/metadata.ts
Normal file
10
types/trpc/routers/contentstack/metadata.ts
Normal 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> {}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user