diff --git a/package.json b/package.json
index 599716ab7..12a2e49ba 100644
--- a/package.json
+++ b/package.json
@@ -1,43 +1,43 @@
{
- "name": "gitbook",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "next lint",
- "format": "prettier ./ --ignore-unknown --write",
- "format:check": "prettier ./ --ignore-unknown --list-different"
- },
- "dependencies": {
- "@geist-ui/icons": "^1.0.2",
- "@gitbook/api": "^0.12.0",
- "@readme/openapi-parser": "^2.5.0",
- "ajv": "^8.12.0",
- "assert-never": "^1.2.1",
- "bun-types": "^1.0.7",
- "jsontoxml": "^1.0.1",
- "katex": "^0.16.9",
- "next": "13.5.6",
- "react": "^18",
- "react-dom": "^18",
- "server-only": "^0.0.1",
- "shiki": "^0.14.5",
- "tailwind-merge": "^1.14.0"
- },
- "devDependencies": {
- "@types/jsontoxml": "^1.0.5",
- "@types/katex": "^0.16.5",
- "@types/node": "^20",
- "@types/react": "^18",
- "@types/react-dom": "^18",
- "autoprefixer": "^10",
- "eslint": "^8",
- "eslint-config-next": "13.5.6",
- "postcss": "^8",
- "prettier": "^3.0.3",
- "tailwindcss": "^3.3.4",
- "typescript": "^5"
- }
+ "name": "gitbook",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint",
+ "format": "prettier ./ --ignore-unknown --write",
+ "format:check": "prettier ./ --ignore-unknown --list-different"
+ },
+ "dependencies": {
+ "@geist-ui/icons": "^1.0.2",
+ "@gitbook/api": "^0.12.0",
+ "@readme/openapi-parser": "^2.5.0",
+ "ajv": "^8.12.0",
+ "assert-never": "^1.2.1",
+ "bun-types": "^1.0.7",
+ "jsontoxml": "^1.0.1",
+ "katex": "^0.16.9",
+ "next": "13.5.6",
+ "react": "^18",
+ "react-dom": "^18",
+ "server-only": "^0.0.1",
+ "shiki": "^0.14.5",
+ "tailwind-merge": "^1.14.0"
+ },
+ "devDependencies": {
+ "@types/jsontoxml": "^1.0.5",
+ "@types/katex": "^0.16.5",
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "autoprefixer": "^10",
+ "eslint": "^8",
+ "eslint-config-next": "13.5.6",
+ "postcss": "^8",
+ "prettier": "^3.0.3",
+ "tailwindcss": "^3.3.4",
+ "typescript": "^5"
+ }
}
diff --git a/src/app/[spaceId]/.gitbook/pdf/page.tsx b/src/app/[spaceId]/.gitbook/pdf/page.tsx
index d67080f9f..9ebddb820 100644
--- a/src/app/[spaceId]/.gitbook/pdf/page.tsx
+++ b/src/app/[spaceId]/.gitbook/pdf/page.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import { api } from '@/lib/api';
import { resolvePageId } from '@/lib/pages';
+import { pagePDFContainerId, PageHrefContext } from '@/lib/links';
import { DocumentView } from '@/components/DocumentView';
import { SpaceParams } from '../../fetch';
@@ -15,12 +16,14 @@ interface PDFSearchParams {
only?: boolean;
}
-
/**
* Render a space as a standalone HTML page without interactive elements.
* The HTML can be converted to PDF.
*/
-export default async function PDFHTMLOutput(props: { params: SpaceParams; searchParams: PDFSearchParams }) {
+export default async function PDFHTMLOutput(props: {
+ params: SpaceParams;
+ searchParams: PDFSearchParams;
+}) {
const { params, searchParams } = props;
const { spaceId } = params;
@@ -31,42 +34,53 @@ export default async function PDFHTMLOutput(props: { params: SpaceParams; search
const pages = selectPages(revision, searchParams).slice(0, 4); // TODO: remove slice
+ const linksContext: PageHrefContext = {
+ pdf: pages.map(({ page }) => page.id),
+ };
+
return (
<>
- {pages.map(({ page, depth }) => (
- page.type === 'group' ? :
- ))}
+ {pages.map(({ page, depth }) =>
+ page.type === 'group' ? (
+
+ ) : (
+
+ ),
+ )}
>
- )
+ );
}
-async function PDFPageGroup(props: {
- space: Space;
- revision: Revision;
- page: RevisionPageGroup;
-}) {
+async function PDFPageGroup(props: { space: Space; revision: Revision; page: RevisionPageGroup }) {
const { page } = props;
return (
{page.title}
- )
+ );
}
async function PDFPageDocument(props: {
space: Space;
revision: Revision;
page: RevisionPageDocument;
+ linksContext: PageHrefContext;
}) {
- const { space, revision, page } = props;
+ const { space, revision, page, linksContext } = props;
const {
data: { document },
} = await api().spaces.getPageInRevisionById(space.id, revision.id, page.id);
return (
-
+
{page.title}
- )
+ );
}
type FlatPageEntry = { page: RevisionPageDocument | RevisionPageGroup; depth: number };
@@ -87,12 +102,17 @@ type FlatPageEntry = { page: RevisionPageDocument | RevisionPageGroup; depth: nu
* Compute the ordered flat set of pages to render.
*/
function selectPages(revision: Revision, params: PDFSearchParams): FlatPageEntry[] {
- const flattenPage = (page: RevisionPageDocument | RevisionPageGroup, depth: number): FlatPageEntry[] => {
+ const flattenPage = (
+ page: RevisionPageDocument | RevisionPageGroup,
+ depth: number,
+ ): FlatPageEntry[] => {
return [
{ page, depth },
- ...page.pages.flatMap((child) => child.type === 'link' ? [] : flattenPage(child, depth + 1)),
+ ...page.pages.flatMap((child) =>
+ child.type === 'link' ? [] : flattenPage(child, depth + 1),
+ ),
];
- }
+ };
if (params.page) {
const found = resolvePageId(revision, params.page);
@@ -107,6 +127,5 @@ function selectPages(revision: Revision, params: PDFSearchParams): FlatPageEntry
return flattenPage(found.page, 0);
}
- return revision.pages.flatMap((page) => page.type === 'link' ? [] : flattenPage(page, 0));
+ return revision.pages.flatMap((page) => (page.type === 'link' ? [] : flattenPage(page, 0)));
}
-
diff --git a/src/app/[spaceId]/[[...pathname]]/page.tsx b/src/app/[spaceId]/[[...pathname]]/page.tsx
index 193943f4b..fda08e280 100644
--- a/src/app/[spaceId]/[[...pathname]]/page.tsx
+++ b/src/app/[spaceId]/[[...pathname]]/page.tsx
@@ -1,5 +1,5 @@
import { SpaceContent } from '@/components/SpaceContent';
-import { pageHref } from '@/lib/links';
+import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
import { Metadata } from 'next';
import { notFound, redirect } from 'next/navigation';
import { PagePathParams, fetchPageData, getPagePath } from '../fetch';
@@ -12,11 +12,12 @@ export default async function Page(props: { params: PagePathParams }) {
const { params } = props;
const { space, revision, page, ancestors } = await fetchPageData(props.params);
+ const linksContext: PageHrefContext = {};
if (!page) {
notFound();
} else if (page.path !== getPagePath(params)) {
- redirect(pageHref(page.path));
+ redirect(pageHref(page, linksContext));
}
const {
@@ -45,7 +46,7 @@ export async function generateMetadata({ params }: { params: PagePathParams }):
description: page.description,
generator: 'GitBook',
openGraph: {
- images: [pageHref('.gitbook/ogimage/' + page.id)],
+ images: [absoluteHref('.gitbook/ogimage/' + page.id)],
},
robots: space.visibility === 'public' ? 'index, follow' : 'noindex, nofollow',
diff --git a/src/app/[spaceId]/sitemap.xml/route.ts b/src/app/[spaceId]/sitemap.xml/route.ts
index 513087979..fd8c834c0 100644
--- a/src/app/[spaceId]/sitemap.xml/route.ts
+++ b/src/app/[spaceId]/sitemap.xml/route.ts
@@ -30,14 +30,14 @@ export async function GET(req: NextRequest, { params }: { params: SpaceParams })
url: {
loc: pageHref(page.path),
priority: normalizedPriority,
- ...(lastModified ? {
- // lastmod format is YYYY-MM-DD
- lastmod: new Date(lastModified)
- .toISOString()
- .split('T')[0],
- } : {})
- }
- }
+ ...(lastModified
+ ? {
+ // lastmod format is YYYY-MM-DD
+ lastmod: new Date(lastModified).toISOString().split('T')[0],
+ }
+ : {}),
+ },
+ };
});
const xml = jsontoxml(
@@ -56,7 +56,7 @@ export async function GET(req: NextRequest, { params }: { params: SpaceParams })
{
xmlHeader: true,
prettyPrint: true,
- }
+ },
);
return new Response(xml, {
@@ -69,14 +69,17 @@ export async function GET(req: NextRequest, { params }: { params: SpaceParams })
type FlatPageEntry = { page: RevisionPageDocument; depth: number };
function flattenPages(revision: Revision): FlatPageEntry[] {
- const flattenPage = (page: RevisionPageDocument | RevisionPageGroup, depth: number): FlatPageEntry[] => {
+ const flattenPage = (
+ page: RevisionPageDocument | RevisionPageGroup,
+ depth: number,
+ ): FlatPageEntry[] => {
return [
...(page.type === 'document' ? [{ page, depth }] : []),
- ...page.pages.flatMap((child) => child.type === 'link' ? [] : flattenPage(child, depth + 1)),
+ ...page.pages.flatMap((child) =>
+ child.type === 'link' ? [] : flattenPage(child, depth + 1),
+ ),
];
- }
+ };
- return revision.pages.flatMap((page) => page.type === 'link' ? [] : flattenPage(page, 0));
+ return revision.pages.flatMap((page) => (page.type === 'link' ? [] : flattenPage(page, 0)));
}
-
-
diff --git a/src/components/DocumentView/Heading.tsx b/src/components/DocumentView/Heading.tsx
index 1e79d7fda..7979c997d 100644
--- a/src/components/DocumentView/Heading.tsx
+++ b/src/components/DocumentView/Heading.tsx
@@ -4,14 +4,15 @@ import { BlockProps } from './Block';
import { Inlines } from './Inlines';
import { DocumentBlockHeading } from '@gitbook/api';
import { getBlockTextStyle } from './spacing';
+import { pageLocalId } from '@/lib/links';
export function Heading(props: BlockProps
) {
- const { block, style, ...contextProps } = props;
+ const { block, style, context, ...rest } = props;
const textStyle = getBlockTextStyle(block);
const Tag = TAGS[block.type];
- const id = block.meta?.id ?? '';
+ const id = pageLocalId(context.page, block.meta?.id ?? '', context);
return (
@@ -56,7 +57,7 @@ export function Heading(props: BlockProps) {
-
+
);
}
diff --git a/src/components/DocumentView/Images.tsx b/src/components/DocumentView/Images.tsx
index eb596f825..442db07e7 100644
--- a/src/components/DocumentView/Images.tsx
+++ b/src/components/DocumentView/Images.tsx
@@ -6,7 +6,7 @@ import { Inlines } from './Inlines';
import { DocumentBlockImage, DocumentBlockImages } from '@gitbook/api';
export function Images(props: BlockProps) {
- const { block, style, ...contextProps } = props;
+ const { block, style, context } = props;
return (
) {
block={node}
style={[i > 0 && 'mt-4', style]}
siblings={block.nodes.length}
- {...contextProps}
+ context={context}
/>
))}
diff --git a/src/components/DocumentView/Table/RecordCard.tsx b/src/components/DocumentView/Table/RecordCard.tsx
index b5252b505..76340d9db 100644
--- a/src/components/DocumentView/Table/RecordCard.tsx
+++ b/src/components/DocumentView/Table/RecordCard.tsx
@@ -9,19 +9,19 @@ export async function RecordCard(
record: TableRecordKV;
},
) {
- const { block, view, record } = props;
+ const { block, view, record, context } = props;
const coverFile = view.coverDefinition
? (record[1].values[view.coverDefinition]?.[0] as string)
: null;
const cover = coverFile
- ? await resolveContentRef({ kind: 'file', file: coverFile }, props.context)
+ ? await resolveContentRef({ kind: 'file', file: coverFile }, context)
: null;
const targetRef = view.targetDefinition
? (record[1].values[view.targetDefinition] as ContentRef)
: null;
- const target = targetRef ? await resolveContentRef(targetRef, props.context) : null;
+ const target = targetRef ? await resolveContentRef(targetRef, context) : null;
const body = (
<>
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index 2ef06fdff..17a0cb59f 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -1,4 +1,4 @@
-import { pageHref } from '@/lib/links';
+import { absoluteHref } from '@/lib/links';
import { Space } from '@gitbook/api';
import { tcls } from '@/lib/tailwind';
@@ -47,7 +47,7 @@ export function Header(props: { space: Space; asFullWidth: boolean }) {
asFullWidth ? null : [CONTAINER_MAX_WIDTH_NORMAL, 'mx-auto'],
)}
>
-
+
{space.title}
diff --git a/src/components/TableOfContents/PageDocumentItem.tsx b/src/components/TableOfContents/PageDocumentItem.tsx
index 08a8b0882..2ac71243d 100644
--- a/src/components/TableOfContents/PageDocumentItem.tsx
+++ b/src/components/TableOfContents/PageDocumentItem.tsx
@@ -15,7 +15,7 @@ export function PageDocumentItem(props: {
const hasActiveDescendant = ancestors.some((ancestor) => ancestor.id === page.id);
const linkProps = {
- href: pageHref(page.path || ''),
+ href: pageHref(page),
className: tcls(
'flex',
'flex-row',
diff --git a/src/lib/links.ts b/src/lib/links.ts
index f99a4f3e1..0f9966749 100644
--- a/src/lib/links.ts
+++ b/src/lib/links.ts
@@ -1,6 +1,15 @@
import 'server-only';
import { headers } from 'next/headers';
+import { RevisionPageDocument } from '@gitbook/api';
+
+export interface PageHrefContext {
+ /**
+ * If defined, we are generating a PDF of the specific page IDs,
+ * and these pages will be rendered in the same HTML output.
+ */
+ pdf?: string[];
+}
/**
* Return the base path for the current request.
@@ -10,9 +19,56 @@ export function basePath(): string {
return headersList.get('x-gitbook-basepath') ?? '';
}
+/**
+ * Create an absolute href in the current content.
+ */
+export function absoluteHref(href: string): string {
+ return `${basePath()}/${href.startsWith('/') ? href.slice(1) : href}`;
+}
+
/**
* Create a link to a page path in the current space.
*/
-export function pageHref(pagePath: string): string {
- return `${basePath()}/${pagePath.startsWith('/') ? pagePath.slice(1) : pagePath}`;
+export function pageHref(
+ page: RevisionPageDocument,
+ context: PageHrefContext = {},
+ /** Anchor to link to in the page. */
+ anchor?: string,
+): string {
+ const { pdf } = context;
+
+ if (pdf) {
+ if (pdf.includes(page.id)) {
+ return '#' + pagePDFContainerId(page, anchor);
+ } else {
+ // Use an absolute URL to the page
+ // TODO: we need to extend RevisionPageDocument with "urls"
+ return page.urls?.published || '/todo';
+ }
+ }
+
+ return absoluteHref(page.path) + (anchor ? '#' + anchor : '');
+}
+
+/**
+ * Create the HTML ID for the container of a page during a PDF rendering.
+ */
+export function pagePDFContainerId(page: RevisionPageDocument, anchor?: string): string {
+ return `pdf-page-${page.id}` + (anchor ? `-${anchor}` : '');
+}
+
+/**
+ * Create an HTML ID for a block in a page.
+ * It ensures the ID is unique in the entire HTML page (in case we are generating a PDF with multiple pages).
+ */
+export function pageLocalId(
+ page: RevisionPageDocument,
+ localId: string,
+ context: PageHrefContext,
+): string {
+ if (!context.pdf?.length) {
+ return localId;
+ }
+
+ return pagePDFContainerId(page, localId);
}
diff --git a/src/lib/references.ts b/src/lib/references.ts
index 66ea8b4dc..a645e86f7 100644
--- a/src/lib/references.ts
+++ b/src/lib/references.ts
@@ -1,6 +1,6 @@
import { ContentRef, Revision, RevisionPageDocument, Space } from '@gitbook/api';
import { resolvePageId } from './pages';
-import { pageHref } from './links';
+import { pageHref, PageHrefContext } from './links';
export interface ResolvedContentRef {
/** Text to render in the content ref */
@@ -9,7 +9,7 @@ export interface ResolvedContentRef {
href: string;
}
-export interface ContentRefContext {
+export interface ContentRefContext extends PageHrefContext {
space: Space;
revision: Revision;
page: RevisionPageDocument;
@@ -20,7 +20,7 @@ export interface ContentRefContext {
*/
export async function resolveContentRef(
contentRef: ContentRef,
- { space, revision, page: activePage }: ContentRefContext,
+ { space, revision, page: activePage, ...linksContext }: ContentRefContext,
): Promise {
// Try to resolve a local ref in the current space
if (contentRef.kind === 'url') {
@@ -49,13 +49,13 @@ export async function resolveContentRef(
if (contentRef.kind === 'page') {
return {
- href: pageHref(page.path),
+ href: pageHref(page, linksContext),
text: page.title,
};
}
return {
- href: pageHref(page.path) + '#' + contentRef.anchor,
+ href: pageHref(page, linksContext, contentRef.anchor),
text: page.title + '#' + contentRef.anchor,
};
} else if (contentRef.kind === 'space' && contentRef.space === space.id) {