diff --git a/example/src/pages/api/preview/[[...slug]].ts b/example/src/pages/api/preview/[[...handle]].ts similarity index 100% rename from example/src/pages/api/preview/[[...slug]].ts rename to example/src/pages/api/preview/[[...handle]].ts diff --git a/package.json b/package.json index 81177b2..a5784b7 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "dependencies": { "fast-deep-equal": "^3.1.3", "intersection-observer": "^0.12.0", + "qs": "^6.10.1", "storyblok-rich-text-react-renderer": "^2.1.1" }, "devDependencies": { diff --git a/src/bridge/withStory.tsx b/src/bridge/withStory.tsx index 33157e1..6d14282 100644 --- a/src/bridge/withStory.tsx +++ b/src/bridge/withStory.tsx @@ -24,7 +24,7 @@ export const withStory = ( if ( (props as any)?.__storyblok_toolkit_preview && typeof window !== 'undefined' && - !window.storyblok?.isInEditor() + (!window.storyblok || !window.storyblok.isInEditor()) ) { setPreview(true); } @@ -44,6 +44,7 @@ export const withStory = ( backgroundColor: '#111', color: '#fff', boxShadow: '0px 12px 24px rgb(102, 102, 102, 0.25)', + zIndex: 9999999, }} >
{ expect(setPreviewDataMock).not.toBeCalled(); }); + it('should enable preview mode and redirect if disable story check', async () => { + server.use( + rest.get( + `https://api.storyblok.com/v1/cdn/stories/${slug}`, + async (_, res, ctx) => { + return res(ctx.status(404), ctx.json({})); + }, + ), + ); + + const setPreviewDataMock = jest.fn(); + EventEmitter.prototype.setPreviewData = setPreviewDataMock; + + const { req, res } = createMocks( + { method: 'GET', query: { slug, token: previewToken, anything: true } }, + { + eventEmitter: EventEmitter, + }, + ); + + await nextPreviewHandlers({ + disableStoryCheck: true, + previewToken, + storyblokToken, + })(req as any, res as any); + + expect(res._getRedirectUrl()).toBe(`/${slug}?anything=true`); + expect(setPreviewDataMock).toBeCalledWith({}); + }); + it('should exit preview mode on clear route', async () => { const clearPreviewData = jest.fn(); EventEmitter.prototype.clearPreviewData = clearPreviewData; const { req, res } = createMocks( - { method: 'GET', query: { slug: ['clear'] } }, + { method: 'GET', query: { handle: ['clear'] } }, { eventEmitter: EventEmitter, }, diff --git a/src/next/previewHandlers.ts b/src/next/previewHandlers.ts index fa6b50a..24ee24c 100644 --- a/src/next/previewHandlers.ts +++ b/src/next/previewHandlers.ts @@ -1,6 +1,13 @@ import type { NextApiRequest, NextApiResponse } from 'next'; +import qs from 'qs'; interface NextPreviewHandlersProps { + /** + * Disable checking if a story with slug exists + * + * @default false + */ + disableStoryCheck?: boolean; /** * A secret token (random string of characters) to activate preview mode. */ @@ -12,26 +19,37 @@ interface NextPreviewHandlersProps { } export const nextPreviewHandlers = ({ + disableStoryCheck, previewToken, storyblokToken, }: NextPreviewHandlersProps) => async ( req: NextApiRequest, res: NextApiResponse, ) => { - if (req.query.slug?.[0] === 'clear') { + const { token, slug, handle, ...rest } = req.query; + + if (handle?.[0] === 'clear') { res.clearPreviewData(); return res.redirect(req.headers.referer || '/'); } // Check the secret and next parameters // This secret should only be known to this API route and the CMS - if (req.query.token !== previewToken || !req.query.slug) { + if (token !== previewToken) { return res.status(401).json({ message: 'Invalid token' }); } + const restParams = + rest && Object.keys(rest).length ? `?${qs.stringify(rest)}` : ''; + + if (disableStoryCheck) { + res.setPreviewData({}); + return res.redirect(`/${slug}${restParams}`); + } + // Fetch Storyblok to check if the provided `slug` exists - const { story } = await fetch( - `https://api.storyblok.com/v1/cdn/stories/${req.query.slug}?token=${storyblokToken}&version=draft`, + let { story } = await fetch( + `https://api.storyblok.com/v1/cdn/stories/${slug}?token=${storyblokToken}&version=draft`, { method: 'GET', }, @@ -47,5 +65,5 @@ export const nextPreviewHandlers = ({ // Redirect to the path from the fetched post // We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities - res.redirect(`/${story.full_slug}`); + res.redirect(`/${story.full_slug}${restParams}`); }; diff --git a/website/docs/api/nextPreviewHandlers.md b/website/docs/api/nextPreviewHandlers.md index 6c769c4..5ada56e 100644 --- a/website/docs/api/nextPreviewHandlers.md +++ b/website/docs/api/nextPreviewHandlers.md @@ -15,6 +15,12 @@ A function that provides API handlers to implement Next.js's preview mode. ```ts no-transpile interface NextPreviewHandlersProps { + /** + * Disable checking if a story with slug exists + * + * @default false + */ + disableStoryCheck?: boolean; /** * A secret token (random string of characters) to activate preview mode. */ @@ -32,7 +38,7 @@ const nextPreviewHandlers: (options: NextPreviewHandlersProps) => (req: NextApiR ### Basic example -Create the file `./pages/api/preview/[[...slug]].ts` with the following contents: +Create the file `./pages/api/preview/[[...handle]].ts` with the following contents: ```ts import { nextPreviewHandlers } from '@storyofams/storyblok-toolkit'; @@ -44,10 +50,10 @@ export default nextPreviewHandlers({ ``` To open preview mode of a story at `/article/article-1`, go to: -`/api/preview?token=YOUR_PREVIEW_TOKEN&slug=/article/article-1` +`/api/preview?token=YOUR_PREVIEW_TOKEN&slug=article/article-1` You can configure preview mode as a preview URL in Storyblok: -`YOUR_WEBSITE/api/preview?token=YOUR_PREVIEW_TOKEN&slug=/` +`YOUR_WEBSITE/api/preview?token=YOUR_PREVIEW_TOKEN&slug=` If you are using the preview handlers and are on a page configured with `withStory`, you will automatically be shown a small indicator to remind you that you are viewing the page in preview mode. It also allows you to exit preview mode. Alternatively you can go to `/api/preview/clear` to exit preview mode. diff --git a/yarn.lock b/yarn.lock index e112bb6..a499e14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10699,6 +10699,13 @@ qrcode-terminal@^0.12.0: resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== +qs@^6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"