Skip to content

Commit 29bab79

Browse files
authored
v2: PDF export for sites and spaces (#2949)
1 parent 8d5de97 commit 29bab79

File tree

38 files changed

+970
-600
lines changed

38 files changed

+970
-600
lines changed

.github/workflows/deploy-preview.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ jobs:
190190
- name: Run Playwright tests
191191
run: bun e2e
192192
env:
193-
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
193+
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
194+
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
194195
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
195196
ARGOS_BUILD_NAME: 'v2-vercel'
196197
visual-testing-customers-v1:
@@ -230,7 +231,8 @@ jobs:
230231
- name: Run Playwright tests
231232
run: bun e2e-customers
232233
env:
233-
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
234+
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
235+
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
234236
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
235237
ARGOS_BUILD_NAME: 'customers-v2'
236238
pagespeed-testing-v1:
@@ -247,7 +249,7 @@ jobs:
247249
env:
248250
PUPPETEER_SKIP_DOWNLOAD: 1
249251
- name: Run pagespeed tests
250-
run: bun ./packages/gitbook/tests/pagespeed-testing.ts $DEPLOYMENT_URL
252+
run: bun ./packages/gitbook/tests/pagespeed-testing.ts
251253
env:
252-
DEPLOYMENT_URL: ${{needs.deploy-v1-cloudflare.outputs.deployment-url}}
254+
BASE_URL: ${{needs.deploy-v1-cloudflare.outputs.deployment-url}}
253255
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PDFRootLayout } from '@/components/PDF';
2+
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
3+
4+
export default async function RootLayout(props: {
5+
params: Promise<RouteLayoutParams>;
6+
children: React.ReactNode;
7+
}) {
8+
const { params, children } = props;
9+
const context = await getDynamicSiteContext(await params);
10+
11+
return <PDFRootLayout context={context}>{children}</PDFRootLayout>;
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { PDFPage, generatePDFMetadata } from '@/components/PDF';
2+
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
3+
4+
export async function generateMetadata({
5+
params,
6+
}: {
7+
params: Promise<RouteLayoutParams>;
8+
}) {
9+
const context = await getDynamicSiteContext(await params);
10+
return generatePDFMetadata(context);
11+
}
12+
13+
export default async function Page(props: {
14+
params: Promise<RouteLayoutParams>;
15+
searchParams: Promise<{ [key: string]: string }>;
16+
}) {
17+
const { params, searchParams } = props;
18+
const context = await getDynamicSiteContext(await params);
19+
return <PDFPage context={context} searchParams={await searchParams} />;
20+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
type GitBookBaseContext,
3+
type GitBookSpaceContext,
4+
fetchSpaceContextByIds,
5+
} from '@v2/lib/context';
6+
import { createDataFetcher } from '@v2/lib/data';
7+
import { GITBOOK_API_URL } from '@v2/lib/env';
8+
import { createLinker } from '@v2/lib/links';
9+
import { getAPITokenFromMiddleware } from '@v2/lib/middleware';
10+
11+
export type SpacePDFRouteParams = {
12+
spaceId: string;
13+
changeRequestId?: string;
14+
revisionId?: string;
15+
};
16+
17+
export async function getSpacePDFContext(
18+
params: SpacePDFRouteParams
19+
): Promise<GitBookSpaceContext> {
20+
const { spaceId } = params;
21+
22+
const apiToken = await getAPITokenFromMiddleware();
23+
24+
const linker = createLinker({
25+
pathname: getPDFRoutePath(params),
26+
});
27+
const dataFetcher = createDataFetcher({
28+
apiToken: apiToken,
29+
apiEndpoint: GITBOOK_API_URL,
30+
});
31+
32+
const baseContext: GitBookBaseContext = {
33+
linker,
34+
dataFetcher,
35+
};
36+
37+
return await fetchSpaceContextByIds(baseContext, {
38+
space: spaceId,
39+
shareKey: undefined,
40+
changeRequest: params.changeRequestId,
41+
revision: params.revisionId,
42+
});
43+
}
44+
45+
function getPDFRoutePath(params: SpacePDFRouteParams) {
46+
let path = `/~space/${params.spaceId}`;
47+
48+
if (params.changeRequestId) {
49+
path += `/~/changes/${params.changeRequestId}`;
50+
}
51+
52+
if (params.revisionId) {
53+
path += `/~/revisions/${params.revisionId}`;
54+
}
55+
56+
path += '~gitbook/pdf';
57+
58+
return path;
59+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import RootLayout from '@v2/app/~space/[spaceId]/~gitbook/pdf/layout';
2+
export default RootLayout;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import PDFPage, { generateMetadata } from '@v2/app/~space/[spaceId]/~gitbook/pdf/page';
2+
3+
export default PDFPage;
4+
export { generateMetadata };
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import RootLayout from '@v2/app/~space/[spaceId]/~gitbook/pdf/layout';
2+
export default RootLayout;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import PDFPage, { generateMetadata } from '@v2/app/~space/[spaceId]/~gitbook/pdf/page';
2+
3+
export default PDFPage;
4+
export { generateMetadata };
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PDFRootLayout } from '@/components/PDF';
2+
import { type SpacePDFRouteParams, getSpacePDFContext } from '@v2/app/~space/[spaceId]/pdf';
3+
4+
export default async function RootLayout(props: {
5+
params: Promise<SpacePDFRouteParams>;
6+
children: React.ReactNode;
7+
}) {
8+
const { params, children } = props;
9+
const context = await getSpacePDFContext(await params);
10+
11+
return <PDFRootLayout context={context}>{children}</PDFRootLayout>;
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { PDFPage, generatePDFMetadata } from '@/components/PDF';
2+
import { type SpacePDFRouteParams, getSpacePDFContext } from '@v2/app/~space/[spaceId]/pdf';
3+
4+
export async function generateMetadata({
5+
params,
6+
}: {
7+
params: Promise<SpacePDFRouteParams>;
8+
}) {
9+
const context = await getSpacePDFContext(await params);
10+
return generatePDFMetadata(context);
11+
}
12+
13+
export default async function Page(props: {
14+
params: Promise<SpacePDFRouteParams>;
15+
searchParams: Promise<{ [key: string]: string }>;
16+
}) {
17+
const { params, searchParams } = props;
18+
const context = await getSpacePDFContext(await params);
19+
return <PDFPage context={context} searchParams={await searchParams} />;
20+
}

packages/gitbook-v2/src/lib/data/errors.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ export async function wrapDataFetcherError<T>(
9393
}
9494
}
9595

96-
function getExposableError(error: Error): DataFetcherErrorData {
96+
/**
97+
* Get a data fetcher exposable error from a JS error.
98+
*/
99+
export function getExposableError(error: Error): DataFetcherErrorData {
97100
if (error instanceof GitBookAPIError) {
98101
return {
99102
code: error.code,

packages/gitbook-v2/src/lib/data/lookup.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { race, tryCatch } from '@/lib/async';
22
import { joinPath } from '@/lib/paths';
3-
import { GitBookAPI, type GitBookAPIError, type PublishedSiteContentLookup } from '@gitbook/api';
3+
import { GitBookAPI, type PublishedSiteContentLookup } from '@gitbook/api';
44
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
5+
import { getExposableError } from './errors';
6+
import type { DataFetcherResponse } from './types';
57
import { getURLLookupAlternatives, stripURLSearch } from './urls';
68

79
/**
@@ -12,10 +14,7 @@ export async function getPublishedContentByURL(input: {
1214
url: string;
1315
visitorAuthToken: string | null;
1416
redirectOnError: boolean;
15-
}): Promise<
16-
| { data: PublishedSiteContentLookup; error?: undefined }
17-
| { data?: undefined; error: Error | GitBookAPIError }
18-
> {
17+
}): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
1918
const lookupURL = new URL(input.url);
2019
const url = stripURLSearch(lookupURL);
2120
const lookup = getURLLookupAlternatives(url);
@@ -27,6 +26,7 @@ export async function getPublishedContentByURL(input: {
2726
userAgent: GITBOOK_USER_AGENT,
2827
});
2928

29+
const startTime = performance.now();
3030
const callResult = await tryCatch(
3131
api.urls.getPublishedContentByUrl(
3232
{
@@ -43,6 +43,12 @@ export async function getPublishedContentByURL(input: {
4343
}
4444
)
4545
);
46+
const endTime = performance.now();
47+
48+
// biome-ignore lint/suspicious/noConsole: we want to log performance data
49+
console.log(
50+
`getPublishedContentByURL(${alternative.url}) Time taken: ${endTime - startTime}ms`
51+
);
4652

4753
if (callResult.error) {
4854
if (alternative.primary) {
@@ -110,9 +116,20 @@ export async function getPublishedContentByURL(input: {
110116

111117
if (!result) {
112118
return {
113-
error: new Error('No content found'),
119+
error: {
120+
code: 404,
121+
message: 'No content found',
122+
},
123+
};
124+
}
125+
126+
if (result.error) {
127+
return {
128+
error: getExposableError(result.error),
114129
};
115130
}
116131

117-
return result;
132+
return {
133+
data: result.data,
134+
};
118135
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ export enum MiddlewareHeaders {
3636
* The visitor token used to access this content
3737
*/
3838
VisitorAuthToken = 'x-gitbook-visitor-token',
39+
40+
/**
41+
* The API token used to fetch the content.
42+
* This should only be passed for non-site dynamic routes.
43+
*/
44+
APIToken = 'x-gitbook-api-token',
3945
}
4046

4147
/**
@@ -112,3 +118,17 @@ export async function getVisitorAuthTokenFromMiddleware(): Promise<string | null
112118

113119
return visitorAuthToken;
114120
}
121+
122+
/**
123+
* Get the API token from the middleware headers.
124+
* This function should only be called in a dynamic route.
125+
*/
126+
export async function getAPITokenFromMiddleware(): Promise<string> {
127+
const headersList = await headers();
128+
const apiToken = headersList.get(MiddlewareHeaders.APIToken);
129+
if (!apiToken) {
130+
throw new Error('API token is not set by the middleware');
131+
}
132+
133+
return apiToken;
134+
}

0 commit comments

Comments
 (0)