From 08ae0344b3654debba59c17f7204c95ccfbc2599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADcholas=20Oliveira?= Date: Mon, 8 Jul 2024 21:31:21 -0300 Subject: [PATCH] feat: revalidateRouteHandler --- .changeset/moody-emus-matter.md | 6 ++ .../strategies/VerifyTokenFetchStrategy.ts | 23 ++++- .../src/rsc/handlers/previewRouteHandler.ts | 34 +++---- .../rsc/handlers/revalidateRouterHandler.ts | 92 +++++++++++++++++++ packages/next/src/rsc/handlers/utils.ts | 21 +++++ packages/next/src/rsc/index.ts | 1 + .../src/app/(single)/[...path]/page.tsx | 2 + .../src/app/api/revalidate/route.ts | 7 ++ 8 files changed, 161 insertions(+), 25 deletions(-) create mode 100644 .changeset/moody-emus-matter.md create mode 100644 packages/next/src/rsc/handlers/revalidateRouterHandler.ts create mode 100644 packages/next/src/rsc/handlers/utils.ts create mode 100644 projects/wp-nextjs-app/src/app/api/revalidate/route.ts diff --git a/.changeset/moody-emus-matter.md b/.changeset/moody-emus-matter.md new file mode 100644 index 000000000..dd41a9f3f --- /dev/null +++ b/.changeset/moody-emus-matter.md @@ -0,0 +1,6 @@ +--- +"@headstartwp/core": minor +"@headstartwp/next": minor +--- + +Introducing `revalidateRouteHandler` for handling revalidate requests in Route Handlers (App Router) diff --git a/packages/core/src/data/strategies/VerifyTokenFetchStrategy.ts b/packages/core/src/data/strategies/VerifyTokenFetchStrategy.ts index 1624fbf03..a2bb95b38 100644 --- a/packages/core/src/data/strategies/VerifyTokenFetchStrategy.ts +++ b/packages/core/src/data/strategies/VerifyTokenFetchStrategy.ts @@ -1,4 +1,4 @@ -import { AppEntity } from '../types'; +import { Entity } from '../types'; import { AbstractFetchStrategy, EndpointParams, FetchOptions } from './AbstractFetchStrategy'; import { endpoints } from '../utils'; @@ -9,6 +9,21 @@ export interface VerifyTokenParams extends EndpointParams { authToken?: string; } +/** + * The TokenEntity represents a token issued by the headless wp plugin + */ +export interface TokenEntity extends Entity { + /** + * The path that the token was issued for + */ + path: string; + + /** + * The post_id that the token was issued for + */ + post_id: number; +} + /** * The Verify Token strategy is used to verify tokens issued by the * headless wp plugin @@ -16,9 +31,9 @@ export interface VerifyTokenParams extends EndpointParams { * @category Data Fetching */ export class VerifyTokenFetchStrategy extends AbstractFetchStrategy< - AppEntity, - EndpointParams, - AppEntity + TokenEntity, + VerifyTokenParams, + TokenEntity > { getDefaultEndpoint(): string { return endpoints.tokenVerify; diff --git a/packages/next/src/rsc/handlers/previewRouteHandler.ts b/packages/next/src/rsc/handlers/previewRouteHandler.ts index 2a89e75b6..f16d8159e 100644 --- a/packages/next/src/rsc/handlers/previewRouteHandler.ts +++ b/packages/next/src/rsc/handlers/previewRouteHandler.ts @@ -3,14 +3,13 @@ import { PostEntity, fetchPost, getCustomPostType, - getHeadstartWPConfig, - getSiteByHost, removeSourceUrl, } from '@headstartwp/core'; import type { NextRequest } from 'next/server'; import { cookies, draftMode } from 'next/headers'; import { redirect } from 'next/navigation'; import type { PreviewData } from '../../handlers'; +import { getHostAndConfigFromRequest } from './utils'; export const COOKIE_NAME = 'headstartwp_preview'; @@ -57,7 +56,6 @@ export type PreviewRouteHandlerOptions = { * import type { NextRequest } from 'next/server'; * * export async function GET(request: NextRequest) { - * // @ts-expect-error * return previewRouteHandler( * request, { * onRedirect(options) { @@ -93,7 +91,6 @@ export type PreviewRouteHandlerOptions = { * import type { NextRequest } from 'next/server'; * * export async function GET(request: NextRequest) { - * // @ts-expect-error * return previewRouteHandler(request); * } * ``` @@ -121,12 +118,7 @@ export async function previewRouteHandler( return new Response('Missing required params', { status: 401 }); } - // get the host header - const host = request.headers.get('host') ?? ''; - const site = getSiteByHost(host, typeof locale === 'string' ? locale : undefined); - const isMultisiteRequest = site !== null && typeof site.sourceUrl === 'string'; - - const config = isMultisiteRequest ? site : getHeadstartWPConfig(); + const { config } = getHostAndConfigFromRequest(request); const { sourceUrl, preview } = config; const revision = is_revision === '1'; @@ -140,7 +132,9 @@ export async function previewRouteHandler( ); } - const { data } = await fetchPost( + const { + data: { post }, + } = await fetchPost( { params: { id: Number(post_id), @@ -159,10 +153,8 @@ export async function previewRouteHandler( const id = Number(post_id); - const result = data.post; - - if (result?.id === id || result?.parent === id) { - const { slug } = result; + if (post?.id === id || post?.parent === id) { + const { slug } = post; let previewData: PreviewData = { id, @@ -175,7 +167,7 @@ export async function previewRouteHandler( typeof options.preparePreviewData === 'function' ? options.preparePreviewData({ req: request, - post: result, + post, previewData, postTypeDef, }) @@ -189,14 +181,14 @@ export async function previewRouteHandler( const getDefaultRedirectPath = () => { if (preview?.usePostLinkForRedirect) { if ( - result.status === 'draft' && - typeof result._headless_wp_preview_link === 'undefined' + post.status === 'draft' && + typeof post._headless_wp_preview_link === 'undefined' ) { throw new Error( 'You are using usePostLinkForRedirect setting but your rest response does not have _headless_wp_preview_link, ensure you are running the latest version of the plugin', ); } - const link = result._headless_wp_preview_link ?? result.link; + const link = post._headless_wp_preview_link ?? post.link; return removeSourceUrl({ link: link as string, backendUrl: sourceUrl ?? '' }); } @@ -214,7 +206,7 @@ export async function previewRouteHandler( ? options.getRedirectPath({ req: request, defaultRedirectPath, - post: result, + post, postTypeDef, previewData, }) @@ -234,7 +226,7 @@ export async function previewRouteHandler( req: request, previewData, postTypeDef, - post: result, + post, redirectPath, }); } else { diff --git a/packages/next/src/rsc/handlers/revalidateRouterHandler.ts b/packages/next/src/rsc/handlers/revalidateRouterHandler.ts new file mode 100644 index 000000000..d33ae0b58 --- /dev/null +++ b/packages/next/src/rsc/handlers/revalidateRouterHandler.ts @@ -0,0 +1,92 @@ +import { VerifyTokenFetchStrategy } from '@headstartwp/core'; +import { revalidatePath } from 'next/cache'; +import { NextRequest } from 'next/server'; +import { getHostAndConfigFromRequest } from './utils'; + +/** + * The revalidateRouteHandler is responsible for handling revalidate requests in Route Requests. + * + * Handling revalidate requires the Headless WordPress Plugin. + * + * **Important**: This function is meant to be used in a route handler route e.g: `/app/api/revalidate/route.ts`. + * + * #### Usage + * + * ```ts + * // pages/api/revalidate.js + * import { revalidateHandler } from '@headstartwp/next'; + * + * export default async function handler(req, res) { + * return revalidateHandler(req, res); + * } + * ``` + * + * @param request The Next Request + * + * @returns A response object. + * + * @category Route handlers + */ +export async function revalidateRouteHandler(request: NextRequest) { + const { searchParams } = request.nextUrl; + const post_id = Number(searchParams.get('post_id') ?? 0); + const path = searchParams.get('path'); + + const token = searchParams.get('token'); + const locale = searchParams.get('locale'); + + if (!path || !post_id || !token) { + return new Response('Missing required params', { status: 401 }); + } + + if (typeof path !== 'string' || typeof token !== 'string') { + return new Response('Invalid params', { status: 401 }); + } + + const { + host, + config: { sourceUrl }, + isMultisiteRequest, + } = getHostAndConfigFromRequest(request); + + // call WordPress API to check token + try { + const verifyTokenStrategy = new VerifyTokenFetchStrategy(sourceUrl); + const { + result: { path, post_id }, + } = await verifyTokenStrategy.get({ + authToken: token, + // TODO: check if this is correct (it's a separate github issue) + lang: typeof locale === 'string' ? locale : undefined, + }); + + const verifiedPath = path ?? ''; + const verifiedPostId = post_id ?? 0; + + // make sure the path and post_id matches with what was encoded in the token + if (verifiedPath !== path || Number(verifiedPostId) !== Number(post_id)) { + throw new Error('Token mismatch'); + } + + let pathToRevalidate = path; + + if (isMultisiteRequest) { + if (locale) { + pathToRevalidate = `/_sites/${host}/${locale}/${path}`; + } + pathToRevalidate = `/_sites/${host}${path}`; + } + + revalidatePath(pathToRevalidate); + + return new Response(JSON.stringify({ message: 'success', path: pathToRevalidate }), { + status: 200, + }); + } catch (err) { + let errorMessage = 'Error verifying the token'; + if (err instanceof Error) { + errorMessage = err.message; + } + return new Response(errorMessage, { status: 500 }); + } +} diff --git a/packages/next/src/rsc/handlers/utils.ts b/packages/next/src/rsc/handlers/utils.ts new file mode 100644 index 000000000..47423224f --- /dev/null +++ b/packages/next/src/rsc/handlers/utils.ts @@ -0,0 +1,21 @@ +import { getHeadstartWPConfig, getSiteByHost } from '@headstartwp/core'; +import { NextRequest } from 'next/server'; + +/** + * Gets the host and config from Next Request + * + * @param request The Next Request + * @returns + */ +export function getHostAndConfigFromRequest(request: NextRequest) { + const { searchParams } = request.nextUrl; + + const locale = searchParams.get('locale'); + + const host = request.headers.get('host') ?? ''; + const site = getSiteByHost(host, typeof locale === 'string' ? locale : undefined); + const isMultisiteRequest = site !== null && typeof site.sourceUrl === 'string'; + const config = isMultisiteRequest ? site : getHeadstartWPConfig(); + + return { host, config, isMultisiteRequest }; +} diff --git a/packages/next/src/rsc/index.ts b/packages/next/src/rsc/index.ts index dfac03fb7..a09b2fda1 100644 --- a/packages/next/src/rsc/index.ts +++ b/packages/next/src/rsc/index.ts @@ -4,6 +4,7 @@ export * from './config'; // handlers export * from './handlers/previewRouteHandler'; +export * from './handlers/revalidateRouterHandler'; // components export * from './components/PreviewIndicator'; diff --git a/projects/wp-nextjs-app/src/app/(single)/[...path]/page.tsx b/projects/wp-nextjs-app/src/app/(single)/[...path]/page.tsx index 64effd589..12addb8d8 100644 --- a/projects/wp-nextjs-app/src/app/(single)/[...path]/page.tsx +++ b/projects/wp-nextjs-app/src/app/(single)/[...path]/page.tsx @@ -1,6 +1,8 @@ import { BlocksRenderer, HtmlDecoder } from '@headstartwp/core/react'; import { HeadstartWPRoute, queryPost } from '@headstartwp/next/app'; +export const dynamic = 'force-static'; + const Single = async ({ params }: HeadstartWPRoute) => { const { data } = await queryPost({ routeParams: params, diff --git a/projects/wp-nextjs-app/src/app/api/revalidate/route.ts b/projects/wp-nextjs-app/src/app/api/revalidate/route.ts new file mode 100644 index 000000000..dc1dc3548 --- /dev/null +++ b/projects/wp-nextjs-app/src/app/api/revalidate/route.ts @@ -0,0 +1,7 @@ +import { revalidateRouteHandler } from '@headstartwp/next/app'; +import type { NextRequest } from 'next/server'; + +export async function GET(request: NextRequest) { + // @ts-expect-error + return revalidateRouteHandler(request); +}