Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.6.5",
"version": "0.7.2",
"dependencies": {
"@gitbook/api": "0.96.1",
"@gitbook/cache-do": "workspace:*",
Expand Down Expand Up @@ -132,7 +132,7 @@
},
"packages/gitbook-v2": {
"name": "gitbook-v2",
"version": "0.1.2",
"version": "0.2.0",
"dependencies": {
"@gitbook/api": "0.96.1",
"@gitbook/cache-tags": "workspace:*",
Expand All @@ -144,7 +144,7 @@
"warn-once": "^0.1.1",
},
"devDependencies": {
"@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@40fec7d",
"@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@236c84d",
"gitbook": "*",
"postcss": "^8",
"tailwindcss": "^3.4.0",
Expand Down Expand Up @@ -172,7 +172,7 @@
},
"packages/openapi-parser": {
"name": "@gitbook/openapi-parser",
"version": "2.0.2",
"version": "2.1.0",
"dependencies": {
"@scalar/openapi-parser": "^0.10.9",
"@scalar/openapi-types": "^0.1.9",
Expand Down Expand Up @@ -227,7 +227,7 @@
},
"packages/react-openapi": {
"name": "@gitbook/react-openapi",
"version": "1.0.5",
"version": "1.1.2",
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "^1.1.36",
Expand Down Expand Up @@ -793,7 +793,7 @@

"@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" } }],

"@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" } }],
"@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" } }],

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

Expand Down
2 changes: 1 addition & 1 deletion packages/gitbook-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"devDependencies": {
"gitbook": "*",
"@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@40fec7d",
"@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@236c84d",
"tailwindcss": "^3.4.0",
"postcss": "^8"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default async function Page(props: PageProps) {
const context = await getDynamicSiteContext(params);
const pathname = getPagePathFromParams(params);

return <SitePage context={context} pageParams={{ pathname }} redirectOnFallback={true} />;
return <SitePage context={context} pageParams={{ pathname }} />;
}

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

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

return generateSitePageMetadata({
context,
pageParams: { pathname },
redirectOnFallback: true,
fallback: !!searchParams.fallback,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default async function Page(props: PageProps) {
})
);

return <SitePage context={context} pageParams={{ pathname }} redirectOnFallback={true} />;
return <SitePage context={context} pageParams={{ pathname }} />;
}

export async function generateViewport(props: PageProps): Promise<Viewport> {
Expand All @@ -44,6 +44,5 @@ export async function generateMetadata(props: PageProps): Promise<Metadata> {
return generateSitePageMetadata({
context,
pageParams: { pathname },
redirectOnFallback: true,
});
}
51 changes: 34 additions & 17 deletions packages/gitbook-v2/src/lib/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,40 +116,57 @@ export function getBaseContext(input: {
apiToken: input.apiToken ?? GITBOOK_API_TOKEN,
apiEndpoint: GITBOOK_API_URL,
});
const gitbookURL = GITBOOK_URL ? new URL(GITBOOK_URL) : undefined;

const linker = getLinkerForSiteURL({
siteURL: url,
urlMode,
});

const imageResizer = createImageResizer({
host: url.host,
// To ensure image resizing work for proxied sites,
// we serve images from the root of the site.
linker: linker,
});

return {
dataFetcher,
linker,
imageResizer,
};
}

