Compare commits

...

5 Commits

Author SHA1 Message Date
Zeno Kapitein 3fdd9de08c Merge branch 'main' into claude/tender-allen-wjt5jb 2026-08-19 11:52:48 +02:00
Claude e3f76571fe Format preview.test.ts imports (oxfmt)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwxstGi1QSoV6NQBdVDBye
2026-08-18 14:52:05 +00:00
Claude 69193a3717 Scope preview cookie to the preview route, not the site base path (RND-12191)
Re-diagnosis after review: app.gitbook.com and sites.gitbook.com are both under
the gitbook.com registrable domain, so the preview iframe is cross-origin but
same-site — a SameSite=Lax host-only cookie was already eligible on in-iframe
navigation, and the earlier SameSite=None change did not address the reported
loss in the default flow. Reverted that.

The real cause is the cookie path. In production the preview is served under
`sites.gitbook.com/preview/<siteId>/...` (url-host mode), but the cookie was
scoped to `siteURLData.siteBasePath`, the site's canonical base path (e.g.
`/docs`), which does not prefix the preview route. The browser therefore
withheld the cookie on the next in-preview navigation, so the override — seeded
on the first page via the `?customization=` query param — was lost.

Scope the cookie to the preview route prefix `<previewBasePath>/<siteId>`,
derived from the request URL, mirroring getVisitorAuthBasePath for proxy
requests. Falls back to siteBasePath for any preview not served under the
standard preview route. Added preview.test.ts cases covering url-host, url, and
the non-preview fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwxstGi1QSoV6NQBdVDBye
2026-08-18 14:50:38 +00:00
Zeno Kapitein c7a3c4fd4f Merge branch 'main' into claude/tender-allen-wjt5jb 2026-08-18 13:29:03 +02:00
Claude a679eb21b1 Fix customization/theme preview being lost when navigating inside the preview (RND-12191)
The site preview runs in a cross-site iframe. The preview cookie that carries
the unsaved customization (and theme) override was set with SameSite=Lax, so the
browser withheld it on in-iframe navigations: the first page reflected the
override (it comes from the ?customization= query param) but clicking to another
page sent no param and no cookie, so the middleware fell back to the saved
settings and the change disappeared.

Send the preview cookie with SameSite=None + Secure in production, mirroring the
visitor cookies that already have to survive this same iframe
(getResponseCookiesForVisitorAuth). Dev stays same-origin so no attributes are
needed there. SameSite=None requires Secure, so both are gated on production.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwxstGi1QSoV6NQBdVDBye
2026-08-10 00:22:06 +00:00
3 changed files with 63 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix previewed customization and theme overrides being lost when navigating between pages inside the site preview. The preview cookie was scoped to the site's canonical base path instead of the preview route it is actually served under, so the browser dropped it on the next in-preview navigation.
+43 -1
View File
@@ -1,6 +1,8 @@
import { describe, expect, it } from 'bun:test';
import { getPreviewRequestIdentifier, isPreviewRequest } from './preview';
import type { PublishedSiteContent } from '@gitbook/api';
import { getPreviewCookieResponse, getPreviewRequestIdentifier, isPreviewRequest } from './preview';
describe('isPreviewRequest', () => {
it('should return true for preview requests', () => {
@@ -23,3 +25,43 @@ describe('getPreviewRequestIdentifier', () => {
expect(getPreviewRequestIdentifier(previewRequestURL)).toBe('site_foo');
});
});
describe('getPreviewCookieResponse', () => {
// The site's canonical base path — deliberately different from the preview route prefix so the
// test fails if the cookie is scoped to it instead of the preview route.
const siteURLData = { siteBasePath: '/docs/' } as unknown as PublishedSiteContent;
it('scopes the cookie to the preview route prefix in url-host mode, not the site base path', () => {
const cookie = getPreviewCookieResponse({
name: 'gitbook-customization',
value: 'v',
mode: 'url-host',
siteRequestURL: new URL('https://sites.gitbook.com/preview/site_foo/hello/world'),
siteURLData,
});
expect(cookie.options?.path).toBe('/preview/site_foo');
expect(cookie.options?.sameSite).toBe('lax');
});
it('scopes the cookie to the proxied preview route prefix in url mode', () => {
const cookie = getPreviewCookieResponse({
name: 'gitbook-customization',
value: 'v',
mode: 'url',
siteRequestURL: new URL('https://sites.gitbook.com/preview/site_foo/hello/world'),
siteURLData,
});
expect(cookie.options?.path).toBe('/url/sites.gitbook.com/preview/site_foo');
});
it('falls back to the site base path for a non-preview url-host request', () => {
const cookie = getPreviewCookieResponse({
name: 'gitbook-customization',
value: 'v',
mode: 'url-host',
siteRequestURL: new URL('https://example.com/docs/hello/world'),
siteURLData,
});
expect(cookie.options?.path).toBe('/docs/');
});
});
+15 -2
View File
@@ -51,8 +51,21 @@ export function getPreviewCookieResponse(args: {
gitbookPreviewBaseURL.host + gitbookPreviewBaseURL.pathname.replace(/\/$/, '');
return `/url/${gitbookPreviewHostPath}/${getPreviewRequestIdentifier(siteRequestURL)}`;
}
case 'url-host':
return siteURLData.siteBasePath;
case 'url-host': {
// The whole site preview (all variants/sections/pages) is served under the preview
// route prefix `<previewBasePath>/<siteId>` (e.g. `/preview/<siteId>`). Scope the
// cookie to that prefix, derived from the request, so it is resent on every
// in-preview navigation. The resolved `siteBasePath` is the site's canonical base
// path and does not prefix the preview route, so relying on it makes the browser
// drop the cookie on the next page — the actual cause of the override being lost.
// Mirrors getVisitorAuthBasePath, which uses the request identifier for proxy
// requests. Fall back to `siteBasePath` for any preview not served under the route.
if (!isPreviewRequest(siteRequestURL)) {
return siteURLData.siteBasePath;
}
const gitbookPreviewBaseURL = new URL(GITBOOK_PREVIEW_BASE_URL);
return `${gitbookPreviewBaseURL.pathname.replace(/\/$/, '')}/${getPreviewRequestIdentifier(siteRequestURL)}`;
}
default:
assertNever(mode);
}