Merged in feat/revalidate-cache-my-pages-breadcrumbs (pull request #135)

Feat/revalidate cache my pages breadcrumbs

Approved-by: Michael Zetterberg
This commit is contained in:
Simon.Emanuelsson
2024-05-06 13:21:19 +00:00
committed by Michael Zetterberg
24 changed files with 322 additions and 57 deletions

View File

@@ -28,7 +28,7 @@ export default async function CurrentContentPage({
url: searchParams.uri,
},
{
tags: [`${searchParams.uri}-${params.lang}`],
next: { tags: [`${searchParams.uri}-${params.lang}`] },
}
)
@@ -44,7 +44,7 @@ export default async function CurrentContentPage({
GetCurrentBlockPageTrackingData,
{ uid: response.data.all_current_blocks_page.items[0].system.uid },
{
tags: [`${searchParams.uri}-en`],
next: { tags: [`${searchParams.uri}-en`] },
}
)

View File

@@ -6,6 +6,7 @@ import { z } from "zod"
import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import { internalServerError } from "@/server/errors/next"
import { affix as breadcrumbsAffix } from "@/server/routers/contentstack/breadcrumbs/utils"
import {
generateRefsResponseTag,
@@ -14,14 +15,16 @@ import {
} from "@/utils/generateTag"
const validateJsonBody = z.object({
api_key: z.string(),
module: z.string(),
data: z.object({
content_type: z.object({
uid: z.string(),
}),
entry: z.object({
breadcrumbs: z
.object({
title: z.string(),
})
.optional(),
locale: z.nativeEnum(Lang),
uid: z.string(),
url: z.string().optional(),
@@ -75,6 +78,25 @@ export async function POST(request: NextRequest) {
console.info(`Revalidating tag: ${tag}`)
revalidateTag(tag)
if (entry.breadcrumbs) {
const breadcrumbsRefsTag = generateRefsResponseTag(
entry.locale,
identifier,
breadcrumbsAffix
)
const breadcrumbsTag = generateTag(
entry.locale,
entry.uid,
breadcrumbsAffix
)
console.info(`Revalidating breadcrumbsRefsTag: ${breadcrumbsRefsTag}`)
revalidateTag(breadcrumbsRefsTag)
console.info(`Revalidating breadcrumbsTag: ${breadcrumbsTag}`)
revalidateTag(breadcrumbsTag)
}
return Response.json({ revalidated: true, now: Date.now() })
} catch (error) {
console.info("Failed to revalidate tag(s)")

View File

@@ -17,7 +17,7 @@ export default async function Footer({ lang }: LangParams) {
locale: lang,
},
{
tags: [`footer-${lang}`],
next: { tags: [`footer-${lang}`] },
}
)

View File

@@ -30,7 +30,7 @@ export default async function Header({ lang, uid }: LangParams & HeaderProps) {
const { data } = await request<HeaderQueryData>(
GetHeader,
{ locale: lang },
{ tags: [`header-${lang}`] }
{ next: { tags: [`header-${lang}`] } }
)
const { data: urls } = await batchRequest<LanguageSwitcherQueryData>([
{

View File

@@ -1,5 +1,9 @@
"use client"
import { cva } from "class-variance-authority"
import { Lang } from "@/constants/languages"
import { trpc } from "@/lib/trpc/client"
import styles from "./maxWidth.module.css"
import type { MaxWidthProps } from "@/types/components/max-width"
@@ -10,8 +14,11 @@ export default function MaxWidth({
className,
tag = "section",
...props
}: MaxWidthProps
) {
}: MaxWidthProps) {
const d = trpc.contentstack.breadcrumbs.get.useQuery({
locale: Lang.en,
href: "/my-pages/overview",
})
const Cmp = tag
return <Cmp className={maxWidthVariants({ className })} {...props} />
}

View File

@@ -11,7 +11,6 @@ export default function Breadcrumbs({ breadcrumbs }: BreadcrumbsProps) {
return (
<nav className={styles.breadcrumbs}>
<ul className={styles.list}>
<BreadcrumbsWithLink href="#">{_("Home")}</BreadcrumbsWithLink>
{breadcrumbs.map((breadcrumb) => {
if (breadcrumb.href) {
return (

2
env/client.ts vendored
View File

@@ -3,10 +3,12 @@ import { z } from "zod"
export const env = createEnv({
client: {
NEXT_PUBLIC_NODE_ENV: z.enum(["development", "test", "production"]),
NEXT_PUBLIC_PORT: z.string().default("3000"),
},
emptyStringAsUndefined: true,
runtimeEnv: {
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT,
},
})

View File

@@ -1,7 +1,7 @@
fragment MyPagesBreadcrumbs on AccountPage {
breadcrumbs {
title
parents: parentsConnection {
parentsConnection {
edges {
node {
... on AccountPage {
@@ -9,6 +9,7 @@ fragment MyPagesBreadcrumbs on AccountPage {
title
}
system {
locale
uid
}
url

View File

@@ -0,0 +1,20 @@
#import "./System.graphql"
fragment MyPagesBreadcrumbsRefs on AccountPage {
breadcrumbs {
parentsConnection {
edges {
node {
... on AccountPage {
system {
...System
}
}
}
}
}
}
system {
...System
}
}

View File

@@ -1,4 +1,5 @@
#import "../Fragments/MyPages/Breadcrumbs.graphql"
#import "../Fragments/Refs/Breadcrumbs.graphql"
query GetMyPagesBreadcrumbs($locale: String!, $url: String!) {
all_account_page(locale: $locale, where: { url: $url }) {
@@ -10,3 +11,11 @@ query GetMyPagesBreadcrumbs($locale: String!, $url: String!) {
}
}
}
query GetMyPagesBreadcrumbsRefs($locale: String!, $url: String!) {
all_account_page(locale: $locale, where: { url: $url }) {
items {
...MyPagesBreadcrumbsRefs
}
}
}

View File

@@ -12,7 +12,9 @@ export async function batchRequest<T>(
try {
const response = await Promise.allSettled(
queries.map((query) =>
request<T>(query.document, query.variables, { tags: query.tags })
request<T>(query.document, query.variables, {
next: { tags: query.tags },
})
)
)

View File

@@ -10,6 +10,7 @@ import type { DocumentNode } from "graphql"
import type { Data } from "@/types/request"
const client = new GraphQLClient(env.CMS_URL, {
cache: "force-cache",
fetch: cache(async function (
url: URL | RequestInfo,
params: RequestInit | undefined
@@ -21,11 +22,14 @@ const client = new GraphQLClient(env.CMS_URL, {
export async function request<T>(
query: string | DocumentNode,
variables?: {},
next?: NextFetchRequestConfig
options?: Pick<RequestInit, "cache" | "next">
): Promise<Data<T>> {
try {
if (next) {
client.requestConfig.next = next
if (options?.cache) {
client.requestConfig.cache = options.cache
}
if (options?.next) {
client.requestConfig.next = options.next
}
if (env.PRINT_QUERY) {

View File

@@ -1,7 +1,7 @@
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { httpBatchLink } from "@trpc/client"
import { httpBatchLink, loggerLink } from "@trpc/client"
import { useState } from "react"
import { env } from "@/env/client"
@@ -14,7 +14,14 @@ function initializeTrpcClient() {
// that trpc and next are running on the same port.
return trpc.createClient({
links: [
loggerLink({
enabled: (opts) =>
(env.NEXT_PUBLIC_NODE_ENV === "development" &&
typeof window !== "undefined") ||
(opts.direction === "down" && opts.result instanceof Error),
}),
httpBatchLink({
fetch,
transformer,
/**
* This is locally in Next.js

View File

@@ -7,6 +7,7 @@ import {
overview,
profile,
profileEdit,
stays,
} from "./constants/routes/myPages.js"
const jiti = createJiti(new URL(import.meta.url).pathname)
@@ -124,6 +125,12 @@ const nextConfig = {
{ source: profileEdit.fi, destination: "/fi/my-pages/profile/edit" },
{ source: profileEdit.no, destination: "/no/my-pages/profile/edit" },
{ source: profileEdit.sv, destination: "/sv/my-pages/profile/edit" },
{ source: stays.da, destination: "/da/my-pages/stays" },
{ source: stays.de, destination: "/de/my-pages/stays" },
{ source: stays.fi, destination: "/fi/my-pages/stays" },
{ source: stays.no, destination: "/no/my-pages/stays" },
{ source: stays.sv, destination: "/sv/my-pages/stays" },
],
}
},

View File

@@ -1,7 +1,7 @@
import { TRPCError } from "@trpc/server"
import {
TRPC_ERROR_CODES_BY_NUMBER,
TRPC_ERROR_CODES_BY_KEY,
TRPC_ERROR_CODES_BY_NUMBER,
} from "@trpc/server/rpc"
export function unauthorizedError() {

View File

@@ -1,10 +1,8 @@
import { z } from "zod";
import { z } from "zod"
import { Lang } from "@/constants/languages";
const langs = Object.keys(Lang) as [keyof typeof Lang]
import { Lang } from "@/constants/languages"
export const getBreadcrumbsInput = z.object({
href: z.string().min(1, { message: "href is required" }),
locale: z.enum(langs),
locale: z.nativeEnum(Lang),
})

View File

@@ -1,12 +1,40 @@
import { Lang } from "@/constants/languages"
import { z } from "zod"
export const validateBreadcrumbsRefsConstenstackSchema = z.object({
all_account_page: z.object({
items: z.array(
z.object({
breadcrumbs: z.object({
parentsConnection: z.object({
edges: z.array(
z.object({
node: z.object({
system: z.object({
content_type_uid: z.string(),
uid: z.string(),
}),
}),
})
),
}),
}),
system: z.object({
content_type_uid: z.string(),
uid: z.string(),
}),
})
),
}),
})
export const validateBreadcrumbsConstenstackSchema = z.object({
all_account_page: z.object({
items: z.array(
z.object({
breadcrumbs: z.object({
title: z.string(),
parents: z.object({
parentsConnection: z.object({
edges: z.array(
z.object({
node: z.object({
@@ -14,6 +42,7 @@ export const validateBreadcrumbsConstenstackSchema = z.object({
title: z.string(),
}),
system: z.object({
locale: z.nativeEnum(Lang),
uid: z.string(),
}),
url: z.string(),

View File

@@ -1,51 +1,120 @@
import { GetMyPagesBreadcrumbs } from "@/lib/graphql/Query/BreadcrumbsMyPages.graphql"
import {
GetMyPagesBreadcrumbs,
GetMyPagesBreadcrumbsRefs,
} from "@/lib/graphql/Query/BreadcrumbsMyPages.graphql"
import { request } from "@/lib/graphql/request"
import { badRequestError, internalServerError } from "@/server/errors/trpc"
import { publicProcedure, router } from "@/server/trpc"
import { getBreadcrumbsInput } from "./input"
import { validateBreadcrumbsConstenstackSchema } from "./output"
import {
generateRefsResponseTag,
generateTag,
generateTags,
} from "@/utils/generateTag"
import { removeMultipleSlashes } from "@/utils/url"
import { GetMyPagesBreadcrumbsData } from "@/types/requests/myPages/breadcrumbs"
import { getBreadcrumbsInput } from "./input"
import {
getBreadcrumbsSchema,
validateBreadcrumbsConstenstackSchema,
validateBreadcrumbsRefsConstenstackSchema,
} from "./output"
import { affix, getConnections, homeBreadcrumbs } from "./utils"
import type {
GetMyPagesBreadcrumbsData,
GetMyPagesBreadcrumbsRefsData,
} from "@/types/requests/myPages/breadcrumbs"
export const breadcrumbsQueryRouter = router({
get: publicProcedure.input(getBreadcrumbsInput).query(async ({ input }) => {
try {
const response = await request<GetMyPagesBreadcrumbsData>(
GetMyPagesBreadcrumbs,
{ locale: input.locale, url: input.href }
const refsResponse = await request<GetMyPagesBreadcrumbsRefsData>(
GetMyPagesBreadcrumbsRefs,
{ locale: input.locale, url: input.href },
{
next: {
tags: [generateRefsResponseTag(input.locale, input.href, affix)],
},
}
)
if (!response.data) {
if (!refsResponse.data) {
console.error("Bad response for `GetMyPagesBreadcrumbsRefs`")
console.error({ refsResponse })
throw internalServerError()
}
const validatedRefsData =
validateBreadcrumbsRefsConstenstackSchema.safeParse(refsResponse.data)
if (!validatedRefsData.success) {
console.info("Bad validation for `GetMyPagesBreadcrumbsRefs`")
console.error(validatedRefsData.error)
throw badRequestError()
}
const validatedBreadcrumbs =
const connections = getConnections(validatedRefsData.data)
const tags = generateTags(input.locale, connections)
const page = validatedRefsData.data.all_account_page.items[0]
tags.push(generateTag(input.locale, page.system.uid, affix))
const response = await request<GetMyPagesBreadcrumbsData>(
GetMyPagesBreadcrumbs,
{ locale: input.locale, url: input.href },
{ next: { tags } }
)
if (!response.data) {
console.error("Bad response for `GetMyPagesBreadcrumbs`")
console.error({ input })
console.error({ response })
throw internalServerError()
}
const validatedBreadcrumbsData =
validateBreadcrumbsConstenstackSchema.safeParse(response.data)
if (!validatedBreadcrumbs.success) {
if (!validatedBreadcrumbsData.success) {
console.error("Bad validation for `GetMyPagesBreadcrumbs`")
console.error(validatedBreadcrumbsData.error)
throw badRequestError()
}
const parentBreadcrumbs =
validatedBreadcrumbs.data.all_account_page.items[0].breadcrumbs.parents.edges.map(
validatedBreadcrumbsData.data.all_account_page.items[0].breadcrumbs.parentsConnection.edges.map(
(breadcrumb) => {
return {
href: breadcrumb.node.url,
href: removeMultipleSlashes(
`/${breadcrumb.node.system.locale}/${breadcrumb.node.url}`
),
title: breadcrumb.node.breadcrumbs.title,
uid: breadcrumb.node.system.uid,
}
}
)
const pageBreadcrumb =
validatedBreadcrumbs.data.all_account_page.items.map((breadcrumb) => {
return {
href: "",
title: breadcrumb.breadcrumbs.title,
uid: breadcrumb.system.uid,
}
})
const breadcrumbs = [parentBreadcrumbs, pageBreadcrumb].flat()
return breadcrumbs
const pageBreadcrumb =
validatedBreadcrumbsData.data.all_account_page.items.map(
(breadcrumb) => {
return {
title: breadcrumb.breadcrumbs.title,
uid: breadcrumb.system.uid,
}
}
)
const breadcrumbs = [
homeBreadcrumbs[input.locale],
parentBreadcrumbs,
pageBreadcrumb,
].flat()
const validatedBreadcrumbs = getBreadcrumbsSchema.safeParse(breadcrumbs)
if (!validatedBreadcrumbs.success) {
console.info("Bad validation for `validatedBreadcrumbs`")
console.error(validatedBreadcrumbs.error)
throw badRequestError()
}
return validatedBreadcrumbs.data
} catch (error) {
console.info(`Get My Pages Breadcrumbs Error`)
console.error(error)

View File

@@ -0,0 +1,50 @@
import { Lang } from "@/constants/languages"
import type { GetMyPagesBreadcrumbsRefsData } from "@/types/requests/myPages/breadcrumbs"
import type { Edges } from "@/types/requests/utils/edges"
import type { NodeRefs } from "@/types/requests/utils/refs"
export function getConnections(refs: GetMyPagesBreadcrumbsRefsData) {
const connections: Edges<NodeRefs>[] = []
refs.all_account_page.items.forEach((ref) => {
connections.push(ref.breadcrumbs.parentsConnection)
})
return connections
}
export const affix = "breadcrumbs"
// TODO: Make these editable in CMS?
export const homeBreadcrumbs = {
[Lang.da]: {
href: "/da",
title: "Hjem",
uid: "da",
},
[Lang.de]: {
href: "/de",
title: "Heim",
uid: "de",
},
[Lang.en]: {
href: "/en",
title: "Home",
uid: "en",
},
[Lang.fi]: {
href: "/fi",
title: "Koti",
uid: "fi",
},
[Lang.no]: {
href: "/no",
title: "Hjem",
uid: "no",
},
[Lang.sv]: {
href: "/sv",
title: "Hem",
uid: "sv",
},
}

View File

@@ -11,6 +11,7 @@ import {
generateTag,
generateTags,
} from "@/utils/generateTag"
import { removeMultipleSlashes } from "@/utils/url"
import { getNavigationInputSchema } from "./input"
import {
@@ -35,7 +36,7 @@ export function mapMenuItems(navigationItems: NavigationItem[]) {
lang: node.system.locale,
linkText: item.link_text || node.title,
uid: node.system.uid,
url: `/${node.system.locale}/${node.url}`.replaceAll(/\/\/+/g, "/"),
url: removeMultipleSlashes(`/${node.system.locale}/${node.url}`),
}
if ("sub_items" in item) {
@@ -54,12 +55,16 @@ export const navigationQueryRouter = router({
const refsResponse = await request<GetNavigationMyPagesRefsData>(
GetNavigationMyPagesRefs,
{ locale: lang },
{ tags: [generateRefsResponseTag(lang, "navigation_my_pages")] }
{
next: {
tags: [generateRefsResponseTag(lang, "navigation_my_pages")],
},
}
)
if (!refsResponse.data) {
console.error("Bad response for `GetNavigationMyPagesRefs`")
console.error(refsResponse)
console.error({ refsResponse })
throw internalServerError()
}
@@ -80,12 +85,13 @@ export const navigationQueryRouter = router({
const response = await request<GetNavigationMyPagesData>(
GetNavigationMyPages,
{ locale: lang },
{ tags }
{ next: { tags } }
)
if (!response.data) {
console.error("Bad response for `GetNavigationMyPages`")
console.error(response)
console.error({ input: lang })
console.error({ response })
throw internalServerError()
}

View File

@@ -31,7 +31,6 @@ export const userQueryRouter = router({
get: protectedProcedure.query(async function ({ ctx }) {
try {
const apiResponse = await api.get(api.endpoints.v0.profile, {
cache: "no-store",
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},

View File

@@ -1,14 +1,17 @@
import { Lang } from "@/constants/languages"
import type { AllRequestResponse } from "../utils/all"
import type { EdgesWithTotalCount } from "../utils/edges"
import type { Edges } from "../utils/edges"
import type { System } from "../system"
interface AccountPageBreadcrumbs {
breadcrumbs: {
title: string
parents: EdgesWithTotalCount<{
parentsConnection: Edges<{
breadcrumbs: {
title: string
}
system: {
locale: Lang
uid: string
}
url: string
@@ -25,3 +28,16 @@ interface AllAccountPageResponse
export interface GetMyPagesBreadcrumbsData {
all_account_page: AllAccountPageResponse
}
interface AccountPageBreadcrumbRefs extends System {
breadcrumbs: {
parentsConnection: Edges<System>
}
}
interface AllAccountPageRefsResponse
extends AllRequestResponse<AccountPageBreadcrumbRefs> { }
export interface GetMyPagesBreadcrumbsRefsData {
all_account_page: AllAccountPageRefsResponse
}

View File

@@ -1,16 +1,25 @@
import type { Edges } from "@/types/requests/utils/edges"
import type { Lang } from "@/constants/languages"
import type { NodeRefs } from "@/types/requests/utils/refs"
import type { Lang } from "@/constants/languages"
/**
* Function to generate tag for initial refs request
*
* @param lang
* @param lang Lang
* @param identifier Should be uri for all pages and content_type_uid for
* everything else
* @param affix possible extra value to add to string, e.g lang:identifier:breadcrumbs:refs
* as it is the same entity as the actual page tag otherwise
* @returns string
*/
export function generateRefsResponseTag(lang: Lang, identifier: string) {
export function generateRefsResponseTag(
lang: Lang,
identifier: string,
affix?: string
) {
if (affix) {
return `${lang}:${identifier}:${affix}:refs`
}
return `${lang}:${identifier}:refs`
}
@@ -35,9 +44,15 @@ export function generateRefTag(
*
* @param lang Lang
* @param uid system.uid of entity
* @param affix possible extra value to add to string, e.g lang:uid:breadcrumbs
* as it is the same entity as the actual page tag otherwise
* @returns string
*/
export function generateTag(lang: Lang, uid: string) {
export function generateTag(lang: Lang, uid: string, affix?: string) {
if (affix) {
return `${lang}:${uid}:${affix}`
}
return `${lang}:${uid}`
}

3
utils/url.ts Normal file
View File

@@ -0,0 +1,3 @@
export function removeMultipleSlashes(str: string) {
return str.replaceAll(/\/\/+/g, "/")
}