diff --git a/src/app/[spaceId]/[[...pathname]]/ClientContexts.tsx b/src/app/[spaceId]/ClientContexts.tsx
similarity index 100%
rename from src/app/[spaceId]/[[...pathname]]/ClientContexts.tsx
rename to src/app/[spaceId]/ClientContexts.tsx
diff --git a/src/app/[spaceId]/[[...pathname]]/layout.tsx b/src/app/[spaceId]/[[...pathname]]/layout.tsx
deleted file mode 100644
index 3f79a2f69..000000000
--- a/src/app/[spaceId]/[[...pathname]]/layout.tsx
+++ /dev/null
@@ -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 (
-
-
- {customization.privacyPolicy.url ? (
-
- ) : null}
-
-
-
- {children}
-
- {scripts.map(({ script }) => (
-
- ))}
-
-
- );
-}
-
-type ColorInput = string | Record;
-function generateColorVariable(name: string, color: ColorInput) {
- const shades: Record = 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);
- }
- }
-}
diff --git a/src/app/[spaceId]/[[...pathname]]/page.tsx b/src/app/[spaceId]/[[...pathname]]/page.tsx
index d37f151bb..2235d71ba 100644
--- a/src/app/[spaceId]/[[...pathname]]/page.tsx
+++ b/src/app/[spaceId]/[[...pathname]]/page.tsx
@@ -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 }) => (
+
+ ))}
+
{scripts.some((script) => script.cookies) || 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',
diff --git a/src/app/[spaceId]/[[...pathname]]/error.tsx b/src/app/[spaceId]/error.tsx
similarity index 100%
rename from src/app/[spaceId]/[[...pathname]]/error.tsx
rename to src/app/[spaceId]/error.tsx
diff --git a/src/app/[spaceId]/global-error.tsx b/src/app/[spaceId]/global-error.tsx
new file mode 100644
index 000000000..310e9d3a8
--- /dev/null
+++ b/src/app/[spaceId]/global-error.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import * as React from 'react';
+
+export default function LayoutError(props: {}) {
+ return (
+
+ Layout error
+
+ );
+}
diff --git a/src/app/[spaceId]/layout.tsx b/src/app/[spaceId]/layout.tsx
index 76f3b6e6a..5a1e5898a 100644
--- a/src/app/[spaceId]/layout.tsx
+++ b/src/app/[spaceId]/layout.tsx
@@ -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 (
+
+
+ {customization.privacyPolicy.url ? (
+
+ ) : null}
+
+
+
+ {children}
+
+
+ );
+}
+
+type ColorInput = string | Record;
+function generateColorVariable(name: string, color: ColorInput) {
+ const shades: Record = 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);
+ }
+ }
}
diff --git a/src/app/[spaceId]/robots.txt/route.ts b/src/app/[spaceId]/robots.txt/route.ts
index 23b21d54f..977655592 100644
--- a/src/app/[spaceId]/robots.txt/route.ts
+++ b/src/app/[spaceId]/robots.txt/route.ts
@@ -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: /`]),
diff --git a/src/app/[spaceId]/.gitbook/icon/route.tsx b/src/app/[spaceId]/~gitbook/icon/route.tsx
similarity index 99%
rename from src/app/[spaceId]/.gitbook/icon/route.tsx
rename to src/app/[spaceId]/~gitbook/icon/route.tsx
index 780b8f2b8..3ede920e3 100644
--- a/src/app/[spaceId]/.gitbook/icon/route.tsx
+++ b/src/app/[spaceId]/~gitbook/icon/route.tsx
@@ -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';
diff --git a/src/app/[spaceId]/.gitbook/ogimage/[pageId]/route.tsx b/src/app/[spaceId]/~gitbook/ogimage/[pageId]/route.tsx
similarity index 99%
rename from src/app/[spaceId]/.gitbook/ogimage/[pageId]/route.tsx
rename to src/app/[spaceId]/~gitbook/ogimage/[pageId]/route.tsx
index 709def61b..39f792468 100644
--- a/src/app/[spaceId]/.gitbook/ogimage/[pageId]/route.tsx
+++ b/src/app/[spaceId]/~gitbook/ogimage/[pageId]/route.tsx
@@ -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';
diff --git a/src/app/[spaceId]/~gitbook/pdf/OpenPrintDialog.tsx b/src/app/[spaceId]/~gitbook/pdf/OpenPrintDialog.tsx
new file mode 100644
index 000000000..09b020da5
--- /dev/null
+++ b/src/app/[spaceId]/~gitbook/pdf/OpenPrintDialog.tsx
@@ -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;
+}
diff --git a/src/app/[spaceId]/.gitbook/pdf/page.tsx b/src/app/[spaceId]/~gitbook/pdf/page.tsx
similarity index 62%
rename from src/app/[spaceId]/.gitbook/pdf/page.tsx
rename to src/app/[spaceId]/~gitbook/pdf/page.tsx
index 3f692b926..0340d7ab4 100644
--- a/src/app/[spaceId]/.gitbook/pdf/page.tsx
+++ b/src/app/[spaceId]/~gitbook/pdf/page.tsx
@@ -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 (
- <>
+
+
{pages.map(({ page, depth }) =>
page.type === 'group' ? (
) : (
-
+
+
+
),
)}
- >
+
+
+
+ );
+}
+
+async function SpaceIntro(props: { space: Space }) {
+ const { space } = props;
+
+ return (
+
+
{space.title}
+
);
}
@@ -73,8 +106,18 @@ async function PDFPageGroup(props: { space: Space; page: RevisionPageGroup }) {
const { page } = props;
return (
-
-
{page.title}
+
+
{page.title}
);
}
@@ -89,12 +132,16 @@ async function PDFPageDocument(props: {
const document = page.documentId ? await getDocument(space.id, page.documentId) : null;
return (
-
-
{page.title}
+
+
{page.title}
{document ? (
resolveContentRef(ref, refContext),
getId: (id) => pagePDFContainerId(page, id),
diff --git a/src/app/[spaceId]/~gitbook/pdf/pdf.css b/src/app/[spaceId]/~gitbook/pdf/pdf.css
new file mode 100644
index 000000000..7ed444b09
--- /dev/null
+++ b/src/app/[spaceId]/~gitbook/pdf/pdf.css
@@ -0,0 +1,8 @@
+@page {
+ size: A4;
+ margin: 60pt 60pt;
+
+ @bottom-right {
+ content: counter(page);
+ }
+}
diff --git a/src/app/.gitbook/image/route.ts b/src/app/~gitbook/image/route.ts
similarity index 97%
rename from src/app/.gitbook/image/route.ts
rename to src/app/~gitbook/image/route.ts
index 3b198eba4..232d4505c 100644
--- a/src/app/.gitbook/image/route.ts
+++ b/src/app/~gitbook/image/route.ts
@@ -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 });
}
diff --git a/src/app/.gitbook/revalidate/route.ts b/src/app/~gitbook/revalidate/route.ts
similarity index 99%
rename from src/app/.gitbook/revalidate/route.ts
rename to src/app/~gitbook/revalidate/route.ts
index 049cac473..3417a19d3 100644
--- a/src/app/.gitbook/revalidate/route.ts
+++ b/src/app/~gitbook/revalidate/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
+
import { revalidateTags } from '@/lib/cache';
export const runtime = 'edge';
diff --git a/src/components/DocumentView/DocumentView.tsx b/src/components/DocumentView/DocumentView.tsx
index ca23aaa6c..34d9fe108 100644
--- a/src/components/DocumentView/DocumentView.tsx
+++ b/src/components/DocumentView/DocumentView.tsx
@@ -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 (
diff --git a/src/components/Header/HeaderLogo.tsx b/src/components/Header/HeaderLogo.tsx
index 405731cf2..d522841dd 100644
--- a/src/components/Header/HeaderLogo.tsx
+++ b/src/components/Header/HeaderLogo.tsx
@@ -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 },
},
}
diff --git a/src/components/PageBody/PageBody.tsx b/src/components/PageBody/PageBody.tsx
index 823f65591..a44e39510 100644
--- a/src/components/PageBody/PageBody.tsx
+++ b/src/components/PageBody/PageBody.tsx
@@ -49,7 +49,6 @@ export function PageBody(props: {
{document ? (
-
resolveContentRef(ref, context),
}}
/>
-
) : null}
{page.layout.pagination ? (
diff --git a/src/lib/images.ts b/src/lib/images.ts
index dc8ca9dba..2cceef240 100644
--- a/src/lib/images.ts
+++ b/src/lib/images.ts
@@ -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) {
diff --git a/src/middleware.ts b/src/middleware.ts
index 5febc3cb0..b26001135 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -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,
};
diff --git a/tests/visual-testing.ts b/tests/visual-testing.ts
index d3ef63a94..61ff65f71 100644
--- a/tests/visual-testing.ts
+++ b/tests/visual-testing.ts
@@ -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',
+ },
],
},
{
diff --git a/tsconfig.json b/tsconfig.json
index e9fb39f4a..4bb21469a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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"]
}