From 228764fa5d5d89554926dd1ef5413f7062ba2fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADcholas=20Andr=C3=A9?= Date: Tue, 11 Jul 2023 00:44:35 -0300 Subject: [PATCH] feat: set cookie path when entering preview mode --- packages/core/src/types.ts | 14 +++- .../src/handlers/__tests__/previewHandler.ts | 74 +++++++++++++++++ packages/next/src/handlers/previewHandler.ts | 81 +++++++++++++++++-- 3 files changed, 157 insertions(+), 12 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 2d382345c..c5be9d8cb 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,16 +1,22 @@ -export type CustomPostTypes = Array<{ +export type CustomPostType = { slug: string; endpoint: string; single?: string; archive?: string; -}>; +}; + +export type CustomPostTypes = Array; + export type RedirectStrategy = '404' | 'none' | 'always'; -export type CustomTaxonomies = Array<{ + +export type CustomTaxonomy = { slug: string; endpoint: string; rewrite?: string; restParam?: string; -}>; +}; + +export type CustomTaxonomies = Array; export interface Integration { enable: boolean; diff --git a/packages/next/src/handlers/__tests__/previewHandler.ts b/packages/next/src/handlers/__tests__/previewHandler.ts index a8dd14391..2177f3574 100644 --- a/packages/next/src/handlers/__tests__/previewHandler.ts +++ b/packages/next/src/handlers/__tests__/previewHandler.ts @@ -51,4 +51,78 @@ describe('previewHandler', () => { expect(res.setPreviewData).toHaveBeenCalled(); expect(res._getStatusCode()).toBe(302); }); + + it('sets preview cookie path', async () => { + const { req, res } = createMocks({ + method: 'GET', + query: { post_id: DRAFT_POST_ID, token: VALID_AUTH_TOKEN, post_type: 'post' }, + }); + + res.setPreviewData = jest.fn(); + await previewHandler(req, res); + + expect(res.setPreviewData).toHaveBeenCalledWith( + { + authToken: 'this is a valid auth', + id: 57, + postType: 'post', + revision: false, + }, + { maxAge: 300, path: '/modi-qui-dignissimos-sed-assumenda-sint-iusto-preview=true' }, + ); + expect(res._getStatusCode()).toBe(302); + }); + + it('set preview cookie path to all paths if onRedirect is passed without getRedirectPath', async () => { + const { req, res } = createMocks({ + method: 'GET', + query: { post_id: DRAFT_POST_ID, token: VALID_AUTH_TOKEN, post_type: 'post' }, + }); + + res.setPreviewData = jest.fn(); + await previewHandler(req, res, { + onRedirect(req, res) { + return res.redirect('/'); + }, + }); + + expect(res.setPreviewData).toHaveBeenCalledWith( + { + authToken: 'this is a valid auth', + id: 57, + postType: 'post', + revision: false, + }, + { maxAge: 300, path: '/' }, + ); + expect(res._getStatusCode()).toBe(302); + }); + + it('set preview cookie path redirectPath if getRedirectPath is passed', async () => { + const { req, res } = createMocks({ + method: 'GET', + query: { post_id: DRAFT_POST_ID, token: VALID_AUTH_TOKEN, post_type: 'post' }, + }); + + res.setPreviewData = jest.fn(); + await previewHandler(req, res, { + getRedirectPath() { + return '/custom-redirect-path/'; + }, + onRedirect(req, res) { + return res.redirect('/'); + }, + }); + + expect(res.setPreviewData).toHaveBeenCalledWith( + { + authToken: 'this is a valid auth', + id: 57, + postType: 'post', + revision: false, + }, + { maxAge: 300, path: '/custom-redirect-path-preview=true' }, + ); + expect(res._getStatusCode()).toBe(302); + }); }); diff --git a/packages/next/src/handlers/previewHandler.ts b/packages/next/src/handlers/previewHandler.ts index c8cd68296..e7f334697 100644 --- a/packages/next/src/handlers/previewHandler.ts +++ b/packages/next/src/handlers/previewHandler.ts @@ -1,4 +1,4 @@ -import { getSiteByHost, PostEntity } from '@headstartwp/core'; +import { CustomPostType, getSiteByHost, PostEntity } from '@headstartwp/core'; import { getCustomPostType, getHeadlessConfig } from '@headstartwp/core/utils'; import type { NextApiRequest, NextApiResponse } from 'next'; import { fetchHookData, usePost } from '../data'; @@ -9,7 +9,9 @@ import { PreviewData } from './types'; */ export type PreviewHandlerOptions = { /** - * If passed will override the behavior of redirecting to the previewed post. + * If passed will override the behavior of redirecting to the previewed post. We recommend implementing `getRedirectPath` instead. If you + * absolutely need to implement a custom redirect handler, we also suggest you implement `getRedirectPath` so that the preview cookie only + * applies to the specific path. * * If set you should handle the redirect yourself by calling `res.redirect`. * @@ -29,14 +31,38 @@ export type PreviewHandlerOptions = { * }); * } * ``` + * + * @param req The NextApiRequest object + * @param res The NextApiResponse object + * @param previewData The previewData object + * @param defaultRedirect The default redirect function + * @param redirectPath The default redirect path or the one implemented in {@link PreviewHandlerOptions['getRedirectPath']} */ onRedirect?: ( req: NextApiRequest, res: NextApiResponse, previewData: PreviewData, defaultRedirect?: PreviewHandlerOptions['onRedirect'], + redirectpath?: string, ) => NextApiResponse; + /** + * If passed will override the default redirect path + * + * **Important**: You should not need to override this but if you do, uou must append `-preview=true` to the end of the redirecte path. + * + * @param defaultRedirectPath the default redirect path + * @param result PostEntity + * @param postTypeDef The object describing a post type + * + * @returns the new redirect path + */ + getRedirectPath?: ( + defaultRedirectPath: string, + result: any, + postTypeDef: CustomPostType, + ) => string; + /** * If passed, this function will be called when the preview data is fetched and allows * for additional preview data to be set. @@ -52,6 +78,18 @@ export type PreviewHandlerOptions = { ) => PreviewData; }; +function withPreviewSuffix(path: string) { + const suffix = '-preview=true'; + // remove trailing slash + const normalizePath = path.replace(/\/+$/, ''); + + if (normalizePath.endsWith(suffix)) { + return normalizePath; + } + + return `${[normalizePath]}${suffix}`; +} + /** * The PreviewHandler is responsible for handling preview requests. * @@ -141,28 +179,55 @@ export async function previewHandler( previewData = options.preparePreviewData(req, res, result, previewData); } - res.setPreviewData(previewData); - const postTypeDef = getCustomPostType(post_type as string, sourceUrl); if (!postTypeDef) { return res.end('Cannot preview an unknown post type'); } - const defaultRedirect: PreviewHandlerOptions['onRedirect'] = (req, res) => { + /** + * Builds the default redirect path + * + * @returns the default redirec tpath + */ + const getDefaultRedirectPath = () => { const singleRoute = postTypeDef.single || '/'; const prefixRoute = singleRoute === '/' ? '' : singleRoute; const slugOrId = revision ? post_id : slug || post_id; if (locale) { - return res.redirect(`/${locale}/${prefixRoute}/${slugOrId}-preview=true`); + return `/${locale}/${prefixRoute}/${slugOrId}`; } - return res.redirect(`${prefixRoute}/${slugOrId}-preview=true`); + return `${prefixRoute}/${slugOrId}`; + }; + + const redirectPath = + typeof options.getRedirectPath === 'function' + ? withPreviewSuffix( + options.getRedirectPath(getDefaultRedirectPath(), result, postTypeDef), + ) + : withPreviewSuffix(getDefaultRedirectPath()); + + // we should set the path cookie if onRedirect is undefined (i.e we're just using default behasvior) + // or if user has supplied getRedirectPath from which we can get the actual path + const shouldSetPathInCookie = + typeof options.onRedirect === 'undefined' || + typeof options.getRedirectPath === 'function'; + + res.setPreviewData(previewData, { + maxAge: 5 * 60, + // we can only safely narrow the cookei to a path if getRedirectPath is implemented or + // it's using the default behavior without a custom onRedirect + path: shouldSetPathInCookie ? redirectPath : '/', + }); + + const defaultRedirect: PreviewHandlerOptions['onRedirect'] = (req, res) => { + return res.redirect(getDefaultRedirectPath()); }; if (options?.onRedirect) { - return options.onRedirect(req, res, previewData, defaultRedirect); + return options.onRedirect(req, res, previewData, defaultRedirect, redirectPath); } return defaultRedirect(req, res, previewData);