feat(SW-200): Added noindex property to seo metadata in contentstack and added implementation

This commit is contained in:
Erik Tiekstra
2024-11-15 09:25:54 +01:00
parent 6aba0d8f52
commit 50f285776a
19 changed files with 159 additions and 160 deletions

View File

@@ -1,5 +1,3 @@
import { getBreadcrumbsListSchema } from "@/utils/getJsonSchemas"
import styles from "./layout.module.css" import styles from "./layout.module.css"
import { import {
@@ -9,9 +7,7 @@ import {
UIDParams, UIDParams,
} from "@/types/params" } from "@/types/params"
export { generateMetadata } from "@/utils/generateMetadata" export default function ContentTypeLayout({
export default async function ContentTypeLayout({
breadcrumbs, breadcrumbs,
preview, preview,
children, children,
@@ -21,25 +17,13 @@ export default async function ContentTypeLayout({
preview: React.ReactNode preview: React.ReactNode
} }
>) { >) {
const breadcrumbsListSchema = await getBreadcrumbsListSchema()
return ( return (
<> <div className={styles.container}>
{breadcrumbsListSchema ? ( <section className={styles.layout}>
<script {preview}
type={breadcrumbsListSchema.type} {breadcrumbs}
dangerouslySetInnerHTML={{ {children}
__html: JSON.stringify(breadcrumbsListSchema.jsonLd), </section>
}} </div>
/>
) : null}
<div className={styles.container}>
<section className={styles.layout}>
{preview}
{breadcrumbs}
{children}
</section>
</div>
</>
) )
} }

View File

@@ -17,6 +17,8 @@ import {
UIDParams, UIDParams,
} from "@/types/params" } from "@/types/params"
export { generateMetadata } from "@/utils/generateMetadata"
export default function ContentTypePage({ export default function ContentTypePage({
params, params,
}: PageArgs<LangParams & ContentTypeParams & UIDParams, {}>) { }: PageArgs<LangParams & ContentTypeParams & UIDParams, {}>) {

View File

@@ -1,6 +1,7 @@
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import BreadcrumbsComp from "@/components/TempDesignSystem/Breadcrumbs" import BreadcrumbsComp from "@/components/TempDesignSystem/Breadcrumbs"
import { generateBreadcrumbsSchema } from "@/utils/jsonSchemas"
export default async function Breadcrumbs() { export default async function Breadcrumbs() {
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get() const breadcrumbs = await serverClient().contentstack.breadcrumbs.get()
@@ -8,6 +9,17 @@ export default async function Breadcrumbs() {
if (!breadcrumbs?.length) { if (!breadcrumbs?.length) {
return null return null
} }
const jsonSchema = generateBreadcrumbsSchema(breadcrumbs)
return <BreadcrumbsComp breadcrumbs={breadcrumbs} /> return (
<>
<script
type={jsonSchema.type}
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonSchema.jsonLd),
}}
/>
<BreadcrumbsComp breadcrumbs={breadcrumbs} />
</>
)
} }

View File

@@ -1,13 +0,0 @@
fragment MetaDataImageConnection on SeoMetadata {
imageConnection {
edges {
node {
dimension {
height
width
}
url(transform: { width: "1200" })
}
}
}
}

View File

@@ -0,0 +1,30 @@
fragment MetadataImageConnection on SeoMetadata {
imageConnection {
edges {
node {
dimension {
height
width
}
url(transform: { width: "1200" })
}
}
}
}
fragment Metadata on SeoMetadata {
noindex
description
title
imageConnection {
edges {
node {
dimension {
height
width
}
url(transform: { width: "1200" })
}
}
}
}

View File

@@ -1,16 +1,14 @@
#import "../../Fragments/MetaData.graphql" #import "../../Fragments/Metadata.graphql"
#import "../../Fragments/System.graphql" #import "../../Fragments/System.graphql"
query GetAccountPageMetaData($locale: String!, $uid: String!) { query GetAccountPageMetadata($locale: String!, $uid: String!) {
account_page(locale: $locale, uid: $uid) { account_page(locale: $locale, uid: $uid) {
web { web {
breadcrumbs { breadcrumbs {
title title
} }
seo_metadata { seo_metadata {
description ...Metadata
title
...MetaDataImageConnection
} }
} }
system { system {

View File

@@ -1,16 +1,14 @@
#import "../../Fragments/MetaData.graphql" #import "../../Fragments/Metadata.graphql"
#import "../../Fragments/System.graphql" #import "../../Fragments/System.graphql"
query GetCollectionPageMetaData($locale: String!, $uid: String!) { query GetCollectionPageMetadata($locale: String!, $uid: String!) {
collection_page(locale: $locale, uid: $uid) { collection_page(locale: $locale, uid: $uid) {
web { web {
breadcrumbs { breadcrumbs {
title title
} }
seo_metadata { seo_metadata {
description ...Metadata
title
...MetaDataImageConnection
} }
} }
header { header {

View File

@@ -1,16 +1,14 @@
#import "../../Fragments/MetaData.graphql" #import "../../Fragments/Metadata.graphql"
#import "../../Fragments/System.graphql" #import "../../Fragments/System.graphql"
query GetContentPageMetaData($locale: String!, $uid: String!) { query GetContentPageMetadata($locale: String!, $uid: String!) {
content_page(locale: $locale, uid: $uid) { content_page(locale: $locale, uid: $uid) {
web { web {
breadcrumbs { breadcrumbs {
title title
} }
seo_metadata { seo_metadata {
description ...Metadata
title
...MetaDataImageConnection
} }
} }
header { header {

View File

@@ -1,17 +1,15 @@
#import "../../Fragments/MetaData.graphql" #import "../../Fragments/Metadata.graphql"
#import "../../Fragments/System.graphql" #import "../../Fragments/System.graphql"
query GetLoyaltyPageMetaData($locale: String!, $uid: String!) { query GetLoyaltyPageMetadata($locale: String!, $uid: String!) {
loyalty_page(locale: $locale, uid: $uid) { loyalty_page(locale: $locale, uid: $uid) {
web { web {
seo_metadata {
description
title
...MetaDataImageConnection
}
breadcrumbs { breadcrumbs {
title title
} }
seo_metadata {
...Metadata
}
} }
heading heading
preamble preamble

View File

@@ -1,7 +1,6 @@
import { metrics } from "@opentelemetry/api" import { metrics } from "@opentelemetry/api"
import { cache } from "react" import { cache } from "react"
import { Lang } from "@/constants/languages"
import { import {
GetMyPagesBreadcrumbs, GetMyPagesBreadcrumbs,
GetMyPagesBreadcrumbsRefs, GetMyPagesBreadcrumbsRefs,
@@ -26,10 +25,11 @@ import { breadcrumbsRefsSchema, breadcrumbsSchema } from "./output"
import { getTags } from "./utils" import { getTags } from "./utils"
import { PageTypeEnum } from "@/types/requests/pageType" import { PageTypeEnum } from "@/types/requests/pageType"
import { import type {
BreadcrumbsRefsSchema, BreadcrumbsRefsSchema,
RawBreadcrumbsSchema, RawBreadcrumbsSchema,
} from "@/types/trpc/routers/contentstack/breadcrumbs" } from "@/types/trpc/routers/contentstack/breadcrumbs"
import type { Lang } from "@/constants/languages"
const meter = metrics.getMeter("trpc.breadcrumbs") const meter = metrics.getMeter("trpc.breadcrumbs")

View File

@@ -4,12 +4,7 @@ import { generateTag, generateTags } from "@/utils/generateTag"
import type { Edges } from "@/types/requests/utils/edges" import type { Edges } from "@/types/requests/utils/edges"
import type { NodeRefs } from "@/types/requests/utils/refs" import type { NodeRefs } from "@/types/requests/utils/refs"
import { BreadcrumbsRefsSchema } from "@/types/trpc/routers/contentstack/breadcrumbs" import type { BreadcrumbsRefsSchema } from "@/types/trpc/routers/contentstack/breadcrumbs"
export type Variables = {
lang: Lang
uid: string
}
export const affix = "breadcrumbs" export const affix = "breadcrumbs"

View File

@@ -10,7 +10,7 @@ import { hotelPageRouter } from "./hotelPage"
import { languageSwitcherRouter } from "./languageSwitcher" import { languageSwitcherRouter } from "./languageSwitcher"
import { loyaltyLevelRouter } from "./loyaltyLevel" import { loyaltyLevelRouter } from "./loyaltyLevel"
import { loyaltyPageRouter } from "./loyaltyPage" import { loyaltyPageRouter } from "./loyaltyPage"
import { metaDataRouter } from "./metadata" import { metadataRouter } from "./metadata"
import { myPagesRouter } from "./myPages" import { myPagesRouter } from "./myPages"
import { rewardRouter } from "./reward" import { rewardRouter } from "./reward"
@@ -25,7 +25,7 @@ export const contentstackRouter = router({
collectionPage: collectionPageRouter, collectionPage: collectionPageRouter,
contentPage: contentPageRouter, contentPage: contentPageRouter,
myPages: myPagesRouter, myPages: myPagesRouter,
metaData: metaDataRouter, metadata: metadataRouter,
rewards: rewardRouter, rewards: rewardRouter,
loyaltyLevels: loyaltyLevelRouter, loyaltyLevels: loyaltyLevelRouter,
}) })

View File

@@ -1,5 +1,5 @@
import { mergeRouters } from "@/server/trpc" import { mergeRouters } from "@/server/trpc"
import { metaDataQueryRouter } from "./query" import { metadataQueryRouter } from "./query"
export const metaDataRouter = mergeRouters(metaDataQueryRouter) export const metadataRouter = mergeRouters(metadataQueryRouter)

View File

@@ -3,14 +3,17 @@ import { z } from "zod"
import { tempImageVaultAssetSchema } from "../schemas/imageVault" import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { getDescription, getImages, getTitle } from "./utils" import { getDescription, getImages, getTitle } from "./utils"
import type { Metadata } from "next"
import { RTETypeEnum } from "@/types/rte/enums" import { RTETypeEnum } from "@/types/rte/enums"
export const rawMetaDataDataSchema = z.object({ export const rawMetadataSchema = z.object({
web: z.object({ web: z.object({
seo_metadata: z seo_metadata: z
.object({ .object({
title: z.string().optional().nullable(), title: z.string().optional().nullable(),
description: z.string().optional().nullable(), description: z.string().optional().nullable(),
noindex: z.boolean().optional().nullable(),
imageConnection: z imageConnection: z
.object({ .object({
edges: z.array( edges: z.array(
@@ -78,12 +81,17 @@ export const rawMetaDataDataSchema = z.object({
.nullable(), .nullable(),
}) })
export const metaDataSchema = rawMetaDataDataSchema.transform((data) => { export const metadataSchema = rawMetadataSchema.transform((data) => {
return { const metadata: Metadata = {
robots: {
index: !data.web.seo_metadata?.noindex,
follow: true,
},
title: getTitle(data), title: getTitle(data),
description: getDescription(data), description: getDescription(data),
openGraph: { openGraph: {
images: getImages(data), images: getImages(data),
}, },
} }
return metadata
}) })

View File

@@ -1,52 +1,52 @@
import { metrics } from "@opentelemetry/api" import { metrics } from "@opentelemetry/api"
import { cache } from "react" import { cache } from "react"
import { Lang } from "@/constants/languages" import { GetAccountPageMetadata } from "@/lib/graphql/Query/AccountPage/Metadata.graphql"
import { GetAccountPageMetaData } from "@/lib/graphql/Query/AccountPage/MetaData.graphql" import { GetCollectionPageMetadata } from "@/lib/graphql/Query/CollectionPage/Metadata.graphql"
import { GetCollectionPageMetaData } from "@/lib/graphql/Query/CollectionPage/MetaData.graphql" import { GetContentPageMetadata } from "@/lib/graphql/Query/ContentPage/Metadata.graphql"
import { GetContentPageMetaData } from "@/lib/graphql/Query/ContentPage/MetaData.graphql" import { GetLoyaltyPageMetadata } from "@/lib/graphql/Query/LoyaltyPage/Metadata.graphql"
import { GetLoyaltyPageMetaData } from "@/lib/graphql/Query/LoyaltyPage/MetaData.graphql"
import { request } from "@/lib/graphql/request" import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc" import { notFound } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc" import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag" import { generateTag } from "@/utils/generateTag"
import { metaDataSchema } from "./output" import { metadataSchema } from "./output"
import { affix } from "./utils" import { affix } from "./utils"
import { PageTypeEnum } from "@/types/requests/pageType" import { PageTypeEnum } from "@/types/requests/pageType"
import { RawMetaDataSchema } from "@/types/trpc/routers/contentstack/metadata" import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
import type { Lang } from "@/constants/languages"
const meter = metrics.getMeter("trpc.metaData") const meter = metrics.getMeter("trpc.metadata")
// OpenTelemetry metrics // OpenTelemetry metrics
const fetchMetaDataCounter = meter.createCounter( const fetchMetadataCounter = meter.createCounter(
"trpc.contentstack.metaData.get" "trpc.contentstack.metadata.get"
) )
const fetchMetaDataSuccessCounter = meter.createCounter( const fetchMetadataSuccessCounter = meter.createCounter(
"trpc.contentstack.metaData.get-success" "trpc.contentstack.metadata.get-success"
) )
const fetchMetaDataFailCounter = meter.createCounter( const fetchMetadataFailCounter = meter.createCounter(
"trpc.contentstack.metaData.get-fail" "trpc.contentstack.metadata.get-fail"
) )
const transformMetaDataCounter = meter.createCounter( const transformMetadataCounter = meter.createCounter(
"trpc.contentstack.metaData.transform" "trpc.contentstack.metadata.transform"
) )
const transformMetaDataSuccessCounter = meter.createCounter( const transformMetadataSuccessCounter = meter.createCounter(
"trpc.contentstack.metaData.transform-success" "trpc.contentstack.metadata.transform-success"
) )
const transformMetaDataFailCounter = meter.createCounter( const transformMetadataFailCounter = meter.createCounter(
"trpc.contentstack.metaData.transform-fail" "trpc.contentstack.metadata.transform-fail"
) )
const fetchMetaData = cache(async function fetchMemoizedMetaData<T>( const fetchMetadata = cache(async function fetchMemoizedMetadata<T>(
query: string, query: string,
{ uid, lang }: { uid: string; lang: Lang } { uid, lang }: { uid: string; lang: Lang }
) { ) {
fetchMetaDataCounter.add(1, { lang, uid }) fetchMetadataCounter.add(1, { lang, uid })
console.info( console.info(
"contentstack.metaData fetch start", "contentstack.metadata fetch start",
JSON.stringify({ query: { lang, uid } }) JSON.stringify({ query: { lang, uid } })
) )
const response = await request<T>( const response = await request<T>(
@@ -61,14 +61,14 @@ const fetchMetaData = cache(async function fetchMemoizedMetaData<T>(
) )
if (!response.data) { if (!response.data) {
const notFoundError = notFound(response) const notFoundError = notFound(response)
fetchMetaDataFailCounter.add(1, { fetchMetadataFailCounter.add(1, {
lang, lang,
uid, uid,
error_type: "not_found", error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }), error: JSON.stringify({ code: notFoundError.code }),
}) })
console.error( console.error(
"contentstack.metaData fetch not found error", "contentstack.metadata fetch not found error",
JSON.stringify({ JSON.stringify({
query: { lang, uid }, query: { lang, uid },
error: { code: notFoundError.code }, error: { code: notFoundError.code },
@@ -77,41 +77,41 @@ const fetchMetaData = cache(async function fetchMemoizedMetaData<T>(
throw notFoundError throw notFoundError
} }
fetchMetaDataSuccessCounter.add(1, { lang, uid }) fetchMetadataSuccessCounter.add(1, { lang, uid })
console.info( console.info(
"contentstack.metaData fetch success", "contentstack.metadata fetch success",
JSON.stringify({ query: { lang, uid } }) JSON.stringify({ query: { lang, uid } })
) )
return response.data return response.data
}) })
function getTransformedMetaData(data: unknown) { function getTransformedMetadata(data: unknown) {
transformMetaDataCounter.add(1) transformMetadataCounter.add(1)
console.info("contentstack.metaData transform start") console.info("contentstack.metadata transform start")
const validatedMetaData = metaDataSchema.safeParse(data) const validatedMetadata = metadataSchema.safeParse(data)
if (!validatedMetaData.success) { if (!validatedMetadata.success) {
transformMetaDataFailCounter.add(1, { transformMetadataFailCounter.add(1, {
error_type: "validation_error", error_type: "validation_error",
error: JSON.stringify(validatedMetaData.error), error: JSON.stringify(validatedMetadata.error),
}) })
console.error( console.error(
"contentstack.metaData validation error", "contentstack.metadata validation error",
JSON.stringify({ JSON.stringify({
error: validatedMetaData.error, error: validatedMetadata.error,
}) })
) )
return null return null
} }
transformMetaDataSuccessCounter.add(1) transformMetadataSuccessCounter.add(1)
console.info("contentstack.metaData transform success") console.info("contentstack.metadata transform success")
return validatedMetaData.data return validatedMetadata.data
} }
export const metaDataQueryRouter = router({ export const metadataQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
const variables = { const variables = {
lang: ctx.lang, lang: ctx.lang,
@@ -120,25 +120,25 @@ export const metaDataQueryRouter = router({
switch (ctx.contentType) { switch (ctx.contentType) {
case PageTypeEnum.accountPage: case PageTypeEnum.accountPage:
const accountPageResponse = await fetchMetaData<{ const accountPageResponse = await fetchMetadata<{
account_page: RawMetaDataSchema account_page: RawMetadataSchema
}>(GetAccountPageMetaData, variables) }>(GetAccountPageMetadata, variables)
return getTransformedMetaData(accountPageResponse.account_page) return getTransformedMetadata(accountPageResponse.account_page)
case PageTypeEnum.collectionPage: case PageTypeEnum.collectionPage:
const collectionPageResponse = await fetchMetaData<{ const collectionPageResponse = await fetchMetadata<{
collection_page: RawMetaDataSchema collection_page: RawMetadataSchema
}>(GetCollectionPageMetaData, variables) }>(GetCollectionPageMetadata, variables)
return getTransformedMetaData(collectionPageResponse.collection_page) return getTransformedMetadata(collectionPageResponse.collection_page)
case PageTypeEnum.contentPage: case PageTypeEnum.contentPage:
const contentPageResponse = await fetchMetaData<{ const contentPageResponse = await fetchMetadata<{
content_page: RawMetaDataSchema content_page: RawMetadataSchema
}>(GetContentPageMetaData, variables) }>(GetContentPageMetadata, variables)
return getTransformedMetaData(contentPageResponse.content_page) return getTransformedMetadata(contentPageResponse.content_page)
case PageTypeEnum.loyaltyPage: case PageTypeEnum.loyaltyPage:
const loyaltyPageResponse = await fetchMetaData<{ const loyaltyPageResponse = await fetchMetadata<{
loyalty_page: RawMetaDataSchema loyalty_page: RawMetadataSchema
}>(GetLoyaltyPageMetaData, variables) }>(GetLoyaltyPageMetadata, variables)
return getTransformedMetaData(loyaltyPageResponse.loyalty_page) return getTransformedMetadata(loyaltyPageResponse.loyalty_page)
default: default:
return null return null
} }

View File

@@ -1,12 +1,12 @@
import { RTETypeEnum } from "@/types/rte/enums" import { RTETypeEnum } from "@/types/rte/enums"
import { RawMetaDataSchema } from "@/types/trpc/routers/contentstack/metadata" import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
export const affix = "metadata" export const affix = "metadata"
export function getTitle(data: RawMetaDataSchema) { export function getTitle(data: RawMetadataSchema) {
const metaData = data.web.seo_metadata const metadata = data.web.seo_metadata
if (metaData?.title) { if (metadata?.title) {
return metaData.title return metadata.title
} }
if (data.web?.breadcrumbs?.title) { if (data.web?.breadcrumbs?.title) {
return data.web.breadcrumbs.title return data.web.breadcrumbs.title
@@ -20,10 +20,10 @@ export function getTitle(data: RawMetaDataSchema) {
return "" return ""
} }
export function getDescription(data: RawMetaDataSchema) { export function getDescription(data: RawMetadataSchema) {
const metaData = data.web.seo_metadata const metadata = data.web.seo_metadata
if (metaData?.description) { if (metadata?.description) {
return metaData.description return metadata.description
} }
if (data.preamble) { if (data.preamble) {
return data.preamble return data.preamble
@@ -45,12 +45,12 @@ export function getDescription(data: RawMetaDataSchema) {
return "" return ""
} }
export function getImages(data: RawMetaDataSchema) { export function getImages(data: RawMetadataSchema) {
const metaDataImages = data.web.seo_metadata?.imageConnection?.edges const metadataImages = data.web.seo_metadata?.imageConnection?.edges
const heroImage = data.hero_image const heroImage = data.hero_image
if (metaDataImages?.length) { if (metadataImages?.length) {
return metaDataImages.map((edge) => { return metadataImages.map((edge) => {
const { width, height } = edge.node.dimension const { width, height } = edge.node.dimension
return { return {
url: edge.node.url, url: edge.node.url,

View File

@@ -1,6 +1,5 @@
import { z } from "zod" import { z } from "zod"
import { rawMetaDataDataSchema } from "@/server/routers/contentstack/metadata/output" import { rawMetadataSchema } from "@/server/routers/contentstack/metadata/output"
export interface RawMetaDataSchema export interface RawMetadataSchema extends z.output<typeof rawMetadataSchema> {}
extends z.output<typeof rawMetaDataDataSchema> {}

View File

@@ -1,7 +1,7 @@
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
export async function generateMetadata() { export async function generateMetadata() {
const data = await serverClient().contentstack.metaData.get() const data = await serverClient().contentstack.metadata.get()
return data return data
} }

View File

@@ -1,15 +1,10 @@
import { env } from "@/env/server" import { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server"
import type { BreadcrumbList, ListItem, WithContext } from "schema-dts" import type { BreadcrumbList, ListItem, WithContext } from "schema-dts"
import { Breadcrumbs } from "@/types/trpc/routers/contentstack/breadcrumbs" import type { Breadcrumbs } from "@/types/trpc/routers/contentstack/breadcrumbs"
function generateBreadcrumbsSchema(breadcrumbs: Breadcrumbs) {
if (!breadcrumbs.length) {
return null
}
export function generateBreadcrumbsSchema(breadcrumbs: Breadcrumbs) {
const itemListElement: ListItem[] = breadcrumbs.map((item, index) => ({ const itemListElement: ListItem[] = breadcrumbs.map((item, index) => ({
"@type": "ListItem", "@type": "ListItem",
position: index + 1, position: index + 1,
@@ -30,8 +25,3 @@ function generateBreadcrumbsSchema(breadcrumbs: Breadcrumbs) {
jsonLd, jsonLd,
} }
} }
export async function getBreadcrumbsListSchema() {
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get()
return generateBreadcrumbsSchema(breadcrumbs)
}