/**
* Get the linker for a given site URL.
*/
export function getLinkerForSiteURL(input: {
siteURL: URL;
urlMode: 'url' | 'url-host';
}) {
const { siteURL, urlMode } = input;

const gitbookURL = GITBOOK_URL ? new URL(GITBOOK_URL) : undefined;
const linker =
urlMode === 'url-host'
? createLinker({
host: url.host,
pathname: url.pathname,
host: siteURL.host,
pathname: siteURL.pathname,
})
: createLinker({
protocol: gitbookURL?.protocol,
host: gitbookURL?.host,
pathname: `/url/${url.host}${url.pathname}`,
pathname: `/url/${siteURL.host}${siteURL.pathname}`,
});

if (urlMode === 'url') {
// Create link in the same format for links to other sites/sections.
linker.toLinkForContent = (rawURL: string) => {
const urlObject = new URL(rawURL);
return `/url/${urlObject.host}${urlObject.pathname}`;
return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`;
};
}

const imageResizer = createImageResizer({
host: url.host,
// To ensure image resizing work for proxied sites,
// we serve images from the root of the site.
linker: linker,
});

return {
dataFetcher,
linker,
imageResizer,
};
return linker;
}

/**
Expand Down
92 changes: 77 additions & 15 deletions packages/gitbook-v2/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import { NextResponse } from 'next/server';
import { getContentSecurityPolicy } from '@/lib/csp';
import { validateSerializedCustomization } from '@/lib/customization';
import { removeLeadingSlash, removeTrailingSlash } from '@/lib/paths';
import { getResponseCookiesForVisitorAuth, getVisitorToken } from '@/lib/visitor-token';
import {
type ResponseCookies,
getResponseCookiesForVisitorAuth,
getVisitorToken,
normalizeVisitorAuthURL,
} from '@/lib/visitor-token';
import { serveResizedImage } from '@/routes/image';
import { getPublishedContentByURL } from '@v2/lib/data';
import { getLinkerForSiteURL } from '@v2/lib/context';
import { getPublishedContentByURL, normalizeURL } from '@v2/lib/data';
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
import { MiddlewareHeaders } from '@v2/lib/middleware';

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

export async function middleware(request: NextRequest) {
try {
const requestURL = new URL(request.url);

// Redirect to normalize the URL
const normalized = normalizeURL(requestURL);
if (normalized.toString() !== requestURL.toString()) {
return NextResponse.redirect(normalized.toString());
}

// Route all requests to a site
const extracted = getSiteURLFromRequest(request);
if (extracted) {
Expand All @@ -38,7 +52,7 @@ export async function middleware(request: NextRequest) {
});
}

return await serveSiteByURL(request, extracted);
return await serveSiteByURL(requestURL, request, extracted);
}

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

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

const { data } = result;
let cookies: ResponseCookies = {};

//
// Handle redirects
//
if ('redirect' in data) {
// biome-ignore lint/suspicious/noConsole: we want to log the redirect
console.log('redirect', data.redirect);
if (data.target === 'content') {
// For content redirects, we use the linker to redirect the optimal URL
// during development and testing in 'url' mode.
const linker = getLinkerForSiteURL({
siteURL: url,
urlMode: mode,
});

const contentRedirect = new URL(linker.toLinkForContent(data.redirect), request.url);

// Keep the same search params as the original request
// as it might contain a VA token
contentRedirect.search = request.nextUrl.search;

return NextResponse.redirect(contentRedirect);
}

return NextResponse.redirect(data.redirect);
}

cookies = {
...cookies,
...getResponseCookiesForVisitorAuth(data.basePath, visitorToken),
};

//
// Make sure the URL is clean of any va token after a successful lookup
// The token is stored in a cookie that is set on the redirect response
//
const requestURLWithoutToken = normalizeVisitorAuthURL(requestURL);
if (requestURLWithoutToken.toString() !== requestURL.toString()) {
return writeResponseCookies(
NextResponse.redirect(requestURLWithoutToken.toString()),
cookies
);
}

//
// Render and serve the content
//

// When visitor has authentication (adaptive content or VA), we serve dynamic routes.
let routeType = visitorToken ? 'dynamic' : 'static';

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

const siteURL = `${url.host}${data.basePath}`;
const siteURLWithoutProtocol = `${url.host}${data.basePath}`;

const route = [
'sites',
routeType,
mode,
encodeURIComponent(siteURL),
encodeURIComponent(siteURLWithoutProtocol),
encodePathInSiteContent(data.pathname),
].join('/');

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

if (visitorToken) {
const cookies = getResponseCookiesForVisitorAuth(data.basePath, visitorToken);
for (const [key, value] of Object.entries(cookies)) {
response.cookies.set(key, value.value, value.options);
}
}

return response;
return writeResponseCookies(response, cookies);
}

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

return url;
}

/**
* Write the cookies to a response.
*/
function writeResponseCookies<R extends NextResponse>(response: R, cookies: ResponseCookies): R {
Object.entries(cookies).forEach(([key, { value, options }]) => {
response.cookies.set(key, value, options);
});

return response;
}
23 changes: 14 additions & 9 deletions packages/gitbook/e2e/internal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ const testCases: TestsCase[] = [
.click();

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

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

// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/2.0/reference/api-reference/pets?fallback=true'
await page.waitForURL((url) =>
url.pathname.includes(
'api-multi-versions-va/2.0/reference/api-reference/pets'
)
);
},
},
Expand Down Expand Up @@ -244,9 +248,7 @@ const testCases: TestsCase[] = [
const sectionGroupDropdown = await page.getByText('Test Section Group 1');
await sectionGroupDropdown.hover();
await page.getByText('Section B').click();
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/sections/sections-4'
);
await page.waitForURL((url) => url.pathname.includes('/sections/sections-4'));
},
},
],
Expand Down Expand Up @@ -820,6 +822,9 @@ const testCases: TestsCase[] = [
expiresIn: '24h',
}
);

// Test that when accessing the non-canonical URL, we are redirected to the canonical URL
// with the jwt token in the query string
return `spacea?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
Expand Down
Loading