Skip to content

Commit 2f28cc1

Browse files
authored
v2: improve link and navigation between content in a same site (#3020)
1 parent f5516d4 commit 2f28cc1

22 files changed

+167
-184
lines changed

packages/gitbook-v2/next.config.mjs

+9
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,17 @@
55
*/
66
const nextConfig = {
77
experimental: {
8+
// This is needed to throw "forbidden" when the api token expired during revalidation
89
authInterrupts: true,
10+
11+
// This is needed to use 'use cache'
912
useCache: true,
13+
14+
// Content is fully static, we can cache it in the session memory cache for a long time
15+
staleTimes: {
16+
dynamic: 3600, // 1 hour
17+
static: 3600, // 1 hour
18+
},
1019
},
1120

1221
env: {

packages/gitbook-v2/src/app/utils.ts

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export async function getStaticSiteContext(params: RouteLayoutParams) {
3838
const context = await fetchSiteContextByURLLookup(
3939
getBaseContext({
4040
siteURL,
41+
siteURLData,
4142
urlMode: getModeFromParams(params.mode),
4243
}),
4344
siteURLData
@@ -60,6 +61,7 @@ export async function getDynamicSiteContext(params: RouteLayoutParams) {
6061
const context = await fetchSiteContextByURLLookup(
6162
getBaseContext({
6263
siteURL,
64+
siteURLData,
6365
urlMode: getModeFromParams(params.mode),
6466
}),
6567
siteURLData

packages/gitbook-v2/src/app/~space/[spaceId]/pdf.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ export async function getSpacePDFContext(
2020

2121
const apiToken = await getAPITokenFromMiddleware();
2222

23+
const basePath = getPDFRoutePath(params);
2324
const linker = createLinker({
24-
pathname: getPDFRoutePath(params),
25+
spaceBasePath: basePath,
26+
siteBasePath: basePath,
2527
});
2628
const dataFetcher = createDataFetcher({
2729
apiToken: apiToken,

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

+35-64
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getSiteStructureSections } from '@/lib/sites';
22
import type {
33
ChangeRequest,
4-
PublishedSiteContentLookup,
4+
PublishedSiteContent,
55
RevisionPage,
66
RevisionPageDocument,
77
Site,
@@ -14,11 +14,10 @@ import type {
1414
Space,
1515
} from '@gitbook/api';
1616
import { type GitBookDataFetcher, createDataFetcher, throwIfDataError } from '@v2/lib/data';
17-
import { redirect } from 'next/navigation';
1817
import { assert } from 'ts-essentials';
1918
import { GITBOOK_URL } from './env';
2019
import { type ImageResizer, createImageResizer } from './images';
21-
import { type GitBookSpaceLinker, createLinker } from './links';
20+
import { type GitBookLinker, createLinker } from './links';
2221

2322
/**
2423
* Generic context when rendering content.
@@ -32,7 +31,7 @@ export type GitBookBaseContext = {
3231
/**
3332
* Linker to generate links in the current space.
3433
*/
35-
linker: GitBookSpaceLinker;
34+
linker: GitBookLinker;
3635

3736
/**
3837
* Image resizer to resize images.
@@ -102,59 +101,33 @@ export type GitBookPageContext = (GitBookSpaceContext | GitBookSiteContext) & {
102101
};
103102

104103
/**
105-
* Get the base context for a request.
104+
* Get the base context for a request on a site.
106105
*/
107106
export function getBaseContext(input: {
108107
siteURL: URL | string;
108+
siteURLData: PublishedSiteContent;
109109
urlMode: 'url' | 'url-host';
110-
apiToken?: string | null;
111110
}) {
112-
const url = typeof input.siteURL === 'string' ? new URL(input.siteURL) : input.siteURL;
113-
const urlMode = input.urlMode;
111+
const { urlMode, siteURLData } = input;
112+
const siteURL = typeof input.siteURL === 'string' ? new URL(input.siteURL) : input.siteURL;
114113

115114
const dataFetcher = createDataFetcher({
116-
apiToken: input.apiToken ?? null,
115+
apiToken: siteURLData.apiToken ?? null,
117116
});
118117

119-
const linker = getLinkerForSiteURL({
120-
siteURL: url,
121-
urlMode,
122-
});
123-
124-
const imageResizer = createImageResizer({
125-
host: url.host,
126-
// To ensure image resizing work for proxied sites,
127-
// we serve images from the root of the site.
128-
linker: linker,
129-
});
130-
131-
return {
132-
dataFetcher,
133-
linker,
134-
imageResizer,
135-
};
136-
}
137-
138-
/**
139-
* Get the linker for a given site URL.
140-
*/
141-
export function getLinkerForSiteURL(input: {
142-
siteURL: URL;
143-
urlMode: 'url' | 'url-host';
144-
}) {
145-
const { siteURL, urlMode } = input;
146-
147118
const gitbookURL = GITBOOK_URL ? new URL(GITBOOK_URL) : undefined;
148119
const linker =
149120
urlMode === 'url-host'
150121
? createLinker({
151122
host: siteURL.host,
152-
pathname: siteURL.pathname,
123+
siteBasePath: siteURLData.siteBasePath,
124+
spaceBasePath: siteURLData.basePath,
153125
})
154126
: createLinker({
155127
protocol: gitbookURL?.protocol,
156128
host: gitbookURL?.host,
157-
pathname: `/url/${siteURL.host}${siteURL.pathname}`,
129+
siteBasePath: `/url/${siteURL.host}${siteURLData.siteBasePath}`,
130+
spaceBasePath: `/url/${siteURL.host}${siteURLData.basePath}`,
158131
});
159132

160133
if (urlMode === 'url') {
@@ -165,39 +138,37 @@ export function getLinkerForSiteURL(input: {
165138
};
166139
}
167140

168-
return linker;
141+
const imageResizer = createImageResizer({
142+
host: siteURL.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+
};
169153
}
170154

171155
/**
172156
* Fetch the context of a site using the resolution of a URL
173157
*/
174158
export async function fetchSiteContextByURLLookup(
175159
baseContext: GitBookBaseContext,
176-
data: PublishedSiteContentLookup
160+
data: PublishedSiteContent
177161
): Promise<GitBookSiteContext> {
178-
const { dataFetcher } = baseContext;
179-
if ('redirect' in data) {
180-
redirect(data.redirect);
181-
}
182-
183-
return await fetchSiteContextByIds(
184-
{
185-
...baseContext,
186-
dataFetcher: dataFetcher.withToken({
187-
apiToken: data.apiToken,
188-
}),
189-
},
190-
{
191-
organization: data.organization,
192-
site: data.site,
193-
siteSection: data.siteSection,
194-
siteSpace: data.siteSpace,
195-
space: data.space,
196-
shareKey: data.shareKey,
197-
changeRequest: data.changeRequest,
198-
revision: data.revision,
199-
}
200-
);
162+
return await fetchSiteContextByIds(baseContext, {
163+
organization: data.organization,
164+
site: data.site,
165+
siteSection: data.siteSection,
166+
siteSpace: data.siteSpace,
167+
space: data.space,
168+
shareKey: data.shareKey,
169+
changeRequest: data.changeRequest,
170+
revision: data.revision,
171+
});
201172
}
202173

203174
/**

packages/gitbook-v2/src/lib/images/createImageResizer.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'server-only';
22

33
import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env';
4-
import type { GitBookSpaceLinker } from '../links';
4+
import type { GitBookLinker } from '../links';
55
import { type SignatureVersion, generateImageSignature } from './signatures';
66
import type { ImageResizer } from './types';
77

@@ -37,7 +37,7 @@ export function createImageResizer({
3737
linker,
3838
}: {
3939
/** The linker to use to create URLs. */
40-
linker: GitBookSpaceLinker;
40+
linker: GitBookLinker;
4141
/** The host name of the current site. */
4242
host: string;
4343
}): ImageResizer {
@@ -62,7 +62,7 @@ export function createImageResizer({
6262
url: urlInput,
6363
});
6464

65-
const url = linker.toAbsoluteURL(linker.toPathInContent('/~gitbook/image'));
65+
const url = linker.toAbsoluteURL(linker.toPathInSite('/~gitbook/image'));
6666
const searchParams = new URLSearchParams();
6767
searchParams.set('url', getImageAPIUrl(urlInput));
6868

+32-27
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
11
import { describe, expect, it } from 'bun:test';
2-
import { appendBasePathToLinker, createLinker } from './links';
2+
import { createLinker } from './links';
33

44
const root = createLinker({
55
host: 'docs.company.com',
6-
pathname: '/',
6+
spaceBasePath: '/',
7+
siteBasePath: '/',
78
});
89

910
const variantInSection = createLinker({
1011
host: 'docs.company.com',
11-
pathname: '/section/variant',
12+
spaceBasePath: '/section/variant',
13+
siteBasePath: '/',
14+
});
15+
16+
const siteGitBookIO = createLinker({
17+
host: 'org.gitbook.io',
18+
spaceBasePath: '/sitename/variant/',
19+
siteBasePath: '/sitename/',
1220
});
1321

1422
describe('toPathInContent', () => {
1523
it('should return the correct path', () => {
16-
expect(root.toPathInContent('some/path')).toBe('/some/path');
17-
expect(variantInSection.toPathInContent('some/path')).toBe('/section/variant/some/path');
24+
expect(root.toPathInSpace('some/path')).toBe('/some/path');
25+
expect(variantInSection.toPathInSpace('some/path')).toBe('/section/variant/some/path');
1826
});
1927

2028
it('should handle leading slash', () => {
21-
expect(root.toPathInContent('/some/path')).toBe('/some/path');
22-
expect(variantInSection.toPathInContent('/some/path')).toBe('/section/variant/some/path');
29+
expect(root.toPathInSpace('/some/path')).toBe('/some/path');
30+
expect(variantInSection.toPathInSpace('/some/path')).toBe('/section/variant/some/path');
31+
});
32+
});
33+
34+
describe('toPathInSite', () => {
35+
it('should return the correct path', () => {
36+
expect(root.toPathInSite('some/path')).toBe('/some/path');
37+
expect(siteGitBookIO.toPathInSite('some/path')).toBe('/sitename/some/path');
2338
});
2439
});
2540

@@ -32,27 +47,17 @@ describe('toAbsoluteURL', () => {
3247
});
3348
});
3449

35-
describe('appendBasePathToLinker', () => {
36-
const prefixedRoot = appendBasePathToLinker(root, '/section/variant');
37-
const prefixedVariantInSection = appendBasePathToLinker(variantInSection, '/base');
38-
39-
describe('toPathInContent', () => {
40-
it('should return the correct path', () => {
41-
expect(prefixedRoot.toPathInContent('some/path')).toBe('/section/variant/some/path');
42-
expect(prefixedVariantInSection.toPathInContent('some/path')).toBe(
43-
'/section/variant/base/some/path'
44-
);
45-
});
50+
describe('toLinkForContent', () => {
51+
it('should return the correct path', () => {
52+
expect(root.toLinkForContent('https://docs.company.com/some/path')).toBe('/some/path');
53+
expect(siteGitBookIO.toLinkForContent('https://org.gitbook.io/sitename/some/path')).toBe(
54+
'/sitename/some/path'
55+
);
4656
});
4757

48-
describe('toAbsoluteURL', () => {
49-
it('should return the correct path', () => {
50-
expect(prefixedRoot.toAbsoluteURL('some/path')).toBe(
51-
'https://docs.company.com/some/path'
52-
);
53-
expect(prefixedVariantInSection.toAbsoluteURL('some/path')).toBe(
54-
'https://docs.company.com/some/path'
55-
);
56-
});
58+
it('should preserve an absolute URL if the site is not the same', () => {
59+
expect(siteGitBookIO.toLinkForContent('https://org.gitbook.io/anothersite/some/path')).toBe(
60+
'https://org.gitbook.io/anothersite/some/path'
61+
);
5762
});
5863
});

0 commit comments

Comments
 (0)