Skip to content

Commit

Permalink
feat: revalidateRouteHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasio committed Jul 9, 2024
1 parent 98bae20 commit 08ae034
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 25 deletions.
6 changes: 6 additions & 0 deletions .changeset/moody-emus-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@headstartwp/core": minor
"@headstartwp/next": minor
---

Introducing `revalidateRouteHandler` for handling revalidate requests in Route Handlers (App Router)
23 changes: 19 additions & 4 deletions packages/core/src/data/strategies/VerifyTokenFetchStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppEntity } from '../types';
import { Entity } from '../types';
import { AbstractFetchStrategy, EndpointParams, FetchOptions } from './AbstractFetchStrategy';
import { endpoints } from '../utils';

Expand All @@ -9,16 +9,31 @@ 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
*
* @category Data Fetching
*/
export class VerifyTokenFetchStrategy extends AbstractFetchStrategy<
AppEntity,
EndpointParams,
AppEntity
TokenEntity,
VerifyTokenParams,
TokenEntity
> {
getDefaultEndpoint(): string {
return endpoints.tokenVerify;
Expand Down
34 changes: 13 additions & 21 deletions packages/next/src/rsc/handlers/previewRouteHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
* }
* ```
Expand Down Expand Up @@ -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';
Expand All @@ -140,7 +132,9 @@ export async function previewRouteHandler(
);
}

const { data } = await fetchPost(
const {
data: { post },
} = await fetchPost(
{
params: {
id: Number(post_id),
Expand All @@ -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,
Expand All @@ -175,7 +167,7 @@ export async function previewRouteHandler(
typeof options.preparePreviewData === 'function'
? options.preparePreviewData({
req: request,
post: result,
post,
previewData,
postTypeDef,
})
Expand All @@ -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 ?? '' });
}

Expand All @@ -214,7 +206,7 @@ export async function previewRouteHandler(
? options.getRedirectPath({
req: request,
defaultRedirectPath,
post: result,
post,
postTypeDef,
previewData,
})
Expand All @@ -234,7 +226,7 @@ export async function previewRouteHandler(
req: request,
previewData,
postTypeDef,
post: result,
post,
redirectPath,
});
} else {
Expand Down
92 changes: 92 additions & 0 deletions packages/next/src/rsc/handlers/revalidateRouterHandler.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
21 changes: 21 additions & 0 deletions packages/next/src/rsc/handlers/utils.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
1 change: 1 addition & 0 deletions packages/next/src/rsc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './config';

// handlers
export * from './handlers/previewRouteHandler';
export * from './handlers/revalidateRouterHandler';

// components
export * from './components/PreviewIndicator';
2 changes: 2 additions & 0 deletions projects/wp-nextjs-app/src/app/(single)/[...path]/page.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
7 changes: 7 additions & 0 deletions projects/wp-nextjs-app/src/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -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);
}

0 comments on commit 08ae034

Please sign in to comment.