Skip to content

v2: PDF export for sites and spaces #2949

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Mar 9, 2025
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
10 changes: 6 additions & 4 deletions .github/workflows/deploy-preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ jobs:
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'v2-vercel'
visual-testing-customers-v1:
Expand Down Expand Up @@ -230,7 +231,8 @@ jobs:
- name: Run Playwright tests
run: bun e2e-customers
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'customers-v2'
pagespeed-testing-v1:
Expand All @@ -247,7 +249,7 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run pagespeed tests
run: bun ./packages/gitbook/tests/pagespeed-testing.ts $DEPLOYMENT_URL
run: bun ./packages/gitbook/tests/pagespeed-testing.ts
env:
DEPLOYMENT_URL: ${{needs.deploy-v1-cloudflare.outputs.deployment-url}}
BASE_URL: ${{needs.deploy-v1-cloudflare.outputs.deployment-url}}
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PDFRootLayout } from '@/components/PDF';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';

export default async function RootLayout(props: {
params: Promise<RouteLayoutParams>;
children: React.ReactNode;
}) {
const { params, children } = props;
const context = await getDynamicSiteContext(await params);

return <PDFRootLayout context={context}>{children}</PDFRootLayout>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PDFPage, generatePDFMetadata } from '@/components/PDF';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';

export async function generateMetadata({
params,
}: {
params: Promise<RouteLayoutParams>;
}) {
const context = await getDynamicSiteContext(await params);
return generatePDFMetadata(context);
}

export default async function Page(props: {
params: Promise<RouteLayoutParams>;
searchParams: Promise<{ [key: string]: string }>;
}) {
const { params, searchParams } = props;
const context = await getDynamicSiteContext(await params);
return <PDFPage context={context} searchParams={await searchParams} />;
}
59 changes: 59 additions & 0 deletions packages/gitbook-v2/src/app/~space/[spaceId]/pdf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
type GitBookBaseContext,
type GitBookSpaceContext,
fetchSpaceContextByIds,
} from '@v2/lib/context';
import { createDataFetcher } from '@v2/lib/data';
import { GITBOOK_API_URL } from '@v2/lib/env';
import { createLinker } from '@v2/lib/links';
import { getAPITokenFromMiddleware } from '@v2/lib/middleware';

export type SpacePDFRouteParams = {
spaceId: string;
changeRequestId?: string;
revisionId?: string;
};

export async function getSpacePDFContext(
params: SpacePDFRouteParams
): Promise<GitBookSpaceContext> {
const { spaceId } = params;

const apiToken = await getAPITokenFromMiddleware();

const linker = createLinker({
pathname: getPDFRoutePath(params),
});
const dataFetcher = createDataFetcher({
apiToken: apiToken,
apiEndpoint: GITBOOK_API_URL,
});

const baseContext: GitBookBaseContext = {
linker,
dataFetcher,
};

return await fetchSpaceContextByIds(baseContext, {
space: spaceId,
shareKey: undefined,
changeRequest: params.changeRequestId,
revision: params.revisionId,
});
}

function getPDFRoutePath(params: SpacePDFRouteParams) {
let path = `/~space/${params.spaceId}`;

if (params.changeRequestId) {
path += `/~/changes/${params.changeRequestId}`;
}

if (params.revisionId) {
path += `/~/revisions/${params.revisionId}`;
}

path += '~gitbook/pdf';

return path;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import RootLayout from '@v2/app/~space/[spaceId]/~gitbook/pdf/layout';
export default RootLayout;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import PDFPage, { generateMetadata } from '@v2/app/~space/[spaceId]/~gitbook/pdf/page';

export default PDFPage;
export { generateMetadata };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import RootLayout from '@v2/app/~space/[spaceId]/~gitbook/pdf/layout';
export default RootLayout;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import PDFPage, { generateMetadata } from '@v2/app/~space/[spaceId]/~gitbook/pdf/page';

export default PDFPage;
export { generateMetadata };
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PDFRootLayout } from '@/components/PDF';
import { type SpacePDFRouteParams, getSpacePDFContext } from '@v2/app/~space/[spaceId]/pdf';

