diff --git a/.env.example b/.env.example
new file mode 100644
index 000000000..f6640d765
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,6 @@
+# Go to https://app.gitbook.com/account/developer and create a new token
+# Copy the token and paste it in the .env file
+# GITBOOK_TOKEN=your_token_here
+
+# Other options:
+# GITBOOK_API_URL=https://api.gitbook.com
diff --git a/.eslintrc.json b/.eslintrc.json
index bffb357a7..1c2aa65d7 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,3 +1,3 @@
{
- "extends": "next/core-web-vitals"
+ "extends": "next/core-web-vitals"
}
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 000000000..a680367ef
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+.next
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 000000000..581887eed
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,5 @@
+{
+ "printWidth": 100,
+ "singleQuote": true,
+ "tabWidth": 4
+}
diff --git a/README.md b/README.md
index c4033664f..e69de29bb 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +0,0 @@
-This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/bun.lockb b/bun.lockb
index b4730dd56..bad6bd4d1 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/next.config.js b/next.config.js
index 767719fc4..658404ac6 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,4 +1,4 @@
/** @type {import('next').NextConfig} */
-const nextConfig = {}
+const nextConfig = {};
-module.exports = nextConfig
+module.exports = nextConfig;
diff --git a/package.json b/package.json
index 258fe4591..e1eeb10eb 100644
--- a/package.json
+++ b/package.json
@@ -1,27 +1,33 @@
{
- "name": "gitbook",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "next lint"
- },
- "dependencies": {
- "react": "^18",
- "react-dom": "^18",
- "next": "13.5.6"
- },
- "devDependencies": {
- "typescript": "^5",
- "@types/node": "^20",
- "@types/react": "^18",
- "@types/react-dom": "^18",
- "autoprefixer": "^10",
- "postcss": "^8",
- "tailwindcss": "^3",
- "eslint": "^8",
- "eslint-config-next": "13.5.6"
- }
+ "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": {
+ "@gitbook/api": "^0.11.0",
+ "clsx": "^2.0.0",
+ "next": "13.5.6",
+ "react": "^18",
+ "react-dom": "^18",
+ "server-only": "^0.0.1"
+ },
+ "devDependencies": {
+ "@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",
+ "typescript": "^5"
+ }
}
diff --git a/postcss.config.js b/postcss.config.js
index 33ad091d2..67cdf1a55 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1,6 +1,6 @@
module.exports = {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-}
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/public/next.svg b/public/next.svg
deleted file mode 100644
index 5174b28c5..000000000
--- a/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/vercel.svg b/public/vercel.svg
deleted file mode 100644
index d2f842227..000000000
--- a/public/vercel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/app/[[...pathname]]/page.tsx b/src/app/[[...pathname]]/page.tsx
new file mode 100644
index 000000000..a48368ef7
--- /dev/null
+++ b/src/app/[[...pathname]]/page.tsx
@@ -0,0 +1,30 @@
+import { headers } from 'next/headers';
+
+import { api } from '@/lib/api';
+import { SpaceContent } from '@/components/SpaceContent';
+
+export default async function Page(props: { params: { pathname?: string[] } }) {
+ const {
+ params: { pathname },
+ } = props;
+
+ const headersList = headers();
+ const spaceId = headersList.get('x-gitbook-space');
+
+ if (!spaceId) {
+ throw new Error('Missing space id');
+ }
+
+ const [{ data: space }, { data: revision }] = await Promise.all([
+ api().spaces.getSpaceById(spaceId),
+ api().spaces.getCurrentRevision(spaceId),
+ ]);
+
+ return (
+
+ );
+}
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
deleted file mode 100644
index 718d6fea4..000000000
Binary files a/src/app/favicon.ico and /dev/null differ
diff --git a/src/app/globals.css b/src/app/globals.css
index fd81e8858..68cb03319 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -2,26 +2,5 @@
@tailwind components;
@tailwind utilities;
-:root {
- --foreground-rgb: 0, 0, 0;
- --background-start-rgb: 214, 219, 220;
- --background-end-rgb: 255, 255, 255;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --foreground-rgb: 255, 255, 255;
- --background-start-rgb: 0, 0, 0;
- --background-end-rgb: 0, 0, 0;
- }
-}
-
body {
- color: rgb(var(--foreground-rgb));
- background: linear-gradient(
- to bottom,
- transparent,
- rgb(var(--background-end-rgb))
- )
- rgb(var(--background-start-rgb));
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 40e027fbe..bc255000d 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,22 +1,12 @@
-import type { Metadata } from 'next'
-import { Inter } from 'next/font/google'
-import './globals.css'
+import { Inter } from 'next/font/google';
+import './globals.css';
-const inter = Inter({ subsets: ['latin'] })
+const inter = Inter({ subsets: ['latin'] });
-export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
-}
-
-export default function RootLayout({
- children,
-}: {
- children: React.ReactNode
-}) {
- return (
-
-
{children}
-
- )
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
deleted file mode 100644
index e38c626e1..000000000
--- a/src/app/page.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import Image from 'next/image'
-
-export default function Home() {
- return (
-
-
-
- Get started by editing
- src/app/page.tsx
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/src/components/DocumentView/Block.tsx b/src/components/DocumentView/Block.tsx
new file mode 100644
index 000000000..4667f0afd
--- /dev/null
+++ b/src/components/DocumentView/Block.tsx
@@ -0,0 +1,32 @@
+import { ClassValue } from 'clsx';
+import { Paragraph } from './Paragraph';
+import { Heading } from './Heading';
+import { ListOrdered } from './ListOrdered';
+import { ListUnordered } from './ListUnordered';
+import { ListItem } from './ListItem';
+
+export interface BlockProps {
+ block: T;
+ style?: ClassValue;
+}
+
+export function Block(props: BlockProps) {
+ const { block } = props;
+
+ switch (block.type) {
+ case 'paragraph':
+ return ;
+ case 'heading-1':
+ case 'heading-2':
+ case 'heading-3':
+ return ;
+ case 'list-ordered':
+ return ;
+ case 'list-unordered':
+ return ;
+ case 'list-item':
+ return ;
+ default:
+ return Unsupported block {block.type}
;
+ }
+}
diff --git a/src/components/DocumentView/Blocks.tsx b/src/components/DocumentView/Blocks.tsx
new file mode 100644
index 000000000..765533631
--- /dev/null
+++ b/src/components/DocumentView/Blocks.tsx
@@ -0,0 +1,41 @@
+import clsx from 'clsx';
+import { ClassValue } from 'clsx';
+import { Block } from './Block';
+
+export function Blocks(props: {
+ /** Blocks to render */
+ nodes: T[];
+
+ /** HTML tag to use */
+ tag?: string;
+
+ /** Style passed to the wrapper */
+ style?: ClassValue;
+
+ /** Style passed to all blocks */
+ blockStyle?: ClassValue;
+
+ /** Style passed to the first block */
+ firstBlockStyle?: ClassValue;
+
+ /** Style passed to the last block */
+ lastBlockStyle?: ClassValue;
+}) {
+ const { nodes, tag: Tag = 'div', style, blockStyle, firstBlockStyle, lastBlockStyle } = props;
+
+ return (
+
+ {nodes.map((node, index) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/DocumentView/DocumentView.tsx b/src/components/DocumentView/DocumentView.tsx
new file mode 100644
index 000000000..b29481835
--- /dev/null
+++ b/src/components/DocumentView/DocumentView.tsx
@@ -0,0 +1,11 @@
+import clsx from 'clsx';
+import { Blocks } from './Blocks';
+
+/**
+ * Render an entire document.
+ */
+export function DocumentView(props: { document: any }) {
+ const { document } = props;
+
+ return ;
+}
diff --git a/src/components/DocumentView/Heading.tsx b/src/components/DocumentView/Heading.tsx
new file mode 100644
index 000000000..f5cf0b589
--- /dev/null
+++ b/src/components/DocumentView/Heading.tsx
@@ -0,0 +1,30 @@
+import clsx from 'clsx';
+import { BlockProps } from './Block';
+import { Inlines } from './Inlines';
+
+export function Heading(props: BlockProps) {
+ const { block, style } = props;
+
+ const headingStyle = STYLES[block.type];
+
+ return (
+
+
+
+ );
+}
+
+const STYLES = {
+ 'heading-1': {
+ tag: 'h2',
+ className: ['text-3xl', 'font-semibold'],
+ },
+ 'heading-2': {
+ tag: 'h3',
+ className: ['text-2xl', 'font-semibold'],
+ },
+ 'heading-3': {
+ tag: 'h4',
+ className: ['text-3xl', 'font-semibold'],
+ },
+};
diff --git a/src/components/DocumentView/Inline.tsx b/src/components/DocumentView/Inline.tsx
new file mode 100644
index 000000000..13d9221ab
--- /dev/null
+++ b/src/components/DocumentView/Inline.tsx
@@ -0,0 +1,14 @@
+import { ClassValue } from 'clsx';
+
+export interface InlineProps {
+ inline: T;
+}
+
+export function Inline(props: InlineProps) {
+ const { inline } = props;
+
+ switch (inline.type) {
+ default:
+ return Unsupported inline {inline.type};
+ }
+}
diff --git a/src/components/DocumentView/Inlines.tsx b/src/components/DocumentView/Inlines.tsx
new file mode 100644
index 000000000..4ced5b699
--- /dev/null
+++ b/src/components/DocumentView/Inlines.tsx
@@ -0,0 +1,19 @@
+import clsx from 'clsx';
+import { Text } from './Text';
+import { Inline } from './Inline';
+
+export function Inlines(props: { nodes: T[] }) {
+ const { nodes } = props;
+
+ return (
+ <>
+ {nodes.map((node, index) => {
+ if (node.object === 'text') {
+ return ;
+ }
+
+ return ;
+ })}
+ >
+ );
+}
diff --git a/src/components/DocumentView/ListItem.tsx b/src/components/DocumentView/ListItem.tsx
new file mode 100644
index 000000000..60ae4eb05
--- /dev/null
+++ b/src/components/DocumentView/ListItem.tsx
@@ -0,0 +1,13 @@
+import clsx from 'clsx';
+import { BlockProps } from './Block';
+import { Blocks } from './Blocks';
+
+export function ListItem(props: BlockProps) {
+ const { block } = props;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/DocumentView/ListOrdered.tsx b/src/components/DocumentView/ListOrdered.tsx
new file mode 100644
index 000000000..ad251441b
--- /dev/null
+++ b/src/components/DocumentView/ListOrdered.tsx
@@ -0,0 +1,8 @@
+import { BlockProps } from './Block';
+import { Blocks } from './Blocks';
+
+export function ListOrdered(props: BlockProps) {
+ const { block, style } = props;
+
+ return ;
+}
diff --git a/src/components/DocumentView/ListUnordered.tsx b/src/components/DocumentView/ListUnordered.tsx
new file mode 100644
index 000000000..c487698d1
--- /dev/null
+++ b/src/components/DocumentView/ListUnordered.tsx
@@ -0,0 +1,8 @@
+import { BlockProps } from './Block';
+import { Blocks } from './Blocks';
+
+export function ListUnordered(props: BlockProps) {
+ const { block, style } = props;
+
+ return ;
+}
diff --git a/src/components/DocumentView/Paragraph.tsx b/src/components/DocumentView/Paragraph.tsx
new file mode 100644
index 000000000..9ffcfc163
--- /dev/null
+++ b/src/components/DocumentView/Paragraph.tsx
@@ -0,0 +1,13 @@
+import clsx from 'clsx';
+import { BlockProps } from './Block';
+import { Inlines } from './Inlines';
+
+export function Paragraph(props: BlockProps) {
+ const { block, style } = props;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/DocumentView/Text.tsx b/src/components/DocumentView/Text.tsx
new file mode 100644
index 000000000..552fde952
--- /dev/null
+++ b/src/components/DocumentView/Text.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+export function Text(props: { text: any }) {
+ const { text } = props;
+
+ return (
+ <>
+ {text.leaves.map((leaf, index) => {
+ // TODO: marks
+ return {leaf.text};
+ })}
+ >
+ );
+}
diff --git a/src/components/DocumentView/index.ts b/src/components/DocumentView/index.ts
new file mode 100644
index 000000000..2b742d2d5
--- /dev/null
+++ b/src/components/DocumentView/index.ts
@@ -0,0 +1 @@
+export * from './DocumentView';
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 000000000..b345e9c93
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,27 @@
+import { pageHref } from '@/lib/links';
+import { Space } from '@gitbook/api';
+
+import clsx from 'clsx';
+import Link from 'next/link';
+
+/**
+ * Render the header for the space.
+ */
+export function Header(props: { space: Space }) {
+ const { space } = props;
+
+ return (
+
+
+
+
{space.title}
+
+
+
+ );
+}
diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts
new file mode 100644
index 000000000..266dec8a1
--- /dev/null
+++ b/src/components/Header/index.ts
@@ -0,0 +1 @@
+export * from './Header';
diff --git a/src/components/PageBody/PageBody.tsx b/src/components/PageBody/PageBody.tsx
new file mode 100644
index 000000000..ae65a897f
--- /dev/null
+++ b/src/components/PageBody/PageBody.tsx
@@ -0,0 +1,14 @@
+import { Revision, RevisionPageDocument, Space } from '@gitbook/api';
+import { PageHeader } from './PageHeader';
+import { PageDocument } from './PageDocument';
+
+export function PageBody(props: { space: Space; revision: Revision; page: RevisionPageDocument }) {
+ const { space, revision, page } = props;
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/PageBody/PageDocument.tsx b/src/components/PageBody/PageDocument.tsx
new file mode 100644
index 000000000..ce681873c
--- /dev/null
+++ b/src/components/PageBody/PageDocument.tsx
@@ -0,0 +1,20 @@
+import { api } from '@/lib/api';
+import { Revision, RevisionPageDocument, Space } from '@gitbook/api';
+import { DocumentView } from '../DocumentView';
+
+/**
+ * Fetch the document for the page and render it.
+ */
+export async function PageDocument(props: {
+ space: Space;
+ revision: Revision;
+ page: RevisionPageDocument;
+}) {
+ const { space, revision, page } = props;
+
+ const {
+ data: { document },
+ } = await api().spaces.getPageInRevisionById(space.id, revision.id, page.id);
+
+ return ;
+}
diff --git a/src/components/PageBody/PageHeader.tsx b/src/components/PageBody/PageHeader.tsx
new file mode 100644
index 000000000..6ede8ca03
--- /dev/null
+++ b/src/components/PageBody/PageHeader.tsx
@@ -0,0 +1,15 @@
+import { RevisionPageDocument } from '@gitbook/api';
+import clsx from 'clsx';
+
+export function PageHeader(props: { page: RevisionPageDocument }) {
+ const { page } = props;
+
+ return (
+
+ {page.title}
+ {page.description ? (
+ {page.description}
+ ) : null}
+
+ );
+}
diff --git a/src/components/PageBody/index.ts b/src/components/PageBody/index.ts
new file mode 100644
index 000000000..9cc5a3f24
--- /dev/null
+++ b/src/components/PageBody/index.ts
@@ -0,0 +1 @@
+export * from './PageBody';
diff --git a/src/components/SpaceContent/SpaceContent.tsx b/src/components/SpaceContent/SpaceContent.tsx
new file mode 100644
index 000000000..4e420b9a3
--- /dev/null
+++ b/src/components/SpaceContent/SpaceContent.tsx
@@ -0,0 +1,95 @@
+import { Revision, RevisionPage, RevisionPageDocument, Space } from '@gitbook/api';
+
+import { TableOfContents } from '@/components/TableOfContents';
+import clsx from 'clsx';
+import { Header } from '@/components/Header';
+import { PageBody } from '@/components/PageBody';
+import { notFound, redirect } from 'next/navigation';
+import { pageHref } from '@/lib/links';
+
+/**
+ * Render the entire content of the space (header, table of contents, footer, and page content).
+ */
+export function SpaceContent(props: { space: Space; revision: Revision; pagePath: string }) {
+ const { space, revision, pagePath } = props;
+ const activePage = resolvePagePath(revision, pagePath);
+
+ return (
+
+ );
+}
+
+function resolvePagePath(revision: Revision, pagePath: string): RevisionPageDocument {
+ const resolveFirstDocument = (pages: RevisionPage[]): RevisionPageDocument | undefined => {
+ for (const page of pages) {
+ if (page.type === 'link') {
+ continue;
+ }
+
+ return resolvePage(page);
+ }
+
+ return;
+ };
+
+ const resolvePage = (page: RevisionPage): RevisionPageDocument => {
+ if (page.type === 'group') {
+ const firstDocument = resolveFirstDocument(page.pages);
+ if (firstDocument) {
+ redirect(pageHref(firstDocument.path));
+ }
+
+ notFound();
+ } else if (page.type === 'link') {
+ notFound();
+ }
+
+ return page;
+ };
+
+ const iteratePages = (pages: RevisionPage[]): RevisionPageDocument | undefined => {
+ for (const page of pages) {
+ if (page.type === 'link') {
+ continue;
+ }
+
+ if (page.path !== pagePath) {
+ // TODO: can be optimized to count the number of slashes and skip the entire subtree
+ const result = iteratePages(page.pages);
+ if (result) {
+ return result;
+ }
+
+ continue;
+ }
+
+ return resolvePage(page);
+ }
+ };
+
+ if (!pagePath) {
+ const firstPage = resolveFirstDocument(revision.pages);
+ if (!firstPage) {
+ notFound();
+ }
+
+ return firstPage;
+ }
+
+ const result = iteratePages(revision.pages);
+ if (!result) {
+ notFound();
+ }
+
+ return result;
+}
diff --git a/src/components/SpaceContent/index.ts b/src/components/SpaceContent/index.ts
new file mode 100644
index 000000000..6436c15b8
--- /dev/null
+++ b/src/components/SpaceContent/index.ts
@@ -0,0 +1 @@
+export * from './SpaceContent';
diff --git a/src/components/TableOfContents/PageDocumentItem.tsx b/src/components/TableOfContents/PageDocumentItem.tsx
new file mode 100644
index 000000000..721d60736
--- /dev/null
+++ b/src/components/TableOfContents/PageDocumentItem.tsx
@@ -0,0 +1,36 @@
+import { RevisionPageDocument } from '@gitbook/api';
+import { clsx } from 'clsx';
+import Link from 'next/link';
+import { PagesList } from './PagesList';
+import { pageHref } from '@/lib/links';
+
+export function PageDocumentItem(props: {
+ page: RevisionPageDocument;
+ activePage: RevisionPageDocument;
+}) {
+ const { page, activePage } = props;
+
+ return (
+
+
+ {page.title}
+
+ {page.pages && page.pages.length ? (
+
+ ) : null}
+
+ );
+}
diff --git a/src/components/TableOfContents/PageGroupItem.tsx b/src/components/TableOfContents/PageGroupItem.tsx
new file mode 100644
index 000000000..32baa9a8d
--- /dev/null
+++ b/src/components/TableOfContents/PageGroupItem.tsx
@@ -0,0 +1,21 @@
+import { RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
+import { clsx } from 'clsx';
+import { PagesList } from './PagesList';
+
+export function PageGroupItem(props: {
+ page: RevisionPageGroup;
+ activePage: RevisionPageDocument;
+}) {
+ const { page, activePage } = props;
+
+ return (
+
+
+ {page.title}
+
+ {page.pages && page.pages.length ? (
+
+ ) : null}
+
+ );
+}
diff --git a/src/components/TableOfContents/PagesList.tsx b/src/components/TableOfContents/PagesList.tsx
new file mode 100644
index 000000000..4cf7db72c
--- /dev/null
+++ b/src/components/TableOfContents/PagesList.tsx
@@ -0,0 +1,24 @@
+import { RevisionPage, RevisionPageDocument } from '@gitbook/api';
+import { ClassValue, clsx } from 'clsx';
+import { PageDocumentItem } from './PageDocumentItem';
+import { PageGroupItem } from './PageGroupItem';
+
+export function PagesList(props: {
+ pages: RevisionPage[];
+ activePage: RevisionPageDocument;
+ style?: ClassValue;
+}) {
+ const { pages, activePage, style } = props;
+
+ return (
+
+ {pages.map((page) => {
+ if (page.type === 'group') {
+ return ;
+ }
+
+ return ;
+ })}
+
+ );
+}
diff --git a/src/components/TableOfContents/TableOfContents.tsx b/src/components/TableOfContents/TableOfContents.tsx
new file mode 100644
index 000000000..fbf0516fc
--- /dev/null
+++ b/src/components/TableOfContents/TableOfContents.tsx
@@ -0,0 +1,22 @@
+import { Revision, RevisionPageDocument } from '@gitbook/api';
+import { clsx } from 'clsx';
+import { PagesList } from './PagesList';
+import { Trademark } from './Trademark';
+
+export function TableOfContents(props: { revision: Revision; activePage: RevisionPageDocument }) {
+ const { revision, activePage } = props;
+
+ return (
+
+ );
+}
diff --git a/src/components/TableOfContents/Trademark.tsx b/src/components/TableOfContents/Trademark.tsx
new file mode 100644
index 000000000..0a1a89587
--- /dev/null
+++ b/src/components/TableOfContents/Trademark.tsx
@@ -0,0 +1,28 @@
+import clsx from 'clsx';
+
+/**
+ * Link to the GitBook platform.
+ */
+export function Trademark() {
+ return (
+
+ );
+}
diff --git a/src/components/TableOfContents/index.ts b/src/components/TableOfContents/index.ts
new file mode 100644
index 000000000..715a3a239
--- /dev/null
+++ b/src/components/TableOfContents/index.ts
@@ -0,0 +1 @@
+export * from './TableOfContents';
diff --git a/src/lib/api.ts b/src/lib/api.ts
new file mode 100644
index 000000000..56c198887
--- /dev/null
+++ b/src/lib/api.ts
@@ -0,0 +1,19 @@
+import 'server-only';
+
+import { headers } from 'next/headers';
+import { GitBookAPI } from '@gitbook/api';
+
+/**
+ * Create an API client for the current request.
+ */
+export function api(): GitBookAPI {
+ const headersList = headers();
+ const apiToken = headersList.get('x-gitbook-token');
+
+ const gitbook = new GitBookAPI({
+ authToken: apiToken || process.env.GITBOOK_TOKEN,
+ endpoint: process.env.GITBOOK_API_URL,
+ });
+
+ return gitbook;
+}
diff --git a/src/lib/links.ts b/src/lib/links.ts
new file mode 100644
index 000000000..889627d44
--- /dev/null
+++ b/src/lib/links.ts
@@ -0,0 +1,13 @@
+import 'server-only';
+
+import { headers } from 'next/headers';
+
+/**
+ * Create a link to a page path in the current space.
+ */
+export function pageHref(pagePath: string): string {
+ const headersList = headers();
+ const basePath = headersList.get('x-gitbook-basepath') ?? '';
+
+ return `${basePath}/${pagePath.startsWith('/') ? pagePath.slice(1) : pagePath}`;
+}
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 000000000..55fe554a3
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,25 @@
+import { NextResponse } from 'next/server';
+import { NextRequest } from 'next/server';
+
+/**
+ * Rewrite the request to extract the spaceId from the URL
+ * and pass it as a header.
+ */
+export function middleware(request: NextRequest) {
+ const url = new URL(request.url);
+ const [space, ...pathRest] = url.pathname.slice(1).split('/');
+
+ const headers = new Headers(request.headers);
+ headers.set('x-gitbook-space', space);
+ headers.set('x-gitbook-basepath', `/${space}`);
+
+ url.pathname = `/${pathRest.join('/')}`;
+
+ return NextResponse.rewrite(url, {
+ headers,
+ });
+}
+
+export const config = {
+ matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
+};
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 1af3b8f01..e23395f3d 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -1,20 +1,19 @@
-import type { Config } from 'tailwindcss'
+import type { Config } from 'tailwindcss';
+import colors from 'tailwindcss/colors';
const config: Config = {
- content: [
- './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
- './src/components/**/*.{js,ts,jsx,tsx,mdx}',
- './src/app/**/*.{js,ts,jsx,tsx,mdx}',
- ],
- theme: {
- extend: {
- backgroundImage: {
- 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
- 'gradient-conic':
- 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
- },
+ content: [
+ './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/components/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
+ ],
+ theme: {
+ extend: {
+ colors: {
+ primary: colors.blue,
+ },
+ },
},
- },
- plugins: [],
-}
-export default config
+ plugins: [],
+};
+export default config;
diff --git a/tsconfig.json b/tsconfig.json
index e59724b28..1445a451f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,27 +1,27 @@
{
- "compilerOptions": {
- "target": "es5",
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "preserve",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
}