From 1dc317f363e617a078927c5bdadacf0d38a22744 Mon Sep 17 00:00:00 2001 From: Tomek Gargula Date: Wed, 27 May 2026 10:15:52 +0200 Subject: [PATCH 1/5] feat: add icon support in tab items --- bun.lock | 6 +++--- package.json | 2 +- .../src/components/DocumentView/Tabs/DynamicTabs.tsx | 9 ++++++--- .../gitbook/src/components/DocumentView/Tabs/Tabs.tsx | 6 ++++++ packages/gitbook/src/components/PageBody/PageHeader.tsx | 1 - packages/gitbook/src/lib/icons/inline.ts | 2 +- packages/gitbook/src/lib/pages.test.ts | 5 +++++ 7 files changed, 22 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index 5ce64721dc..c5756be0f6 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@changesets/cli": "^2.31.0", - "turbo": "^2.9.12", + "turbo": "^2.9.14", "vercel": "50.37.3", }, }, @@ -359,7 +359,7 @@ "react-dom": "catalog:", }, "catalog": { - "@gitbook/api": "0.181.0", + "@gitbook/api": "0.182.0", "@scalar/api-client-react": "^1.3.46", "@tsconfig/node20": "^20.1.6", "@tsconfig/strictest": "^2.0.6", @@ -755,7 +755,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="], - "@gitbook/api": ["@gitbook/api@0.181.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-tj7bPzty9jlG3zKiZlmvvqalbTCtukmU/BlFB3HPRZM4jQrmLBc+Ya0rOyTpTaLJEH1BZy0U9HGsoCfbP5W8Fg=="], + "@gitbook/api": ["@gitbook/api@0.182.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-O5CgVzbRL2esrsQJ816UcHMqY22fsMFAeLRQ8Gapus4S+/teXk3nf5YU5T10o3112niSD3QeNp2uIcsyMGOweg=="], "@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"], diff --git a/package.json b/package.json index 4cd67423ee..b5d4373667 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "catalog": { "@tsconfig/strictest": "^2.0.6", "@tsconfig/node20": "^20.1.6", - "@gitbook/api": "0.181.0", + "@gitbook/api": "0.182.0", "@scalar/api-client-react": "^1.3.46", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", diff --git a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx index c5a7a07a7c..6ba79f9510 100644 --- a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx +++ b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx @@ -15,7 +15,7 @@ import { useLanguage } from '@/intl/client'; import { tString } from '@/intl/translate'; import { getLocalStorageItem, setLocalStorageItem } from '@/lib/browser'; import { tcls } from '@/lib/tailwind'; -import { Icon } from '@gitbook/icons'; +import { Icon, type IconName } from '@gitbook/icons'; import { useRouter } from 'next/navigation'; interface TabsState { @@ -56,6 +56,7 @@ const TITLES_MAX = 5; export interface TabsItem { id: string; title: string; + icon?: IconName; body: React.ReactNode; } @@ -319,6 +320,7 @@ function TabsDropdownMenu(props: { key={tab.id} onClick={() => onSelect(tab.id)} active={tab.id === activeTabId} + leadingIcon={tab.icon} > {tab.title} @@ -347,7 +349,8 @@ const TabItem = memo(function TabItem(props: { id={getTabButtonId(tab.id)} onClick={() => onSelect(tab.id)} > - {tab.title} + {tab.icon && } + {tab.title} ); }); @@ -411,7 +414,7 @@ function TabButton( {...rest} type="button" className={tcls( - 'relative inline-block max-w-full truncate px-3.5 py-2 font-medium text-sm transition-[color]', + 'relative inline-flex max-w-full items-center gap-1.5 px-3.5 py-2 font-medium text-sm transition-[color]', props.className )} /> diff --git a/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx index a7b85e23be..4151d0e9e8 100644 --- a/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx +++ b/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx @@ -1,4 +1,6 @@ import type { DocumentBlockTabs } from '@gitbook/api'; +import type { IconName } from '@gitbook/icons'; +import { validateIconName } from '@gitbook/icons/icons'; import { tcls } from '@/lib/tailwind'; @@ -20,9 +22,13 @@ export function Tabs(props: BlockProps) { throw new Error('Tab block is missing a key'); } + const icon: IconName | undefined = + tab.data.icon && validateIconName(tab.data.icon) ? tab.data.icon : undefined; + return { id: tab.meta?.id ?? tab.key, title: tab.data.title ?? '', + icon, body: ( 0; - // @ts-expect-error available in next API update const pageActionsEnabled = page.layout.actions !== false; // Show page actions if *any* of the actions are enabled diff --git a/packages/gitbook/src/lib/icons/inline.ts b/packages/gitbook/src/lib/icons/inline.ts index d8d000cddd..76e3d01144 100644 --- a/packages/gitbook/src/lib/icons/inline.ts +++ b/packages/gitbook/src/lib/icons/inline.ts @@ -358,7 +358,7 @@ function collectDocumentIconSourceRequests( const record = node as Record; - if (record.type === 'icon' || record.type === 'button') { + if (record.type === 'icon' || record.type === 'button' || record.type === 'tabs-item') { const data = record.data; if (data && typeof data === 'object') { addIconSourceRequest(requests, (data as Record).icon, iconStyle); diff --git a/packages/gitbook/src/lib/pages.test.ts b/packages/gitbook/src/lib/pages.test.ts index 114d396585..e1be6faeeb 100644 --- a/packages/gitbook/src/lib/pages.test.ts +++ b/packages/gitbook/src/lib/pages.test.ts @@ -85,6 +85,7 @@ describe('resolveFirstDocument', () => { width: RevisionPageLayoutOptionsWidth.Default, metadata: true, tags: true, + actions: true, }, }, ], @@ -137,6 +138,7 @@ describe('resolveFirstDocument', () => { width: RevisionPageLayoutOptionsWidth.Default, metadata: true, tags: true, + actions: true, }, }, ]; @@ -182,6 +184,7 @@ describe('resolvePagePath', () => { width: RevisionPageLayoutOptionsWidth.Default, metadata: true, tags: true, + actions: true, }, }, ]; @@ -257,6 +260,7 @@ describe('resolvePagePath', () => { width: RevisionPageLayoutOptionsWidth.Default, metadata: true, tags: true, + actions: true, }, }, ], @@ -374,6 +378,7 @@ function createDocumentPage(id: string, path: string, hidden = false): RevisionP width: RevisionPageLayoutOptionsWidth.Default, metadata: true, tags: true, + actions: true, }, }; } From 0507bacac9ec9c829d1b0be5b45b9cb85ded4b6c Mon Sep 17 00:00:00 2001 From: Tomek Gargula Date: Wed, 27 May 2026 10:17:31 +0200 Subject: [PATCH 2/5] fix: migrate force-revalidate to App Router to eliminate hybrid mode Moves the Pages Router API route to an App Router Route Handler, removing the pages/ directory entirely. This fixes the build error: Error: should not be imported outside of pages/_document which occurs in hybrid mode (App Router + Pages Router) when Next.js tries to statically generate the Pages Router /404 page. - Delete: packages/gitbook/src/pages/api/~gitbook/force-revalidate.ts - Create: packages/gitbook/src/app/~gitbook/force-revalidate/route.ts - Uses withVerifySignature + revalidatePath (App Router equivalents) Note: the endpoint URL changes from /api/~gitbook/force-revalidate to /~gitbook/force-revalidate. Co-Authored-By: Claude Sonnet 4.6 --- .changeset/fix-html-hybrid-mode.md | 5 ++ .../app/~gitbook/force-revalidate/route.ts | 33 +++++++++++++ .../pages/api/~gitbook/force-revalidate.ts | 49 ------------------- 3 files changed, 38 insertions(+), 49 deletions(-) create mode 100644 .changeset/fix-html-hybrid-mode.md create mode 100644 packages/gitbook/src/app/~gitbook/force-revalidate/route.ts delete mode 100644 packages/gitbook/src/pages/api/~gitbook/force-revalidate.ts diff --git a/.changeset/fix-html-hybrid-mode.md b/.changeset/fix-html-hybrid-mode.md new file mode 100644 index 0000000000..95ac4c7530 --- /dev/null +++ b/.changeset/fix-html-hybrid-mode.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Migrate force-revalidate endpoint from Pages Router to App Router to fix hybrid mode `` build error. diff --git a/packages/gitbook/src/app/~gitbook/force-revalidate/route.ts b/packages/gitbook/src/app/~gitbook/force-revalidate/route.ts new file mode 100644 index 0000000000..ed9d9de55c --- /dev/null +++ b/packages/gitbook/src/app/~gitbook/force-revalidate/route.ts @@ -0,0 +1,33 @@ +import { revalidatePath } from 'next/cache'; +import { type NextRequest, NextResponse } from 'next/server'; + +import { withVerifySignature } from '@/lib/routes'; + +interface JsonBody { + // The paths need to be the rewritten ones + paths: string[]; +} + +export async function POST(req: NextRequest) { + return withVerifySignature(req, async (body) => { + if (!body.paths || !Array.isArray(body.paths)) { + return NextResponse.json({ error: 'paths must be an array' }, { status: 400 }); + } + + const results = await Promise.allSettled( + body.paths.map((path) => { + // biome-ignore lint/suspicious/noConsole: we want to log here + console.log(`Revalidating path: ${path}`); + revalidatePath(path); + return Promise.resolve(); + }) + ); + + return NextResponse.json({ + success: results.every((result) => result.status === 'fulfilled'), + errors: results + .filter((result) => result.status === 'rejected') + .map((result) => (result as PromiseRejectedResult).reason), + }); + }); +} diff --git a/packages/gitbook/src/pages/api/~gitbook/force-revalidate.ts b/packages/gitbook/src/pages/api/~gitbook/force-revalidate.ts deleted file mode 100644 index 45e6c7eca1..0000000000 --- a/packages/gitbook/src/pages/api/~gitbook/force-revalidate.ts +++ /dev/null @@ -1,49 +0,0 @@ -import crypto from 'node:crypto'; -import type { NextApiRequest, NextApiResponse } from 'next'; - -interface JsonBody { - // The paths need to be the rewritten one, `res.revalidate` call don't go through the middleware - paths: string[]; -} - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - // Only allow POST requests - if (req.method !== 'POST') { - return res.status(405).json({ error: 'Method not allowed' }); - } - const signatureHeader = req.headers['x-gitbook-signature'] as string | undefined; - if (!signatureHeader) { - return res.status(400).json({ error: 'Missing signature header' }); - } - // We cannot use env from `@/v2/lib/env` here as it make it crash because of the import "server-only" in the file. - if (process.env.GITBOOK_SECRET) { - try { - const computedSignature = crypto - .createHmac('sha256', process.env.GITBOOK_SECRET) - .update(JSON.stringify(req.body)) - .digest('hex'); - - if (computedSignature === signatureHeader) { - const results = await Promise.allSettled( - (req.body as JsonBody).paths.map((path) => { - // biome-ignore lint/suspicious/noConsole: we want to log here - console.log(`Revalidating path: ${path}`); - return res.revalidate(path); - }) - ); - return res.status(200).json({ - success: results.every((result) => result.status === 'fulfilled'), - errors: results - .filter((result) => result.status === 'rejected') - .map((result) => (result as PromiseRejectedResult).reason), - }); - } - return res.status(401).json({ error: 'Invalid signature' }); - } catch (error) { - console.error('Error during revalidation:', error); - return res.status(400).json({ error: 'Invalid request or unable to parse JSON' }); - } - } - // If no secret is set, we do not allow revalidation - return res.status(403).json({ error: 'Revalidation is disabled' }); -} From 794e113d9824eaab3884c5c1d9dd01d23fe9981f Mon Sep 17 00:00:00 2001 From: Tomek Gargula Date: Wed, 27 May 2026 10:17:34 +0200 Subject: [PATCH 3/5] changeset --- .../gitbook/src/app/{ => api}/~gitbook/force-revalidate/route.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/gitbook/src/app/{ => api}/~gitbook/force-revalidate/route.ts (100%) diff --git a/packages/gitbook/src/app/~gitbook/force-revalidate/route.ts b/packages/gitbook/src/app/api/~gitbook/force-revalidate/route.ts similarity index 100% rename from packages/gitbook/src/app/~gitbook/force-revalidate/route.ts rename to packages/gitbook/src/app/api/~gitbook/force-revalidate/route.ts From ffd0564d2140895301741a34fde211619d852969 Mon Sep 17 00:00:00 2001 From: Tomek Gargula Date: Wed, 27 May 2026 14:29:10 +0200 Subject: [PATCH 4/5] chore: revert pages to api router change --- .changeset/fix-html-hybrid-mode.md | 5 -- .../api/~gitbook/force-revalidate/route.ts | 33 ------------- .../pages/api/~gitbook/force-revalidate.ts | 49 +++++++++++++++++++ 3 files changed, 49 insertions(+), 38 deletions(-) delete mode 100644 .changeset/fix-html-hybrid-mode.md delete mode 100644 packages/gitbook/src/app/api/~gitbook/force-revalidate/route.ts create mode 100644 packages/gitbook/src/pages/api/~gitbook/force-revalidate.ts diff --git a/.changeset/fix-html-hybrid-mode.md b/.changeset/fix-html-hybrid-mode.md deleted file mode 100644 index 95ac4c7530..0000000000 --- a/.changeset/fix-html-hybrid-mode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Migrate force-revalidate endpoint from Pages Router to App Router to fix hybrid mode `` build error. diff --git a/packages/gitbook/src/app/api/~gitbook/force-revalidate/route.ts b/packages/gitbook/src/app/api/~gitbook/force-revalidate/route.ts deleted file mode 100644 index ed9d9de55c..0000000000 --- a/packages/gitbook/src/app/api/~gitbook/force-revalidate/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { revalidatePath } from 'next/cache'; -import { type NextRequest, NextResponse } from 'next/server'; - -import { withVerifySignature } from '@/lib/routes'; - -interface JsonBody { - // The paths need to be the rewritten ones - paths: string[]; -} - -export async function POST(req: NextRequest) { - return withVerifySignature(req, async (body) => { - if (!body.paths || !Array.isArray(body.paths)) { - return NextResponse.json({ error: 'paths must be an array' }, { status: 400 }); - } - - const results = await Promise.allSettled( - body.paths.map((path) => { - // biome-ignore lint/suspicious/noConsole: we want to log here - console.log(`Revalidating path: ${path}`); - revalidatePath(path); - return Promise.resolve(); - }) - ); - - return NextResponse.json({ - success: results.every((result) => result.status === 'fulfilled'), - errors: results - .filter((result) => result.status === 'rejected') - .map((result) => (result as PromiseRejectedResult).reason), - }); - }); -} diff --git a/packages/gitbook/src/pages/api/~gitbook/force-revalidate.ts b/packages/gitbook/src/pages/api/~gitbook/force-revalidate.ts new file mode 100644 index 0000000000..45e6c7eca1 --- /dev/null +++ b/packages/gitbook/src/pages/api/~gitbook/force-revalidate.ts @@ -0,0 +1,49 @@ +import crypto from 'node:crypto'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +interface JsonBody { + // The paths need to be the rewritten one, `res.revalidate` call don't go through the middleware + paths: string[]; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + // Only allow POST requests + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + const signatureHeader = req.headers['x-gitbook-signature'] as string | undefined; + if (!signatureHeader) { + return res.status(400).json({ error: 'Missing signature header' }); + } + // We cannot use env from `@/v2/lib/env` here as it make it crash because of the import "server-only" in the file. + if (process.env.GITBOOK_SECRET) { + try { + const computedSignature = crypto + .createHmac('sha256', process.env.GITBOOK_SECRET) + .update(JSON.stringify(req.body)) + .digest('hex'); + + if (computedSignature === signatureHeader) { + const results = await Promise.allSettled( + (req.body as JsonBody).paths.map((path) => { + // biome-ignore lint/suspicious/noConsole: we want to log here + console.log(`Revalidating path: ${path}`); + return res.revalidate(path); + }) + ); + return res.status(200).json({ + success: results.every((result) => result.status === 'fulfilled'), + errors: results + .filter((result) => result.status === 'rejected') + .map((result) => (result as PromiseRejectedResult).reason), + }); + } + return res.status(401).json({ error: 'Invalid signature' }); + } catch (error) { + console.error('Error during revalidation:', error); + return res.status(400).json({ error: 'Invalid request or unable to parse JSON' }); + } + } + // If no secret is set, we do not allow revalidation + return res.status(403).json({ error: 'Revalidation is disabled' }); +} From c5607a07d891e2e2250fb8c0e84dac9e8cca5a51 Mon Sep 17 00:00:00 2001 From: Tomek Gargula Date: Wed, 27 May 2026 17:05:18 +0200 Subject: [PATCH 5/5] chore: add changeset --- .changeset/long-wombats-raise.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/long-wombats-raise.md diff --git a/.changeset/long-wombats-raise.md b/.changeset/long-wombats-raise.md new file mode 100644 index 0000000000..dc6c8725d0 --- /dev/null +++ b/.changeset/long-wombats-raise.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Add icon support in tab items