mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-23 19:06:31 +00:00
PDF export (#72)
* Start * Improve generation * Improve style * Lint * Use tilde instead of dot in the url * Improve style * Fix page group * Format * Add visual tests for PDF
This commit is contained in:
@@ -1,172 +0,0 @@
|
||||
import { CustomizationHeaderPreset, CustomizationSettings } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
import Script from 'next/script';
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
import { fonts, ibmPlexMono } from '@/fonts';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { getSpaceContent } from '@/lib/api';
|
||||
import { hexToRgb, shadesOfColor } from '@/lib/colors';
|
||||
import { getContentSecurityPolicyNonce } from '@/lib/csp';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { ClientContexts } from './ClientContexts';
|
||||
import { PagePathParams } from '../fetch';
|
||||
|
||||
export default async function SpaceRootLayout(props: {
|
||||
children: React.ReactNode;
|
||||
params: PagePathParams;
|
||||
}) {
|
||||
const { params, children } = props;
|
||||
|
||||
const { customization, scripts } = await getSpaceContent({
|
||||
spaceId: params.spaceId,
|
||||
});
|
||||
const headerTheme = generateHeaderTheme(customization);
|
||||
const nonce = getContentSecurityPolicyNonce();
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={customization.internationalization.locale}
|
||||
className={tcls(
|
||||
customization.header.preset === CustomizationHeaderPreset.None
|
||||
? null
|
||||
: [
|
||||
// Take the sticky header in consideration for the scrolling
|
||||
`scroll-pt-[76px]`,
|
||||
],
|
||||
)}
|
||||
>
|
||||
<head>
|
||||
{customization.privacyPolicy.url ? (
|
||||
<link rel="privacy-policy" href={customization.privacyPolicy.url} />
|
||||
) : null}
|
||||
<style
|
||||
nonce={
|
||||
//Since I can't get the nonce to work for inline styles, we need to allow unsafe-inline
|
||||
undefined
|
||||
}
|
||||
>{`
|
||||
:root {
|
||||
${generateColorVariable(
|
||||
'primary-color',
|
||||
customization.styling.primaryColor.light,
|
||||
)}
|
||||
${generateColorVariable(
|
||||
'header-background',
|
||||
headerTheme.backgroundColor.light,
|
||||
)}
|
||||
${generateColorVariable('header-link', headerTheme.linkColor.light)}
|
||||
${generateColorVariable('yellow', '#f4e28d')}
|
||||
${generateColorVariable('teal', '#3f89a1')}
|
||||
${generateColorVariable('pomegranate', '#f25b3a')}
|
||||
}
|
||||
.dark {
|
||||
${generateColorVariable(
|
||||
'primary-color',
|
||||
customization.styling.primaryColor.dark,
|
||||
)}
|
||||
${generateColorVariable(
|
||||
'header-background',
|
||||
headerTheme.backgroundColor.dark,
|
||||
)}
|
||||
${generateColorVariable('header-link', headerTheme.linkColor.dark)}
|
||||
}
|
||||
`}</style>
|
||||
</head>
|
||||
<body
|
||||
className={tcls(
|
||||
`${fonts[customization.styling.font].className}`,
|
||||
`${ibmPlexMono.variable}`,
|
||||
'bg-light',
|
||||
'dark:bg-dark',
|
||||
)}
|
||||
>
|
||||
<ClientContexts language={language}>{children}</ClientContexts>
|
||||
|
||||
{scripts.map(({ script }) => (
|
||||
<Script key={script} src={script} strategy="lazyOnload" nonce={nonce} />
|
||||
))}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
type ColorInput = string | Record<string, string>;
|
||||
function generateColorVariable(name: string, color: ColorInput) {
|
||||
const shades: Record<string, string> = typeof color === 'string' ? shadesOfColor(color) : color;
|
||||
|
||||
return Object.entries(shades)
|
||||
.map(([key, value]) => {
|
||||
// Check the original hex value
|
||||
const rgbValue = hexToRgb(value);
|
||||
return `--${name}-${key}: ${rgbValue};`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function generateHeaderTheme(customization: CustomizationSettings): {
|
||||
backgroundColor: { light: ColorInput; dark: ColorInput };
|
||||
linkColor: { light: ColorInput; dark: ColorInput };
|
||||
} {
|
||||
switch (customization.header.preset) {
|
||||
case CustomizationHeaderPreset.None:
|
||||
case CustomizationHeaderPreset.Default: {
|
||||
return {
|
||||
backgroundColor: {
|
||||
light: colors.white,
|
||||
dark: colors.black,
|
||||
},
|
||||
linkColor: {
|
||||
light: customization.styling.primaryColor.light,
|
||||
dark: customization.styling.primaryColor.dark,
|
||||
},
|
||||
};
|
||||
}
|
||||
case CustomizationHeaderPreset.Bold: {
|
||||
return {
|
||||
backgroundColor: {
|
||||
light: customization.styling.primaryColor.light,
|
||||
dark: customization.styling.primaryColor.dark,
|
||||
},
|
||||
linkColor: {
|
||||
// TODO: should depend on the color of the background
|
||||
light: colors.white,
|
||||
dark: colors.black,
|
||||
},
|
||||
};
|
||||
}
|
||||
case CustomizationHeaderPreset.Contrast: {
|
||||
return {
|
||||
backgroundColor: {
|
||||
light: customization.styling.primaryColor.dark,
|
||||
dark: customization.styling.primaryColor.light,
|
||||
},
|
||||
linkColor: {
|
||||
light: colors.white,
|
||||
dark: colors.black,
|
||||
},
|
||||
};
|
||||
}
|
||||
case CustomizationHeaderPreset.Custom: {
|
||||
return {
|
||||
backgroundColor: {
|
||||
light: customization.header.backgroundColor?.light ?? colors.white,
|
||||
dark: customization.header.backgroundColor?.dark ?? colors.black,
|
||||
},
|
||||
linkColor: {
|
||||
light:
|
||||
customization.header.linkColor?.light ??
|
||||
customization.styling.primaryColor.light,
|
||||
dark:
|
||||
customization.header.linkColor?.dark ??
|
||||
customization.styling.primaryColor.dark,
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertNever(customization.header.preset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { CustomizationThemeMode } from '@gitbook/api';
|
||||
import { Metadata, Viewport } from 'next';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import Script from 'next/script';
|
||||
import React from 'react';
|
||||
|
||||
import { CookiesToast } from '@/components/Cookies';
|
||||
import { SpaceContent } from '@/components/SpaceContent';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { getContentSecurityPolicyNonce } from '@/lib/csp';
|
||||
import { PageHrefContext, absoluteHref, baseUrl, pageHref } from '@/lib/links';
|
||||
import { getPagePath } from '@/lib/pages';
|
||||
import { shouldIndexSpace } from '@/lib/seo';
|
||||
|
||||
import { ClientContexts } from './ClientContexts';
|
||||
import { PagePathParams, fetchPageData, getPathnameParam } from '../fetch';
|
||||
|
||||
export const runtime = 'edge';
|
||||
@@ -21,6 +21,7 @@ export const runtime = 'edge';
|
||||
export default async function Page(props: { params: PagePathParams }) {
|
||||
const { params } = props;
|
||||
|
||||
const nonce = getContentSecurityPolicyNonce();
|
||||
const {
|
||||
content,
|
||||
space,
|
||||
@@ -54,6 +55,11 @@ export default async function Page(props: { params: PagePathParams }) {
|
||||
collection={collection}
|
||||
collectionSpaces={collectionSpaces}
|
||||
/>
|
||||
|
||||
{scripts.map(({ script }) => (
|
||||
<Script key={script} src={script} strategy="lazyOnload" nonce={nonce} />
|
||||
))}
|
||||
|
||||
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
|
||||
<React.Suspense fallback={null}>
|
||||
<CookiesToast privacyPolicy={customization.privacyPolicy.url} />
|
||||
@@ -94,14 +100,14 @@ export async function generateMetadata({ params }: { params: PagePathParams }):
|
||||
{
|
||||
url:
|
||||
customIcon?.light ??
|
||||
absoluteHref('.gitbook/icon?size=small&theme=light', true),
|
||||
absoluteHref('~gitbook/icon?size=small&theme=light', true),
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: light)',
|
||||
},
|
||||
{
|
||||
url:
|
||||
customIcon?.dark ??
|
||||
absoluteHref('.gitbook/icon?size=small&theme=dark', true),
|
||||
absoluteHref('~gitbook/icon?size=small&theme=dark', true),
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
@@ -109,7 +115,7 @@ export async function generateMetadata({ params }: { params: PagePathParams }):
|
||||
},
|
||||
openGraph: {
|
||||
images: [
|
||||
customization.socialPreview.url ?? absoluteHref(`.gitbook/ogimage/${page.id}`),
|
||||
customization.socialPreview.url ?? absoluteHref(`~gitbook/ogimage/${page.id}`),
|
||||
],
|
||||
},
|
||||
robots: shouldIndexSpace({ space, collection }) ? 'index, follow' : 'noindex, nofollow',
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
export default function LayoutError(props: {}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>Layout error</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,166 @@
|
||||
import { CustomizationHeaderPreset, CustomizationSettings } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
import { fonts, ibmPlexMono } from '@/fonts';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { getSpaceContent } from '@/lib/api';
|
||||
import { hexToRgb, shadesOfColor } from '@/lib/colors';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { ClientContexts } from './ClientContexts';
|
||||
import { SpaceParams } from './fetch';
|
||||
import './globals.css';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
export default async function SpaceRootLayout(props: {
|
||||
children: React.ReactNode;
|
||||
params: SpaceParams;
|
||||
}) {
|
||||
const { params, children } = props;
|
||||
|
||||
const { customization } = await getSpaceContent({
|
||||
spaceId: params.spaceId,
|
||||
});
|
||||
const headerTheme = generateHeaderTheme(customization);
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={customization.internationalization.locale}
|
||||
className={tcls(
|
||||
customization.header.preset === CustomizationHeaderPreset.None
|
||||
? null
|
||||
: [
|
||||
// Take the sticky header in consideration for the scrolling
|
||||
`scroll-pt-[76px]`,
|
||||
],
|
||||
)}
|
||||
>
|
||||
<head>
|
||||
{customization.privacyPolicy.url ? (
|
||||
<link rel="privacy-policy" href={customization.privacyPolicy.url} />
|
||||
) : null}
|
||||
<style
|
||||
nonce={
|
||||
//Since I can't get the nonce to work for inline styles, we need to allow unsafe-inline
|
||||
undefined
|
||||
}
|
||||
>{`
|
||||
:root {
|
||||
${generateColorVariable(
|
||||
'primary-color',
|
||||
customization.styling.primaryColor.light,
|
||||
)}
|
||||
${generateColorVariable(
|
||||
'header-background',
|
||||
headerTheme.backgroundColor.light,
|
||||
)}
|
||||
${generateColorVariable('header-link', headerTheme.linkColor.light)}
|
||||
${generateColorVariable('yellow', '#f4e28d')}
|
||||
${generateColorVariable('teal', '#3f89a1')}
|
||||
${generateColorVariable('pomegranate', '#f25b3a')}
|
||||
}
|
||||
.dark {
|
||||
${generateColorVariable(
|
||||
'primary-color',
|
||||
customization.styling.primaryColor.dark,
|
||||
)}
|
||||
${generateColorVariable(
|
||||
'header-background',
|
||||
headerTheme.backgroundColor.dark,
|
||||
)}
|
||||
${generateColorVariable('header-link', headerTheme.linkColor.dark)}
|
||||
}
|
||||
`}</style>
|
||||
</head>
|
||||
<body
|
||||
className={tcls(
|
||||
`${fonts[customization.styling.font].className}`,
|
||||
`${ibmPlexMono.variable}`,
|
||||
'bg-light',
|
||||
'dark:bg-dark',
|
||||
)}
|
||||
>
|
||||
<ClientContexts language={language}>{children}</ClientContexts>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
type ColorInput = string | Record<string, string>;
|
||||
function generateColorVariable(name: string, color: ColorInput) {
|
||||
const shades: Record<string, string> = typeof color === 'string' ? shadesOfColor(color) : color;
|
||||
|
||||
return Object.entries(shades)
|
||||
.map(([key, value]) => {
|
||||
// Check the original hex value
|
||||
const rgbValue = hexToRgb(value);
|
||||
return `--${name}-${key}: ${rgbValue};`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function generateHeaderTheme(customization: CustomizationSettings): {
|
||||
backgroundColor: { light: ColorInput; dark: ColorInput };
|
||||
linkColor: { light: ColorInput; dark: ColorInput };
|
||||
} {
|
||||
switch (customization.header.preset) {
|
||||
case CustomizationHeaderPreset.None:
|
||||
case CustomizationHeaderPreset.Default: {
|
||||
return {
|
||||
backgroundColor: {
|
||||
light: colors.white,
|
||||
dark: colors.black,
|
||||
},
|
||||
linkColor: {
|
||||
light: customization.styling.primaryColor.light,
|
||||
dark: customization.styling.primaryColor.dark,
|
||||
},
|
||||
};
|
||||
}
|
||||
case CustomizationHeaderPreset.Bold: {
|
||||
return {
|
||||
backgroundColor: {
|
||||
light: customization.styling.primaryColor.light,
|
||||
dark: customization.styling.primaryColor.dark,
|
||||
},
|
||||
linkColor: {
|
||||
// TODO: should depend on the color of the background
|
||||
light: colors.white,
|
||||
dark: colors.black,
|
||||
},
|
||||
};
|
||||
}
|
||||
case CustomizationHeaderPreset.Contrast: {
|
||||
return {
|
||||
backgroundColor: {
|
||||
light: customization.styling.primaryColor.dark,
|
||||
dark: customization.styling.primaryColor.light,
|
||||
},
|
||||
linkColor: {
|
||||
light: colors.white,
|
||||
dark: colors.black,
|
||||
},
|
||||
};
|
||||
}
|
||||
case CustomizationHeaderPreset.Custom: {
|
||||
return {
|
||||
backgroundColor: {
|
||||
light: customization.header.backgroundColor?.light ?? colors.white,
|
||||
dark: customization.header.backgroundColor?.dark ?? colors.black,
|
||||
},
|
||||
linkColor: {
|
||||
light:
|
||||
customization.header.linkColor?.light ??
|
||||
customization.styling.primaryColor.light,
|
||||
dark:
|
||||
customization.header.linkColor?.dark ??
|
||||
customization.styling.primaryColor.dark,
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertNever(customization.header.preset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function GET(req: NextRequest, { params }: { params: SpaceParams })
|
||||
|
||||
const lines = [
|
||||
`User-agent: *`,
|
||||
'Disallow: /.gitbook/',
|
||||
'Disallow: /~gitbook/',
|
||||
...(shouldIndexSpace({ space, collection })
|
||||
? [`Allow: /`, `Sitemap: ${absoluteHref(`/sitemap.xml`, true)}`]
|
||||
: [`Disallow: /`]),
|
||||
|
||||
+10
-8
@@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { SpaceParams } from '../../fetch';
|
||||
import { getCollection, getSpace, getSpaceCustomization } from '@/lib/api';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getEmojiForCode } from '@/lib/emojis';
|
||||
import { ContentVisibility } from '@gitbook/api';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { NextRequest } from 'next/server';
|
||||
import React from 'react';
|
||||
|
||||
import { getCollection, getSpace, getSpaceCustomization } from '@/lib/api';
|
||||
import { getEmojiForCode } from '@/lib/emojis';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { SpaceParams } from '../../fetch';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { NextRequest } from 'next/server';
|
||||
import React from 'react';
|
||||
|
||||
import { PageIdParams, fetchPageData } from '../../../fetch';
|
||||
|
||||
export const runtime = 'edge';
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
export function OpenPrintDialog(props: {}) {
|
||||
React.useEffect(() => {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
window.print();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { getDocument, getSpace, getRevisionPages, ContentPointer } from '@/lib/api';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { pagePDFContainerId, PageHrefContext } from '@/lib/links';
|
||||
import { DocumentView } from '@/components/DocumentView';
|
||||
|
||||
import { SpaceParams } from '../../fetch';
|
||||
import { Revision, RevisionPageDocument, RevisionPageGroup, Space } from '@gitbook/api';
|
||||
import { notFound } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
|
||||
import { DocumentView } from '@/components/DocumentView';
|
||||
import { getDocument, getSpace, getRevisionPages, ContentPointer } from '@/lib/api';
|
||||
import { pagePDFContainerId, PageHrefContext } from '@/lib/links';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { OpenPrintDialog } from './OpenPrintDialog';
|
||||
import { SpaceParams } from '../../fetch';
|
||||
import './pdf.css';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
@@ -17,6 +20,8 @@ interface PDFSearchParams {
|
||||
page?: string;
|
||||
/** If true, only the `page` is exported, and not its descendant */
|
||||
only?: boolean;
|
||||
/** Limit the number of pages */
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,33 +44,61 @@ export default async function PDFHTMLOutput(props: {
|
||||
getRevisionPages(contentPointer),
|
||||
]);
|
||||
|
||||
const pages = selectPages(rootPages, searchParams).slice(0, 4); // TODO: remove slice
|
||||
const pages = selectPages(rootPages, searchParams).slice(0, searchParams.limit ?? 10);
|
||||
|
||||
const linksContext: PageHrefContext = {
|
||||
pdf: pages.map(({ page }) => page.id),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={tcls(
|
||||
'my-11',
|
||||
'print:my-0',
|
||||
'mx-auto',
|
||||
'max-w-4xl',
|
||||
'w-full',
|
||||
'p-12',
|
||||
'print:p-0',
|
||||
'shadow-xl',
|
||||
'print:shadow-none',
|
||||
'rounded-sm',
|
||||
'bg-white',
|
||||
)}
|
||||
>
|
||||
<SpaceIntro space={space} />
|
||||
{pages.map(({ page, depth }) =>
|
||||
page.type === 'group' ? (
|
||||
<PDFPageGroup key={page.id} space={space} page={page} />
|
||||
) : (
|
||||
<PDFPageDocument
|
||||
key={page.id}
|
||||
space={space}
|
||||
page={page}
|
||||
refContext={{
|
||||
content: contentPointer,
|
||||
space,
|
||||
pages: rootPages,
|
||||
page,
|
||||
...linksContext,
|
||||
}}
|
||||
/>
|
||||
<React.Suspense key={page.id} fallback={null}>
|
||||
<PDFPageDocument
|
||||
space={space}
|
||||
page={page}
|
||||
refContext={{
|
||||
content: contentPointer,
|
||||
space,
|
||||
pages: rootPages,
|
||||
page,
|
||||
...linksContext,
|
||||
}}
|
||||
/>
|
||||
</React.Suspense>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
|
||||
<OpenPrintDialog />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function SpaceIntro(props: { space: Space }) {
|
||||
const { space } = props;
|
||||
|
||||
return (
|
||||
<div className={tcls('flex', 'items-center', 'justify-center', 'py-12')}>
|
||||
<h1 className={tcls('text-6xl', 'font-bold')}>{space.title}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,8 +106,18 @@ async function PDFPageGroup(props: { space: Space; page: RevisionPageGroup }) {
|
||||
const { page } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{page.title}</h1>
|
||||
<div
|
||||
className={tcls(
|
||||
'break-before-page',
|
||||
'mt-10',
|
||||
'print:mt-0',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'py-12',
|
||||
)}
|
||||
>
|
||||
<h1 className={tcls('text-5xl', 'font-bold')}>{page.title}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -89,12 +132,16 @@ async function PDFPageDocument(props: {
|
||||
const document = page.documentId ? await getDocument(space.id, page.documentId) : null;
|
||||
|
||||
return (
|
||||
<div id={pagePDFContainerId(page)}>
|
||||
<h1>{page.title}</h1>
|
||||
<div
|
||||
id={pagePDFContainerId(page)}
|
||||
className={tcls('break-before-page', 'mt-10', 'print:mt-0')}
|
||||
>
|
||||
<h1 className={tcls('text-3xl', 'font-bold')}>{page.title}</h1>
|
||||
{document ? (
|
||||
<DocumentView
|
||||
document={document}
|
||||
style={'mt-6'}
|
||||
blockStyle={['max-w-full']}
|
||||
context={{
|
||||
resolveContentRef: (ref) => resolveContentRef(ref, refContext),
|
||||
getId: (id) => pagePDFContainerId(page, id),
|
||||
@@ -0,0 +1,8 @@
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 60pt 60pt;
|
||||
|
||||
@bottom-right {
|
||||
content: counter(page);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Prevent infinite loops
|
||||
if (url.includes('/.gitbook/image')) {
|
||||
if (url.includes('/~gitbook/image')) {
|
||||
return new Response('Invalid url parameter', { status: 400 });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { revalidateTags } from '@/lib/cache';
|
||||
|
||||
export const runtime = 'edge';
|
||||
@@ -33,17 +33,22 @@ export interface DocumentContextProps {
|
||||
export function DocumentView(
|
||||
props: DocumentContextProps & {
|
||||
document: JSONDocument;
|
||||
|
||||
/** Style passed to the container */
|
||||
style?: ClassValue;
|
||||
|
||||
/** Style passed to all blocks */
|
||||
blockStyle?: ClassValue;
|
||||
},
|
||||
) {
|
||||
const { document, style, context } = props;
|
||||
const { document, style, blockStyle = [], context } = props;
|
||||
|
||||
return (
|
||||
<Blocks
|
||||
nodes={document.nodes}
|
||||
document={document}
|
||||
ancestorBlocks={[]}
|
||||
blockStyle={[]}
|
||||
blockStyle={blockStyle}
|
||||
style={['space-y-6', style]}
|
||||
context={context}
|
||||
/>
|
||||
|
||||
@@ -93,11 +93,11 @@ function LogoFallback(props: HeaderLogoProps) {
|
||||
}
|
||||
: {
|
||||
light: {
|
||||
src: absoluteHref('.gitbook/icon?size=medium&theme=light'),
|
||||
src: absoluteHref('~gitbook/icon?size=medium&theme=light'),
|
||||
size: { width: 256, height: 256 },
|
||||
},
|
||||
dark: {
|
||||
src: absoluteHref('.gitbook/icon?size=medium&theme=dark'),
|
||||
src: absoluteHref('~gitbook/icon?size=medium&theme=dark'),
|
||||
size: { width: 256, height: 256 },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ export function PageBody(props: {
|
||||
|
||||
<PageHeader page={page} />
|
||||
{document ? (
|
||||
|
||||
<DocumentView
|
||||
document={document}
|
||||
style={['space-y-5', 'grid']}
|
||||
@@ -58,7 +57,6 @@ export function PageBody(props: {
|
||||
resolveContentRef: (ref) => resolveContentRef(ref, context),
|
||||
}}
|
||||
/>
|
||||
|
||||
) : null}
|
||||
|
||||
{page.layout.pagination ? (
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ export async function getResizedImageURL(
|
||||
}
|
||||
|
||||
return (options) => {
|
||||
const url = new URL('/.gitbook/image', rootUrl());
|
||||
const url = new URL('/~gitbook/image', rootUrl());
|
||||
url.searchParams.set('url', input);
|
||||
|
||||
if (options.width) {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
||||
import { createContentSecurityPolicyNonce, getContentSecurityPolicy } from '@/lib/csp';
|
||||
|
||||
export const config = {
|
||||
matcher: '/((?!_next/static|_next/image|.gitbook/revalidate|.gitbook/image).*)',
|
||||
matcher: '/((?!_next/static|_next/image|~gitbook/revalidate|~gitbook/image).*)',
|
||||
skipTrailingSlashRedirect: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ const testCases: TestsCase[] = [
|
||||
name: 'Home',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
url: '~gitbook/pdf?limit=10',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -61,6 +65,10 @@ const testCases: TestsCase[] = [
|
||||
name: 'Home',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
name: 'PDF',
|
||||
url: '~gitbook/pdf?limit=10',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
+1
-8
@@ -25,13 +25,6 @@
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/.gitbook/**/*.ts",
|
||||
"**/.gitbook/**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user