export default async function RootLayout(props: {
params: Promise<SpacePDFRouteParams>;
children: React.ReactNode;
}) {
const { params, children } = props;
const context = await getSpacePDFContext(await params);

return <PDFRootLayout context={context}>{children}</PDFRootLayout>;
}
20 changes: 20 additions & 0 deletions packages/gitbook-v2/src/app/~space/[spaceId]/~gitbook/pdf/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PDFPage, generatePDFMetadata } from '@/components/PDF';
import { type SpacePDFRouteParams, getSpacePDFContext } from '@v2/app/~space/[spaceId]/pdf';

export async function generateMetadata({
params,
}: {
params: Promise<SpacePDFRouteParams>;
}) {
const context = await getSpacePDFContext(await params);
return generatePDFMetadata(context);
}

export default async function Page(props: {
params: Promise<SpacePDFRouteParams>;
searchParams: Promise<{ [key: string]: string }>;
}) {
const { params, searchParams } = props;
const context = await getSpacePDFContext(await params);
return <PDFPage context={context} searchParams={await searchParams} />;
}
5 changes: 4 additions & 1 deletion packages/gitbook-v2/src/lib/data/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ export async function wrapDataFetcherError<T>(
}
}

function getExposableError(error: Error): DataFetcherErrorData {
/**
* Get a data fetcher exposable error from a JS error.
*/
export function getExposableError(error: Error): DataFetcherErrorData {
if (error instanceof GitBookAPIError) {
return {
code: error.code,
Expand Down
31 changes: 24 additions & 7 deletions packages/gitbook-v2/src/lib/data/lookup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { race, tryCatch } from '@/lib/async';
import { joinPath } from '@/lib/paths';
import { GitBookAPI, type GitBookAPIError, type PublishedSiteContentLookup } from '@gitbook/api';
import { GitBookAPI, type PublishedSiteContentLookup } from '@gitbook/api';
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
import { getExposableError } from './errors';
import type { DataFetcherResponse } from './types';
import { getURLLookupAlternatives, stripURLSearch } from './urls';

/**
Expand All @@ -12,10 +14,7 @@ export async function getPublishedContentByURL(input: {
url: string;
visitorAuthToken: string | null;
redirectOnError: boolean;
}): Promise<
| { data: PublishedSiteContentLookup; error?: undefined }
| { data?: undefined; error: Error | GitBookAPIError }
> {
}): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
const lookupURL = new URL(input.url);
const url = stripURLSearch(lookupURL);
const lookup = getURLLookupAlternatives(url);
Expand All @@ -27,6 +26,7 @@ export async function getPublishedContentByURL(input: {
userAgent: GITBOOK_USER_AGENT,
});

const startTime = performance.now();
const callResult = await tryCatch(
api.urls.getPublishedContentByUrl(
{
Expand All @@ -43,6 +43,12 @@ export async function getPublishedContentByURL(input: {
}
)
);
const endTime = performance.now();

// biome-ignore lint/suspicious/noConsole: we want to log performance data
console.log(
`getPublishedContentByURL(${alternative.url}) Time taken: ${endTime - startTime}ms`
);

if (callResult.error) {
if (alternative.primary) {
Expand Down Expand Up @@ -110,9 +116,20 @@ export async function getPublishedContentByURL(input: {

if (!result) {
return {
error: new Error('No content found'),
error: {
code: 404,
message: 'No content found',
},
};
}

if (result.error) {
return {
error: getExposableError(result.error),
};
}

return result;
return {
data: result.data,
};
}
20 changes: 20 additions & 0 deletions packages/gitbook-v2/src/lib/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export enum MiddlewareHeaders {
* The visitor token used to access this content
*/
VisitorAuthToken = 'x-gitbook-visitor-token',

/**
* The API token used to fetch the content.
* This should only be passed for non-site dynamic routes.
*/
APIToken = 'x-gitbook-api-token',
}

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

return visitorAuthToken;
}

/**
* Get the API token from the middleware headers.
* This function should only be called in a dynamic route.
*/
export async function getAPITokenFromMiddleware(): Promise<string> {
const headersList = await headers();
const apiToken = headersList.get(MiddlewareHeaders.APIToken);
if (!apiToken) {
throw new Error('API token is not set by the middleware');
}

return apiToken;
}
Loading