diff --git a/docs/advanced-features/preview-mode.md b/docs/advanced-features/preview-mode.md index c3fdf3608d53..a6700718692d 100644 --- a/docs/advanced-features/preview-mode.md +++ b/docs/advanced-features/preview-mode.md @@ -194,10 +194,12 @@ Then, send a request to `/api/clear-preview-mode-cookies` to invoke the API Rout `setPreviewData` takes an optional second parameter which should be an options object. It accepts the following keys: - `maxAge`: Specifies the number (in seconds) for the preview session to last for. +- `path`: Specifies the path the cookie should be applied under. Defaults to `/` enabling preview mode for all paths. ```js setPreviewData(data, { maxAge: 60 * 60, // The preview mode cookies expire in 1 hour + path: '/about', // The preview mode cookies apply to paths with /about }) ``` diff --git a/packages/next/server/api-utils/node.ts b/packages/next/server/api-utils/node.ts index 85fa6f1f2a5a..0f9a7654a8d8 100644 --- a/packages/next/server/api-utils/node.ts +++ b/packages/next/server/api-utils/node.ts @@ -469,6 +469,7 @@ function setPreviewData( data: object | string, // TODO: strict runtime type checking options: { maxAge?: number + path?: string } & __ApiPreviewProps ): NextApiResponse { if (isNotValidData(options.previewModeId)) { @@ -522,6 +523,9 @@ function setPreviewData( ...(options.maxAge !== undefined ? ({ maxAge: options.maxAge } as CookieSerializeOptions) : undefined), + ...(options.path !== undefined + ? ({ path: options.path } as CookieSerializeOptions) + : undefined), }), serialize(COOKIE_NAME_PRERENDER_DATA, payload, { httpOnly: true, @@ -531,6 +535,9 @@ function setPreviewData( ...(options.maxAge !== undefined ? ({ maxAge: options.maxAge } as CookieSerializeOptions) : undefined), + ...(options.path !== undefined + ? ({ path: options.path } as CookieSerializeOptions) + : undefined), }), ]) return res diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index 0f53979f2858..b279a0df4be1 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -252,6 +252,11 @@ export type NextApiResponse = ServerResponse & { * when the client shuts down (browser is closed). */ maxAge?: number + /** + * Specifies the path for the preview session to work under. By default, + * the path is considered the "default path", i.e., any pages under "/". + */ + path?: string } ) => NextApiResponse clearPreviewData: () => NextApiResponse diff --git a/test/integration/prerender-preview/pages/api/preview.js b/test/integration/prerender-preview/pages/api/preview.js index 5476b7fd4c46..9303bcd24662 100644 --- a/test/integration/prerender-preview/pages/api/preview.js +++ b/test/integration/prerender-preview/pages/api/preview.js @@ -6,14 +6,12 @@ export default (req, res) => { return res.status(500).end('too big') } } else { - res.setPreviewData( - req.query, - req.query.cookieMaxAge - ? { - maxAge: req.query.cookieMaxAge, - } - : undefined - ) + res.setPreviewData(req.query, { + ...(req.query.cookieMaxAge + ? { maxAge: req.query.cookieMaxAge } + : undefined), + ...(req.query.cookiePath ? { path: req.query.cookiePath } : undefined), + }) } res.status(200).end() diff --git a/test/integration/prerender-preview/test/index.test.js b/test/integration/prerender-preview/test/index.test.js index df2fb44573e4..ce9d77087f93 100644 --- a/test/integration/prerender-preview/test/index.test.js +++ b/test/integration/prerender-preview/test/index.test.js @@ -121,6 +121,26 @@ function runTests(startServer = nextStart) { expect(cookies[1]).toHaveProperty('__next_preview_data') expect(cookies[1]['Max-Age']).toBe(expiry) }) + it('should set custom path cookies', async () => { + const path = '/path' + const res = await fetchViaHTTP(appPort, '/api/preview', { + cookiePath: path, + }) + expect(res.status).toBe(200) + + const originalCookies = res.headers.get('set-cookie').split(',') + const cookies = originalCookies.map(cookie.parse) + + expect(originalCookies.every((c) => c.includes('; Secure;'))).toBe(true) + + expect(cookies.length).toBe(2) + expect(cookies[0]).toMatchObject({ Path: path, SameSite: 'None' }) + expect(cookies[0]).toHaveProperty('__prerender_bypass') + expect(cookies[0]['Path']).toBe(path) + expect(cookies[0]).toMatchObject({ Path: path, SameSite: 'None' }) + expect(cookies[1]).toHaveProperty('__next_preview_data') + expect(cookies[1]['Path']).toBe(path) + }) it('should not return fallback page on preview request', async () => { const res = await fetchViaHTTP( appPort, diff --git a/test/unit/split-cookies-string.test.ts b/test/unit/split-cookies-string.test.ts index cf6764b42e9b..2d5e89fc091b 100644 --- a/test/unit/split-cookies-string.test.ts +++ b/test/unit/split-cookies-string.test.ts @@ -44,6 +44,16 @@ describe('splitCookiesString', () => { expect(result).toEqual(expected) }) + it('should parse path', () => { + const { joined, expected } = generateCookies({ + name: 'foo', + value: 'bar', + path: '/path', + }) + const result = splitCookiesString(joined) + expect(result).toEqual(expected) + }) + it('should parse with all the options', () => { const { joined, expected } = generateCookies({ name: 'foo', @@ -111,6 +121,23 @@ describe('splitCookiesString', () => { expect(result).toEqual(expected) }) + it('should parse path', () => { + const { joined, expected } = generateCookies( + { + name: 'foo', + value: 'bar', + path: '/path', + }, + { + name: 'x', + value: 'y', + path: '/path', + } + ) + const result = splitCookiesString(joined) + expect(result).toEqual(expected) + }) + it('should parse with all the options', () => { const { joined, expected } = generateCookies( {