diff --git a/apps/cms/src/hooks/revalidate-globals.ts b/apps/cms/src/hooks/revalidate-globals.ts new file mode 100644 index 00000000..8d37ae94 --- /dev/null +++ b/apps/cms/src/hooks/revalidate-globals.ts @@ -0,0 +1,52 @@ +// revalidate the page in the background, so the user doesn't have to wait +// notice that the hook itself is not async and we are not awaiting `revalidate` + +import type { AfterChangeHook } from "payload/dist/globals/config/types"; + +// only revalidate existing docs that are published (not drafts) +export const revalidateGlobal: AfterChangeHook = ({ doc, req, global }) => { + const locale = req.locale; + if (!locale) { + req.payload.logger.error("locale not set, cannot revalidate"); + return; + } + const revalidate = async (): Promise => { + const revalidationKey = process.env.PAYLOAD_REVALIDATION_KEY; + if (!revalidationKey) { + req.payload.logger.error( + "PAYLOAD_REVALIDATION_KEY not set, cannot revalidate", + ); + return; + } + try { + const fetchUrl = `${ + process.env.PUBLIC_FRONTEND_URL + }/next_api/revalidate-global?${new URLSearchParams({ + secret: encodeURIComponent(revalidationKey), + global: encodeURIComponent(global.slug), + locale: encodeURIComponent(locale), + }).toString()}`; + req.payload.logger.info( + `sending revalidate request ${fetchUrl.replace(revalidationKey, "REDACTED")}`, + ); + const res = await fetch(fetchUrl); + if (res.ok) { + const thing = await res.json(); + req.payload.logger.info(`revalidate response ${JSON.stringify(thing)}`); + req.payload.logger.info(`Revalidated global ${global.slug}`); + } else { + req.payload.logger.error( + `Error revalidating collection ${global.slug}`, + ); + } + } catch (err: unknown) { + req.payload.logger.error( + `Error hitting revalidate collection ${global.slug}`, + ); + } + }; + + void revalidate(); + + return doc; +}; diff --git a/apps/cms/src/payload.config.ts b/apps/cms/src/payload.config.ts index 459e2d74..69827f4b 100644 --- a/apps/cms/src/payload.config.ts +++ b/apps/cms/src/payload.config.ts @@ -35,6 +35,7 @@ import { Footer } from "./globals/footer"; import { LandingPage } from "./globals/landing-page"; import { MainNavigation } from "./globals/main-navigation"; import { useCloudStorage } from "./util"; +import { revalidateGlobal } from "./hooks/revalidate-globals"; declare module "payload" { // eslint-disable-next-line @typescript-eslint/no-empty-interface -- not applicable @@ -203,5 +204,21 @@ export default buildConfig({ }, }, }), + // add revalidateGlobal hook to all globals + (config) => { + return { + ...config, + globals: config.globals?.map((global) => ({ + ...global, + hooks: { + ...global.hooks, + afterChange: [ + ...(global.hooks?.afterChange ?? []), + revalidateGlobal, + ], + }, + })), + }; + }, ], }); diff --git a/apps/web/src/app/[lang]/page.tsx b/apps/web/src/app/[lang]/page.tsx index dedf8a93..b416e8de 100644 --- a/apps/web/src/app/[lang]/page.tsx +++ b/apps/web/src/app/[lang]/page.tsx @@ -22,7 +22,7 @@ export default async function Home({ }) { const dictionary = await getDictionary(lang); - const landingPageData = await fetchLandingPage({}); + const landingPageData = await fetchLandingPage(lang)({}); if (!landingPageData) { // TODO: Real error handling / show error page return "Unable to fetch landing page data, please refresh"; diff --git a/apps/web/src/app/next_api/revalidate-global/route.ts b/apps/web/src/app/next_api/revalidate-global/route.ts new file mode 100644 index 00000000..eb22e07d --- /dev/null +++ b/apps/web/src/app/next_api/revalidate-global/route.ts @@ -0,0 +1,38 @@ +import { revalidateTag } from "next/cache"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +// this endpoint will revalidate a page by tag or path +// this is to achieve on-demand revalidation of pages that use this data +// send either `collection` and `slug` or `revalidatePath` as query params +export function GET(request: NextRequest): NextResponse { + const global = decodeURIComponent( + request.nextUrl.searchParams.get("global") ?? "", + ); + const locale = decodeURIComponent( + request.nextUrl.searchParams.get("locale") ?? "", + ); + const secret = decodeURIComponent( + request.nextUrl.searchParams.get("secret") ?? "", + ); + + if (secret !== process.env.NEXT_REVALIDATION_KEY) { + // eslint-disable-next-line no-console -- for debugging purposes + console.log("invalid secret from revalidate request: ", secret); + return NextResponse.json({ revalidated: false, now: Date.now() }); + } + + if (typeof global === "string" && typeof locale === "string") { + const tagToRevalidate = `getGlobal_/api/globals/${global}?locale=${locale}`; + // eslint-disable-next-line no-console -- for debugging purposes + console.log("revalidating tag: ", tagToRevalidate); + revalidateTag(tagToRevalidate); + return NextResponse.json({ revalidated: true, now: Date.now() }); + } + // eslint-disable-next-line no-console -- for debugging purposes + console.log( + "invalid collection or fetchData from revalidate request: ", + global, + ); + return NextResponse.json({ revalidated: false, now: Date.now() }); +} diff --git a/apps/web/src/lib/api/fetcher.ts b/apps/web/src/lib/api/fetcher.ts index a0dcd156..830393ef 100644 --- a/apps/web/src/lib/api/fetcher.ts +++ b/apps/web/src/lib/api/fetcher.ts @@ -74,19 +74,19 @@ export const getAll = < export const getOne = , Response>(path: string) => - (req: Request & { locale?: string }) => + (req: Request & { locale: string }) => getAll(path)(req).then((res) => res?.[0]); -export const getGlobal = (path: string, locale?: string) => +export const getGlobal = (path: string, locale: string) => fetcher, Response>( - () => `getGlobal_${path}`, + () => `getGlobal_${path}?locale=${locale}`, async (_, draft, fetchOptions): Promise => { const result = await fetch( `${process.env.PUBLIC_SERVER_URL}${path}?${qsStringify({ + locale, depth: 10, // TODO: remove this when we have a better way to handle depth for example with GraphQL // Needs to be bigger than 1 to get media / images ...(draft ? { draft: "true" } : {}), - ...(locale ? { locale } : {}), }).toString()}`, { method: "GET", diff --git a/apps/web/src/lib/api/landing-page.ts b/apps/web/src/lib/api/landing-page.ts index 1824d31f..1eb43976 100644 --- a/apps/web/src/lib/api/landing-page.ts +++ b/apps/web/src/lib/api/landing-page.ts @@ -1,6 +1,5 @@ import type { LandingPage } from "@tietokilta/cms-types/payload"; import { getGlobal } from "./fetcher"; -export const fetchLandingPage = getGlobal( - "/api/globals/landing-page", -); +export const fetchLandingPage = (locale: string) => + getGlobal("/api/globals/landing-page", locale);