Skip to content

Commit 8d5de97

Browse files
authored
v2: fix fallback and VA redirects (#2948)
1 parent d9b5a89 commit 8d5de97

File tree

14 files changed

+279
-176
lines changed

14 files changed

+279
-176
lines changed

bun.lock

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
},
5050
"packages/gitbook": {
5151
"name": "gitbook",
52-
"version": "0.6.5",
52+
"version": "0.7.2",
5353
"dependencies": {
5454
"@gitbook/api": "0.96.1",
5555
"@gitbook/cache-do": "workspace:*",
@@ -132,7 +132,7 @@
132132
},
133133
"packages/gitbook-v2": {
134134
"name": "gitbook-v2",
135-
"version": "0.1.2",
135+
"version": "0.2.0",
136136
"dependencies": {
137137
"@gitbook/api": "0.96.1",
138138
"@gitbook/cache-tags": "workspace:*",
@@ -144,7 +144,7 @@
144144
"warn-once": "^0.1.1",
145145
},
146146
"devDependencies": {
147-
"@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@40fec7d",
147+
"@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@236c84d",
148148
"gitbook": "*",
149149
"postcss": "^8",
150150
"tailwindcss": "^3.4.0",
@@ -172,7 +172,7 @@
172172
},
173173
"packages/openapi-parser": {
174174
"name": "@gitbook/openapi-parser",
175-
"version": "2.0.2",
175+
"version": "2.1.0",
176176
"dependencies": {
177177
"@scalar/openapi-parser": "^0.10.9",
178178
"@scalar/openapi-types": "^0.1.9",
@@ -227,7 +227,7 @@
227227
},
228228
"packages/react-openapi": {
229229
"name": "@gitbook/react-openapi",
230-
"version": "1.0.5",
230+
"version": "1.1.2",
231231
"dependencies": {
232232
"@gitbook/openapi-parser": "workspace:*",
233233
"@scalar/api-client-react": "^1.1.36",
@@ -793,7 +793,7 @@
793793

794794
"@opennextjs/aws": ["@opennextjs/aws@https://pkg.pr.new/@opennextjs/aws@756", { "dependencies": { "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0" }, "bin": { "open-next": "./dist/index.js" } }],
795795

796-
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@40fec7d", { "dependencies": { "@ast-grep/napi": "^0.34.1", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@756", "enquirer": "^2.4.1", "glob": "^11.0.0", "yaml": "^2.7.0" }, "peerDependencies": { "wrangler": "^3.111.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }],
796+
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@236c84d", { "dependencies": { "@ast-grep/napi": "^0.34.1", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@756", "enquirer": "^2.4.1", "glob": "^11.0.0", "yaml": "^2.7.0" }, "peerDependencies": { "wrangler": "^3.111.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }],
797797

798798
"@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
799799

packages/gitbook-v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"devDependencies": {
1616
"gitbook": "*",
17-
"@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@40fec7d",
17+
"@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@236c84d",
1818
"tailwindcss": "^3.4.0",
1919
"postcss": "^8"
2020
},

packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/page.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default async function Page(props: PageProps) {
1616
const context = await getDynamicSiteContext(params);
1717
const pathname = getPagePathFromParams(params);
1818

19-
return <SitePage context={context} pageParams={{ pathname }} redirectOnFallback={true} />;
19+
return <SitePage context={context} pageParams={{ pathname }} />;
2020
}
2121

2222
export async function generateViewport(props: PageProps): Promise<Viewport> {
@@ -25,14 +25,12 @@ export async function generateViewport(props: PageProps): Promise<Viewport> {
2525
}
2626

2727
export async function generateMetadata(props: PageProps): Promise<Metadata> {
28-
const [params, searchParams] = await Promise.all([props.params, props.searchParams]);
28+
const params = await props.params;
2929
const context = await getDynamicSiteContext(params);
3030
const pathname = getPagePathFromParams(params);
3131

3232
return generateSitePageMetadata({
3333
context,
3434
pageParams: { pathname },
35-
redirectOnFallback: true,
36-
fallback: !!searchParams.fallback,
3735
});
3836
}

packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/page.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default async function Page(props: PageProps) {
2828
})
2929
);
3030

31-
return <SitePage context={context} pageParams={{ pathname }} redirectOnFallback={true} />;
31+
return <SitePage context={context} pageParams={{ pathname }} />;
3232
}
3333

3434
export async function generateViewport(props: PageProps): Promise<Viewport> {
@@ -44,6 +44,5 @@ export async function generateMetadata(props: PageProps): Promise<Metadata> {
4444
return generateSitePageMetadata({
4545
context,
4646
pageParams: { pathname },
47-
redirectOnFallback: true,
4847
});
4948
}

packages/gitbook-v2/src/lib/context.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -116,40 +116,57 @@ export function getBaseContext(input: {
116116
apiToken: input.apiToken ?? GITBOOK_API_TOKEN,
117117
apiEndpoint: GITBOOK_API_URL,
118118
});
119-
const gitbookURL = GITBOOK_URL ? new URL(GITBOOK_URL) : undefined;
120119

120+
const linker = getLinkerForSiteURL({
121+
siteURL: url,
122+
urlMode,
123+
});
124+
125+
const imageResizer = createImageResizer({
126+
host: url.host,
127+
// To ensure image resizing work for proxied sites,
128+
// we serve images from the root of the site.
129+
linker: linker,
130+
});
131+
132+
return {
133+
dataFetcher,
134+
linker,
135+
imageResizer,
136+
};
137+
}
138+
139+
/**
140+
* Get the linker for a given site URL.
141+
*/
142+
export function getLinkerForSiteURL(input: {
143+
siteURL: URL;
144+
urlMode: 'url' | 'url-host';
145+
}) {
146+
const { siteURL, urlMode } = input;
147+
148+
const gitbookURL = GITBOOK_URL ? new URL(GITBOOK_URL) : undefined;
121149
const linker =
122150
urlMode === 'url-host'
123151
? createLinker({
124-
host: url.host,
125-
pathname: url.pathname,
152+
host: siteURL.host,
153+
pathname: siteURL.pathname,
126154
})
127155
: createLinker({
128156
protocol: gitbookURL?.protocol,
129157
host: gitbookURL?.host,
130-
pathname: `/url/${url.host}${url.pathname}`,
158+
pathname: `/url/${siteURL.host}${siteURL.pathname}`,
131159
});
132160

133161
if (urlMode === 'url') {
134162
// Create link in the same format for links to other sites/sections.
135163
linker.toLinkForContent = (rawURL: string) => {
136164
const urlObject = new URL(rawURL);
137-
return `/url/${urlObject.host}${urlObject.pathname}`;
165+
return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`;
138166
};
139167
}
140168

141-
const imageResizer = createImageResizer({
142-
host: url.host,
143-
// To ensure image resizing work for proxied sites,
144-
// we serve images from the root of the site.
145-
linker: linker,
146-
});
147-
148-
return {
149-
dataFetcher,
150-
linker,
151-
imageResizer,
152-
};
169+
return linker;
153170
}
154171

155172
/**

packages/gitbook-v2/src/middleware.ts

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ import { NextResponse } from 'next/server';
55
import { getContentSecurityPolicy } from '@/lib/csp';
66
import { validateSerializedCustomization } from '@/lib/customization';
77
import { removeLeadingSlash, removeTrailingSlash } from '@/lib/paths';
8-
import { getResponseCookiesForVisitorAuth, getVisitorToken } from '@/lib/visitor-token';
8+
import {
9+
type ResponseCookies,
10+
getResponseCookiesForVisitorAuth,
11+
getVisitorToken,
12+
normalizeVisitorAuthURL,
13+
} from '@/lib/visitor-token';
914
import { serveResizedImage } from '@/routes/image';
10-
import { getPublishedContentByURL } from '@v2/lib/data';
15+
import { getLinkerForSiteURL } from '@v2/lib/context';
16+
import { getPublishedContentByURL, normalizeURL } from '@v2/lib/data';
1117
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
1218
import { MiddlewareHeaders } from '@v2/lib/middleware';
1319

@@ -21,6 +27,14 @@ type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
2127

2228
export async function middleware(request: NextRequest) {
2329
try {
30+
const requestURL = new URL(request.url);
31+
32+
// Redirect to normalize the URL
33+
const normalized = normalizeURL(requestURL);
34+
if (normalized.toString() !== requestURL.toString()) {
35+
return NextResponse.redirect(normalized.toString());
36+
}
37+
2438
// Route all requests to a site
2539
const extracted = getSiteURLFromRequest(request);
2640
if (extracted) {
@@ -38,7 +52,7 @@ export async function middleware(request: NextRequest) {
3852
});
3953
}
4054

41-
return await serveSiteByURL(request, extracted);
55+
return await serveSiteByURL(requestURL, request, extracted);
4256
}
4357

4458
// Handle the rest with the router default logic
@@ -51,7 +65,7 @@ export async function middleware(request: NextRequest) {
5165
/**
5266
* Serve site by URL.
5367
*/
54-
async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
68+
async function serveSiteByURL(requestURL: URL, request: NextRequest, urlWithMode: URLWithMode) {
5569
const { url, mode } = urlWithMode;
5670

5771
// Visitor authentication
@@ -72,11 +86,55 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
7286
}
7387

7488
const { data } = result;
89+
let cookies: ResponseCookies = {};
7590

91+
//
92+
// Handle redirects
93+
//
7694
if ('redirect' in data) {
95+
// biome-ignore lint/suspicious/noConsole: we want to log the redirect
96+
console.log('redirect', data.redirect);
97+
if (data.target === 'content') {
98+
// For content redirects, we use the linker to redirect the optimal URL
99+
// during development and testing in 'url' mode.
100+
const linker = getLinkerForSiteURL({
101+
siteURL: url,
102+
urlMode: mode,
103+
});
104+
105+
const contentRedirect = new URL(linker.toLinkForContent(data.redirect), request.url);
106+
107+
// Keep the same search params as the original request
108+
// as it might contain a VA token
109+
contentRedirect.search = request.nextUrl.search;
110+
111+
return NextResponse.redirect(contentRedirect);
112+
}
113+
77114
return NextResponse.redirect(data.redirect);
78115
}
79116

117+
cookies = {
118+
...cookies,
119+
...getResponseCookiesForVisitorAuth(data.basePath, visitorToken),
120+
};
121+
122+
//
123+
// Make sure the URL is clean of any va token after a successful lookup
124+
// The token is stored in a cookie that is set on the redirect response
125+
//
126+
const requestURLWithoutToken = normalizeVisitorAuthURL(requestURL);
127+
if (requestURLWithoutToken.toString() !== requestURL.toString()) {
128+
return writeResponseCookies(
129+
NextResponse.redirect(requestURLWithoutToken.toString()),
130+
cookies
131+
);
132+
}
133+
134+
//
135+
// Render and serve the content
136+
//
137+
80138
// When visitor has authentication (adaptive content or VA), we serve dynamic routes.
81139
let routeType = visitorToken ? 'dynamic' : 'static';
82140

@@ -108,13 +166,13 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
108166
requestHeaders.set('x-forwarded-host', request.nextUrl.host);
109167
requestHeaders.set('origin', request.nextUrl.origin);
110168

111-
const siteURL = `${url.host}${data.basePath}`;
169+
const siteURLWithoutProtocol = `${url.host}${data.basePath}`;
112170

113171
const route = [
114172
'sites',
115173
routeType,
116174
mode,
117-
encodeURIComponent(siteURL),
175+
encodeURIComponent(siteURLWithoutProtocol),
118176
encodePathInSiteContent(data.pathname),
119177
].join('/');
120178

@@ -135,16 +193,9 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
135193
response.headers.set('x-content-type-options', 'nosniff');
136194
// Debug header
137195
response.headers.set('x-gitbook-route-type', routeType);
138-
response.headers.set('x-gitbook-site-url', siteURL);
196+
response.headers.set('x-gitbook-route-site', siteURLWithoutProtocol);
139197

140-
if (visitorToken) {
141-
const cookies = getResponseCookiesForVisitorAuth(data.basePath, visitorToken);
142-
for (const [key, value] of Object.entries(cookies)) {
143-
response.cookies.set(key, value.value, value.options);
144-
}
145-
}
146-
147-
return response;
198+
return writeResponseCookies(response, cookies);
148199
}
149200

150201
/**
@@ -248,3 +299,14 @@ function appendQueryParams(url: URL, from: URLSearchParams) {
248299

249300
return url;
250301
}
302+
303+
/**
304+
* Write the cookies to a response.
305+
*/
306+
function writeResponseCookies<R extends NextResponse>(response: R, cookies: ResponseCookies): R {
307+
Object.entries(cookies).forEach(([key, { value, options }]) => {
308+
response.cookies.set(key, value, options);
309+
});
310+
311+
return response;
312+
}

packages/gitbook/e2e/internal.spec.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ const testCases: TestsCase[] = [
155155
.click();
156156

157157
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
158-
await page.waitForURL(
159-
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/2.0/reference/api-reference/pets?fallback=true'
158+
await page.waitForURL((url) =>
159+
url.pathname.includes('api-multi-versions/2.0/reference/api-reference/pets')
160160
);
161161
},
162162
},
@@ -178,8 +178,10 @@ const testCases: TestsCase[] = [
178178
.click();
179179

180180
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
181-
await page.waitForURL(
182-
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/2.0/reference/api-reference/pets?fallback=true'
181+
await page.waitForURL((url) =>
182+
url.pathname.includes(
183+
'api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/2.0/reference/api-reference/pets'
184+
)
183185
);
184186
},
185187
},
@@ -213,8 +215,10 @@ const testCases: TestsCase[] = [
213215
.click();
214216

215217
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
216-
await page.waitForURL(
217-
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/2.0/reference/api-reference/pets?fallback=true'
218+
await page.waitForURL((url) =>
219+
url.pathname.includes(
220+
'api-multi-versions-va/2.0/reference/api-reference/pets'
221+
)
218222
);
219223
},
220224
},
@@ -244,9 +248,7 @@ const testCases: TestsCase[] = [
244248
const sectionGroupDropdown = await page.getByText('Test Section Group 1');
245249
await sectionGroupDropdown.hover();
246250
await page.getByText('Section B').click();
247-
await page.waitForURL(
248-
'https://gitbook-open-e2e-sites.gitbook.io/sections/sections-4'
249-
);
251+
await page.waitForURL((url) => url.pathname.includes('/sections/sections-4'));
250252
},
251253
},
252254
],
@@ -820,6 +822,9 @@ const testCases: TestsCase[] = [
820822
expiresIn: '24h',
821823
}
822824
);
825+
826+
// Test that when accessing the non-canonical URL, we are redirected to the canonical URL
827+
// with the jwt token in the query string
823828
return `spacea?jwt_token=${token}`;
824829
})(),
825830
run: waitForCookiesDialog,

0 commit comments

Comments
 (0)