From b49caafa9cfa9ae96cb5dcf01cd5f468666ffcd1 Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Mon, 1 Dec 2025 14:48:48 +0100
Subject: [PATCH 01/10] Support for hidden sections (#3831)
---
.../gitbook/src/components/Header/Header.tsx | 26 +++----
.../src/components/SitePage/SitePage.tsx | 6 +-
.../SpaceLayout/SpaceLayout.test.ts | 1 +
.../components/SpaceLayout/SpaceLayout.tsx | 17 +++--
.../SpaceLayout/categorizeVariants.ts | 2 +-
packages/gitbook/src/lib/context.ts | 72 +++++++++++++++++--
6 files changed, 97 insertions(+), 27 deletions(-)
diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx
index dcd8e8fb7..89d6cce7a 100644
--- a/packages/gitbook/src/components/Header/Header.tsx
+++ b/packages/gitbook/src/components/Header/Header.tsx
@@ -25,12 +25,12 @@ export function Header(props: {
};
}) {
const { context, withTopHeader, variants } = props;
- const { siteSpace, siteSpaces, sections, customization } = context;
+ const { siteSpace, visibleSiteSpaces, visibleSections, customization } = context;
const withSections = Boolean(
- sections &&
- (sections.list.length > 1 || // Show section tabs if there are at least 2 sections or at least 1 section group
- sections.list.some((s) => s.object === 'site-section-group'))
+ visibleSections &&
+ (visibleSections.list.length > 1 || // Show section tabs if there are at least 2 sections or at least 1 section group
+ visibleSections.list.some((s) => s.object === 'site-section-group'))
);
return (
@@ -139,20 +139,22 @@ export function Header(props: {
style={customization.styling.search}
withVariants={variants.generic.length > 1}
withSiteVariants={
- sections?.list.some(
+ visibleSections?.list.some(
(s) =>
s.object === 'site-section' && s.siteSpaces.length > 1
) ?? false
}
- withSections={sections ? sections.list.length > 1 : false}
+ withSections={
+ visibleSections ? visibleSections.list.length > 1 : false
+ }
section={
- sections
- ? // Client-encode to avoid a serialisation issue that was causing the language selector to disappear
- encodeClientSiteSections(context, sections).current
+ visibleSections
+ ? // Client-encode to avoid a serialization issue that was causing the language selector to disappear
+ encodeClientSiteSections(context, visibleSections).current
: undefined
}
siteSpace={siteSpace}
- siteSpaces={siteSpaces}
+ siteSpaces={visibleSiteSpaces}
viewport={!withTopHeader ? 'mobile' : undefined}
/>
@@ -196,9 +198,9 @@ export function Header(props: {
- {sections && withSections ? (
+ {visibleSections && withSections ? (
-
+
{variants.translations.length > 1 ? (
0);
+ const withSections = Boolean(visibleSections && visibleSections.list.length > 0);
const document = await getPageDocument(context, page);
diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts
index 1b210ac66..a43c06d6a 100644
--- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts
+++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts
@@ -14,6 +14,7 @@ function makeContext(current: FakeSiteSpace, all: FakeSiteSpace[]) {
// Only the properties used by categorizeVariants are required for these tests
siteSpace: current,
siteSpaces: all,
+ visibleSiteSpaces: all,
} as unknown as Parameters[0];
}
diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
index 2f2f2870b..36097a3bc 100644
--- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
+++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
@@ -99,11 +99,11 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
*/
export function SpaceLayout(props: SpaceLayoutProps) {
const { context, children } = props;
- const { siteSpace, customization, sections, siteSpaces } = context;
+ const { siteSpace, customization, visibleSections, visibleSiteSpaces } = context;
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
- const withSections = Boolean(sections && sections.list.length > 1);
+ const withSections = Boolean(visibleSections && visibleSections.list.length > 1);
const variants = categorizeVariants(context);
const withFooter =
@@ -181,25 +181,28 @@ export function SpaceLayout(props: SpaceLayoutProps) {
style={CustomizationSearchStyle.Subtle}
withVariants={variants.generic.length > 1}
withSiteVariants={
- sections?.list.some(
+ visibleSections?.list.some(
(s) =>
s.object === 'site-section' &&
s.siteSpaces.length > 1
) ?? false
}
withSections={withSections}
- section={sections?.current}
+ section={visibleSections?.current}
siteSpace={siteSpace}
- siteSpaces={siteSpaces}
+ siteSpaces={visibleSiteSpaces}
className="max-lg:hidden"
viewport="desktop"
/>
)}
- {!withTopHeader && withSections && sections && (
+ {!withTopHeader && withSections && visibleSections && (
)}
{variants.generic.length > 1 ? (
diff --git a/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts b/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts
index 94df27104..81064f59f 100644
--- a/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts
+++ b/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts
@@ -5,7 +5,7 @@ import type { GitBookSiteContext } from '@/lib/context';
* Categorize the variants of the space into generic and translation variants.
*/
export function categorizeVariants(context: GitBookSiteContext) {
- const { siteSpace, siteSpaces } = context;
+ const { siteSpace, visibleSiteSpaces: siteSpaces } = context;
const currentLanguage = siteSpace.space.language;
// Get all languages of the variants.
diff --git a/packages/gitbook/src/lib/context.ts b/packages/gitbook/src/lib/context.ts
index 320c8e7c8..f619b39a6 100644
--- a/packages/gitbook/src/lib/context.ts
+++ b/packages/gitbook/src/lib/context.ts
@@ -120,9 +120,15 @@ export type GitBookSiteContext = GitBookSpaceContext & {
/** All site spaces in the current section / or entire site */
siteSpaces: SiteSpace[];
+ /** Site spaces that are not hidden (visible to visitors). */
+ visibleSiteSpaces: SiteSpace[];
+
/** Sections of the site. */
sections: null | SiteSections;
+ /** Sections filtered to visible site spaces only. */
+ visibleSections: null | SiteSections;
+
/** Customizations of the site. */
customization: SiteCustomizationSettings;
@@ -261,9 +267,16 @@ export async function fetchSiteContextByIds(
const sections = ids.siteSection
? parseSiteSectionsAndGroups(siteStructure, ids.siteSection)
: null;
+ const visibleSections = ids.siteSection
+ ? parseVisibleSiteSectionsAndGroups(siteStructure, ids.siteSection)
+ : null;
// Parse the current siteSpace and siteSpaces based on the site structure type.
- const { siteSpaces, siteSpace }: { siteSpaces: SiteSpace[]; siteSpace: SiteSpace } = (() => {
+ const {
+ siteSpaces,
+ siteSpace,
+ visibleSiteSpaces,
+ }: { siteSpaces: SiteSpace[]; siteSpace: SiteSpace; visibleSiteSpaces: SiteSpace[] } = (() => {
if (siteStructure.type === 'siteSpaces') {
const siteSpaces = siteStructure.structure;
const siteSpace = siteSpaces.find((siteSpace) => siteSpace.id === ids.siteSpace);
@@ -274,7 +287,7 @@ export async function fetchSiteContextByIds(
);
}
- return { siteSpaces: filterHiddenSiteSpaces(siteSpaces), siteSpace };
+ return { siteSpaces, siteSpace, visibleSiteSpaces: filterHiddenSiteSpaces(siteSpaces) };
}
if (siteStructure.type === 'sections') {
@@ -295,7 +308,11 @@ export async function fetchSiteContextByIds(
);
}
- return { siteSpaces: filterHiddenSiteSpaces(siteSpaces), siteSpace };
+ return {
+ siteSpaces,
+ siteSpace,
+ visibleSiteSpaces: filterHiddenSiteSpaces(siteSpaces),
+ };
}
// @ts-expect-error
@@ -327,10 +344,12 @@ export async function fetchSiteContextByIds(
organizationId: ids.organization,
site,
siteSpaces,
+ visibleSiteSpaces,
siteSpace,
customization,
structure: siteStructure,
sections,
+ visibleSections,
scripts,
contextId: ids.contextId,
isFallback: ids.isFallback,
@@ -434,13 +453,58 @@ function filterHiddenSiteSpaces(siteSpaces: SiteSpace[]): SiteSpace[] {
}
function parseSiteSectionsAndGroups(structure: SiteStructure, siteSectionId: string) {
- const sectionsAndGroups = getSiteStructureSections(structure, { ignoreGroups: false });
+ const sectionsAndGroups = getSiteStructureSections(structure);
const section = parseCurrentSection(structure, siteSectionId);
assert(section, `couldn't find section "${siteSectionId}" in site structure`);
return { list: sectionsAndGroups, current: section } satisfies SiteSections;
}
+function parseVisibleSiteSectionsAndGroups(structure: SiteStructure, siteSectionId: string) {
+ const { list: sectionsAndGroups, current: section } = parseSiteSectionsAndGroups(
+ structure,
+ siteSectionId
+ );
+ const visibleSectionsAndGroups = filterSectionsAndGroupsWithHiddenSiteSpaces(sectionsAndGroups);
+ const current = section && !sectionHasOnlyHiddenSiteSpaces(section) ? section : null;
+ assert(current, `couldn't find section "${siteSectionId}" in site structure`);
+ return { list: visibleSectionsAndGroups, current } satisfies SiteSections;
+}
+
function parseCurrentSection(structure: SiteStructure, siteSectionId: string) {
const sections = getSiteStructureSections(structure, { ignoreGroups: true });
return sections.find((section) => section.id === siteSectionId);
}
+
+type SectionOrGroup = SiteSection | SiteSectionGroup;
+
+/**
+ * Filter out sections where all site spaces are hidden and groups that become empty after filtering.
+ */
+function filterSectionsAndGroupsWithHiddenSiteSpaces(
+ sectionsOrGroups: SectionOrGroup[]
+): SectionOrGroup[] {
+ return sectionsOrGroups
+ .map((entry) => {
+ if (entry.object === 'site-section') {
+ return sectionHasOnlyHiddenSiteSpaces(entry) ? null : entry;
+ }
+
+ const visibleChildren: SectionOrGroup[] = filterSectionsAndGroupsWithHiddenSiteSpaces(
+ entry.children
+ );
+
+ if (visibleChildren.length === 0) {
+ return null;
+ }
+
+ return {
+ ...entry,
+ children: visibleChildren,
+ };
+ })
+ .filter((entry): entry is SiteSection | SiteSectionGroup => Boolean(entry));
+}
+
+function sectionHasOnlyHiddenSiteSpaces(section: SiteSection) {
+ return section.siteSpaces.every((siteSpace) => siteSpace.hidden);
+}
From 9e1d7b296b8beb3e1e01a1f69b67e6dd86bd7db8 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein
Date: Tue, 2 Dec 2025 10:24:18 +0100
Subject: [PATCH 02/10] Update AIChat context card open delay (#3834)
---
packages/gitbook/src/components/AIChat/AIChatInput.tsx | 2 +-
packages/gitbook/src/components/primitives/HoverCard.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/gitbook/src/components/AIChat/AIChatInput.tsx b/packages/gitbook/src/components/AIChat/AIChatInput.tsx
index 98f586fcd..edee25d8e 100644
--- a/packages/gitbook/src/components/AIChat/AIChatInput.tsx
+++ b/packages/gitbook/src/components/AIChat/AIChatInput.tsx
@@ -108,7 +108,7 @@ export function AIChatInput(props: {
) : null}
-
+
Date: Tue, 2 Dec 2025 11:37:55 +0100
Subject: [PATCH 03/10] Fix hidden section not found (#3836)
---
.changeset/tiny-carrots-rush.md | 5 +++++
packages/gitbook/src/lib/context.ts | 4 +---
2 files changed, 6 insertions(+), 3 deletions(-)
create mode 100644 .changeset/tiny-carrots-rush.md
diff --git a/.changeset/tiny-carrots-rush.md b/.changeset/tiny-carrots-rush.md
new file mode 100644
index 000000000..faf9bc5b4
--- /dev/null
+++ b/.changeset/tiny-carrots-rush.md
@@ -0,0 +1,5 @@
+---
+'gitbook': patch
+---
+
+Fix hidden section not found
diff --git a/packages/gitbook/src/lib/context.ts b/packages/gitbook/src/lib/context.ts
index f619b39a6..bfb5f0fb8 100644
--- a/packages/gitbook/src/lib/context.ts
+++ b/packages/gitbook/src/lib/context.ts
@@ -465,9 +465,7 @@ function parseVisibleSiteSectionsAndGroups(structure: SiteStructure, siteSection
siteSectionId
);
const visibleSectionsAndGroups = filterSectionsAndGroupsWithHiddenSiteSpaces(sectionsAndGroups);
- const current = section && !sectionHasOnlyHiddenSiteSpaces(section) ? section : null;
- assert(current, `couldn't find section "${siteSectionId}" in site structure`);
- return { list: visibleSectionsAndGroups, current } satisfies SiteSections;
+ return { list: visibleSectionsAndGroups, current: section } satisfies SiteSections;
}
function parseCurrentSection(structure: SiteStructure, siteSectionId: string) {
From 9e062b10d3d1d75d6f7d21e9f990aa07bf480070 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein
Date: Wed, 3 Dec 2025 10:39:41 +0100
Subject: [PATCH 04/10] Make semantic text colors more vibrant (#3838)
---
.../components/DocumentView/utils/colors.ts | 22 +++++++++----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/packages/gitbook/src/components/DocumentView/utils/colors.ts b/packages/gitbook/src/components/DocumentView/utils/colors.ts
index 1b922ed46..15d90d158 100644
--- a/packages/gitbook/src/components/DocumentView/utils/colors.ts
+++ b/packages/gitbook/src/components/DocumentView/utils/colors.ts
@@ -3,17 +3,17 @@ import type { DocumentMarkColor } from '@gitbook/api';
export const textColorToStyle: { [color in DocumentMarkColor['data']['text']]: ClassValue } = {
default: [],
- blue: ['text-blue-500'],
- red: ['text-red-500'],
- green: ['text-green-500'],
- yellow: ['text-yellow-600'],
- purple: ['text-purple-500'],
- orange: ['text-orange-500'],
- $primary: ['text-primary'],
- $info: ['text-info'],
- $success: ['text-success'],
- $warning: ['text-warning'],
- $danger: ['text-danger'],
+ blue: ['text-blue-500 contrast-more:text-blue-800'],
+ red: ['text-red-500 contrast-more:text-red-800'],
+ green: ['text-green-500 contrast-more:text-green-800'],
+ yellow: ['text-yellow-600 contrast-more:text-yellow-800'],
+ purple: ['text-purple-500 contrast-more:text-purple-800'],
+ orange: ['text-orange-500 contrast-more:text-orange-800'],
+ $primary: ['text-primary-subtle contrast-more:text-primary'],
+ $info: ['text-info-subtle contrast-more:text-info'],
+ $success: ['text-success-subtle contrast-more:text-success'],
+ $warning: ['text-warning-subtle contrast-more:text-warning'],
+ $danger: ['text-danger-subtle contrast-more:text-danger'],
};
export const backgroundColorToStyle: {
From 27b9f7817bf2a06433fedc3466a2c1e90ec47617 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein
Date: Wed, 3 Dec 2025 11:43:02 +0100
Subject: [PATCH 05/10] Remove corner radius of hint block with heading (#3839)
---
packages/gitbook/src/components/DocumentView/Hint.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/packages/gitbook/src/components/DocumentView/Hint.tsx b/packages/gitbook/src/components/DocumentView/Hint.tsx
index fc65638e2..978dd441d 100644
--- a/packages/gitbook/src/components/DocumentView/Hint.tsx
+++ b/packages/gitbook/src/components/DocumentView/Hint.tsx
@@ -32,10 +32,9 @@ export function Hint({
className={tcls(
'hint',
'transition-colors',
- 'rounded-md',
- hasHeading ? 'rounded-l-sm' : null,
- 'straight-corners:rounded-none',
+ 'rounded-corners:rounded-md',
'circular-corners:rounded-xl',
+ hasHeading ? 'circular-corners:rounded-l-none rounded-corners:rounded-l-none' : '',
'overflow-hidden',
hasHeading ? ['border-l-2', hintStyle.containerWithHeader] : hintStyle.container,
From d460dc4b39a2fca0631cb7a18a26b6a5cac45445 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein
Date: Wed, 3 Dec 2025 13:18:44 +0100
Subject: [PATCH 06/10] Update Stepper styling for theme-muted (#3841)
---
.../gitbook/src/components/DocumentView/StepperStep.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/gitbook/src/components/DocumentView/StepperStep.tsx b/packages/gitbook/src/components/DocumentView/StepperStep.tsx
index 2c16ab923..4ce319125 100644
--- a/packages/gitbook/src/components/DocumentView/StepperStep.tsx
+++ b/packages/gitbook/src/components/DocumentView/StepperStep.tsx
@@ -36,13 +36,13 @@ export function StepperStep(props: BlockProps) {
Date: Wed, 3 Dec 2025 13:20:22 +0100
Subject: [PATCH 07/10] Support new "link title" page option (#3798)
---
bun.lock | 5 ++---
package.json | 2 +-
.../TableOfContents/encodeClientTableOfContents.ts | 2 +-
packages/gitbook/src/lib/references.tsx | 8 ++++----
4 files changed, 8 insertions(+), 9 deletions(-)
diff --git a/bun.lock b/bun.lock
index 2f67c7d8c..5b9d520f0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
- "configVersion": 0,
"workspaces": {
"": {
"name": "gitbook",
@@ -346,7 +345,7 @@
"react-dom": "catalog:",
},
"catalog": {
- "@gitbook/api": "0.151.0",
+ "@gitbook/api": "0.153.0",
"@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6",
@@ -727,7 +726,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
- "@gitbook/api": ["@gitbook/api@0.151.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-5d9+rZ2u6CKKIiVHO1Toyk+7wHtTOXmP0+sVIE3teRkceX4z5FGIIpa4XsFeKC9XeosncvehdshPaOGQtSDpTQ=="],
+ "@gitbook/api": ["@gitbook/api@0.153.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-ArNPFoqwId4Flicz8xPEdGqXQkzxyxP7S8Uv3wIfCX2e4cLhpcS7xCJGEHOJrqe1tNs1ovT1N2MWfpdIqzXqig=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
diff --git a/package.json b/package.json
index b83bbd2e9..54ec8c2aa 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
- "@gitbook/api": "0.151.0",
+ "@gitbook/api": "0.153.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/TableOfContents/encodeClientTableOfContents.ts b/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts
index 3c3820c3f..d087e80dc 100644
--- a/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts
+++ b/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts
@@ -72,7 +72,7 @@ export async function encodeClientTableOfContents(
result.push(
removeUndefined({
id: page.id,
- title: page.title,
+ title: page.linkTitle ?? page.title,
href,
emoji: page.emoji,
icon: page.icon,
diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx
index a0051ead9..c9b6aefa5 100644
--- a/packages/gitbook/src/lib/references.tsx
+++ b/packages/gitbook/src/lib/references.tsx
@@ -129,7 +129,7 @@ export async function resolveContentRef(
const page = resolvePageResult?.page;
const ancestors =
resolvePageResult?.ancestors.map((ancestor) => ({
- label: ancestor.title,
+ label: ancestor.linkTitle ?? ancestor.title,
icon:
ancestor.emoji || ancestor.icon ? (
,
href,
});
@@ -177,7 +177,7 @@ export async function resolveContentRef(
parentPage && contentRef.page === parentPage.id && parentPage.type === 'group'
? parentPage
: page;
- text = pageOrGroup.title;
+ text = pageOrGroup.linkTitle ?? pageOrGroup.title;
emoji = isCurrentPage ? undefined : page.emoji;
icon = ;
}
From 3fbd6f0d503d4077eefe1bbb26f2b30d11c6aea3 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein
Date: Wed, 3 Dec 2025 13:21:38 +0100
Subject: [PATCH 08/10] Hide empty Footer correctly (#3842)
---
packages/gitbook/src/components/Footer/Footer.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/gitbook/src/components/Footer/Footer.tsx b/packages/gitbook/src/components/Footer/Footer.tsx
index 27a64c0b8..e909bba3d 100644
--- a/packages/gitbook/src/components/Footer/Footer.tsx
+++ b/packages/gitbook/src/components/Footer/Footer.tsx
@@ -29,7 +29,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
className={tcls(
'border-tint-subtle border-t',
// If the footer only contains a mode toggle, we only show it on smaller screens
- mobileOnly ? '@7xl:hidden' : null
+ mobileOnly ? 'xl:hidden' : null
)}
>
From 1430ceebea974666116b169e06b9bc4e85925494 Mon Sep 17 00:00:00 2001
From: Zeno Kapitein
Date: Thu, 4 Dec 2025 10:01:25 +0100
Subject: [PATCH 09/10] Update Docs Embed with new styling and tabs (#3823)
---
.changeset/grumpy-peas-hammer.md | 6 +
packages/embed/README.md | 378 +++++++++++++++++-
packages/embed/src/client/createGitBook.ts | 2 +-
.../embed/src/client/createGitBookFrame.ts | 5 +-
packages/embed/src/client/protocol.ts | 20 +-
packages/embed/src/react/GitBookFrame.tsx | 9 +-
packages/embed/src/standalone/index.ts | 75 ++--
packages/embed/src/standalone/style.css | 12 +
.../~gitbook/embed/assistant/page.tsx | 25 +-
.../[siteData]/~gitbook/embed/page.tsx | 21 +
.../~gitbook/embed/assistant/page.tsx | 25 +-
.../[siteData]/~gitbook/embed/page.tsx | 23 ++
packages/gitbook/src/components/AI/useAI.tsx | 1 +
.../gitbook/src/components/AI/useAIChat.tsx | 64 ++-
.../gitbook/src/components/AIChat/AIChat.tsx | 54 +--
.../components/AIChat/AIChatControlButton.tsx | 37 +-
.../Embeddable/EmbeddableAIChat.tsx | 75 +++-
.../Embeddable/EmbeddableAssistantPage.tsx | 9 +-
.../Embeddable/EmbeddableDocsPage.tsx | 90 +++--
.../EmbeddableDocsPageControlButtons.tsx | 21 +
.../components/Embeddable/EmbeddableFrame.tsx | 31 +-
.../Embeddable/EmbeddableIframeAPI.tsx | 117 +++++-
.../Embeddable/EmbeddableRootLayout.tsx | 26 +-
.../components/Header/HeaderMobileMenu.tsx | 31 +-
.../SiteSections/SiteSectionTabs.tsx | 17 +-
.../TableOfContents/TableOfContents.tsx | 6 +-
.../components/TableOfContents/Trademark.tsx | 36 +-
.../src/components/primitives/Button.tsx | 43 +-
.../src/components/primitives/Link.tsx | 4 +-
.../src/components/primitives/Tooltip.tsx | 6 +
packages/gitbook/src/lib/embeddable.ts | 11 +
packages/gitbook/src/middleware.ts | 1 +
32 files changed, 1045 insertions(+), 236 deletions(-)
create mode 100644 .changeset/grumpy-peas-hammer.md
create mode 100644 packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx
create mode 100644 packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx
create mode 100644 packages/gitbook/src/components/Embeddable/EmbeddableDocsPageControlButtons.tsx
diff --git a/.changeset/grumpy-peas-hammer.md b/.changeset/grumpy-peas-hammer.md
new file mode 100644
index 000000000..abc1f9a18
--- /dev/null
+++ b/.changeset/grumpy-peas-hammer.md
@@ -0,0 +1,6 @@
+---
+"@gitbook/embed": minor
+"gitbook": patch
+---
+
+Improve Docs Embed with separate Assistant and Docs tabs
diff --git a/packages/embed/README.md b/packages/embed/README.md
index 45efed3ac..089b603a3 100644
--- a/packages/embed/README.md
+++ b/packages/embed/README.md
@@ -1,24 +1,61 @@
-# `@gitbook/embed`
+# GitBook Docs Embed (`@gitbook/embed`)
-Embed the GitBook Docs Assistant in your product or website.
+Embed your GitBook docs in your product or website.
+
+The Docs Embed can contain two tabs:
+- **Assistant**: The [GitBook Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) - an AI-powered chat interface to help users find answers
+- **Docs**: A browser for navigating your documentation site
+
+The embed is set up automatically based on your site's configuration. You can optionally customize and override the configuration with custom actions, tools, suggested questions, [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access), and more. See the [Configuration](#configuration) section for all available options.
# Usage
-## As a script from your docs site
+## As a standalone script from your docs site
-All GitBook docs site includes a script to easily embed the docs assistant as a widget on your website.
+All GitBook docs sites include a script to easily add the Docs Embed as a widget on your website.
The script is served at `https://docs.company.com/~gitbook/embed/script.js`.
-You can find the embed script from your docs site settings, or you can copy the following and replace the `docs.company.com` by your docs site hostname.
+You can find the embed script from your docs site settings, or you can copy the following and replace `docs.company.com` with your docs site hostname.
```html
```
+The standalone script provides a global `GitBook` function. See the [API Reference](#api-reference) section for all available methods.
+
+### Example: Configuring the widget
+
+```javascript
+GitBook('configure', {
+ button: {
+ label: 'Ask',
+ icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
+ },
+ tabs: ['assistant', 'docs'],
+ actions: [
+ {
+ icon: 'circle-question',
+ label: 'Contact Support',
+ onClick: () => window.open('https://support.example.com', '_blank')
+ }
+ ],
+ greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
+ suggestions: ['What is GitBook?', 'How do I get started?'],
+ tools: [/* ... */]
+});
+```
+
+See the [Configuration](#configuration) section for all available options.
+
## As a package from NPM
Install the package: `npm install @gitbook/embed` and import it in your web application:
@@ -30,10 +67,46 @@ const gitbook = createGitBook({
siteURL: 'https://docs.company.com'
});
+// Create an iframe and get its URL
const iframe = document.createElement('iframe');
-iframe.src = gitbook.getFrameURL();
+iframe.src = gitbook.getFrameURL({
+ visitor: {
+ token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
+ unsignedClaims: { // Optional: custom claims for dynamic expressions
+ userId: '123',
+ plan: 'premium'
+ }
+ }
+});
+// Create a frame client to communicate with the iframe
const frame = gitbook.createFrame(iframe);
+
+// Use the frame client methods
+frame.navigateToPage('/getting-started'); // Navigate to a page in the docs tab
+frame.navigateToAssistant(); // Switch to the assistant tab
+frame.postUserMessage('How do I get started?');
+frame.clearChat();
+
+// Configure the embed (see Configuration section for all options)
+frame.configure({
+ tabs: ['assistant', 'docs'],
+ actions: [
+ {
+ icon: 'circle-question',
+ label: 'Contact Support',
+ onClick: () => window.open('https://support.example.com', '_blank')
+ }
+ ],
+ greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
+ suggestions: ['What is GitBook?', 'How do I get started?'],
+ tools: [/* ... */]
+});
+
+// Listen to events
+frame.on('close', () => {
+ console.log('Frame closed');
+});
```
## As React components
@@ -41,9 +114,298 @@ const frame = gitbook.createFrame(iframe);
After installing the NPM package, you can import prebuilt React components:
```tsx
-import { GitBookProvider, GitBookAssistantFrame } from '@gitbook/embed/react';
+import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
-
+ window.open('https://support.example.com', '_blank')
+ }
+ ]}
+ tools={[/* ... */]}
+ />
```
+
+You can also use the `useGitBook` hook to access the client:
+
+```tsx
+import { useGitBook } from '@gitbook/embed/react';
+
+function MyComponent() {
+ const gitbook = useGitBook();
+ const frameURL = gitbook.getFrameURL({ visitor: { token: '...' } });
+ // ...
+}
+```
+
+# API Reference
+
+## Method Comparison
+
+| Method | Standalone Script | NPM Package | React Components |
+|--------|------------------|-------------|------------------|
+| **Initialize** | `GitBook('init', options, frameOptions)` | `createGitBook(options)` | `` |
+| **Get frame URL** | ❌ (handled internally) | `client.getFrameURL(options)` | `useGitBook().getFrameURL(options)` |
+| **Create frame client** | ❌ (handled internally) | `client.createFrame(iframe)` | `useGitBook().createFrame(iframe)` |
+| **Show/Hide widget** | `GitBook('show')` / `GitBook('hide')` | ❌ | ❌ |
+| **Open/Close window** | `GitBook('open')` / `GitBook('close')` / `GitBook('toggle')` | ❌ | ❌ |
+| **Navigate to page** | `GitBook('navigateToPage', path)` | `frame.navigateToPage(path)` | Via frame client |
+| **Navigate to assistant** | `GitBook('navigateToAssistant')` | `frame.navigateToAssistant()` | Via frame client |
+| **Post message** | `GitBook('postUserMessage', message)` | `frame.postUserMessage(message)` | Via frame client |
+| **Clear chat** | `GitBook('clearChat')` | `frame.clearChat()` | Via frame client |
+| **Configure** | `GitBook('configure', settings)` | `frame.configure(settings)` | Props on `` |
+| **Event listeners** | ❌ | `frame.on(event, listener)` | Via frame client |
+| **Unload** | `GitBook('unload')` | ❌ | ❌ |
+
+## Method Signatures
+
+### Standalone Script
+
+- `GitBook('init', options: { siteURL: string }, frameOptions?: { visitor?: {...} })` - Initialize widget
+- `GitBook('show')` - Show widget button
+- `GitBook('hide')` - Hide widget button
+- `GitBook('open')` - Open widget window
+- `GitBook('close')` - Close widget window
+- `GitBook('toggle')` - Toggle widget window
+- `GitBook('navigateToPage', path: string)` - Navigate to page
+- `GitBook('navigateToAssistant')` - Navigate to assistant tab
+- `GitBook('postUserMessage', message: string)` - Post message to chat
+- `GitBook('clearChat')` - Clear chat history
+- `GitBook('configure', settings: {...})` - Configure widget
+- `GitBook('unload')` - Unload widget
+
+### NPM Package
+
+**Client Factory:**
+- `createGitBook(options: { siteURL: string })` → `GitBookClient`
+- `client.getFrameURL(options?: { visitor?: {...} })` → `string`
+- `client.createFrame(iframe: HTMLIFrameElement)` → `GitBookFrameClient`
+
+**Frame Client:**
+- `frame.navigateToPage(path: string)` → `void`
+- `frame.navigateToAssistant()` → `void`
+- `frame.postUserMessage(message: string)` → `void`
+- `frame.clearChat()` → `void`
+- `frame.configure(settings: Partial)` → `void`
+- `frame.on(event: string, listener: Function)` → `() => void` (unsubscribe)
+
+### React Components
+
+**Components:**
+- `` - Provider component
+- `` - Frame component (accepts all config options as props)
+
+**Hooks:**
+- `useGitBook()` → `GitBookClient` (must be used within ``)
+
+# Configuration
+
+Configuration options are available across usage methods as follows:
+- **Standalone script**: via `GitBook('configure', {...})`
+- **NPM package**: via `frame.configure({...})`
+- **React components**: via props on ``
+
+### `tabs`
+
+Available in: Standalone script, NPM package, React components
+
+Override which tabs are displayed. Defaults to your site's configuration.
+
+- **Type**: `('assistant' | 'docs')[]`
+- **Options**:
+ - `['assistant', 'docs']` - Show both tabs
+ - `['assistant']` - Show only the assistant tab
+ - `['docs']` - Show only the docs tab
+
+```javascript
+tabs: ['assistant', 'docs']
+```
+
+### `actions`
+
+Available in: Standalone script, NPM package, React components
+
+Custom action buttons rendered in the sidebar alongside tabs. Each action button triggers a callback when clicked.
+
+**Note**: This prop was previously named `buttons`. Use `actions` instead, it has the same functionality.
+
+- **Type**: `GitBookEmbeddableActionDefinition[]`
+- **Properties**:
+ - `icon`: `string` - Icon name. Any [FontAwesome icon](https://fontawesome.com/search) is supported. (e.g., `'rocket'`, `'comments'`, `'user-circle'`, ...)
+ - `label`: `string` - Button label text
+ - `onClick`: `() => void | Promise` - Callback function when clicked
+
+```javascript
+actions: [
+ {
+ icon: 'comments',
+ label: 'Contact Support',
+ onClick: () => window.open('https://support.example.com', '_blank')
+ },
+ {
+ icon: 'rocket',
+ label: 'Get started',
+ onClick: () => {
+ GitBook('navigateToPage', '/getting-started');
+ }
+ }
+]
+```
+
+### `greeting`
+
+Available in: Standalone script, NPM package, React components
+
+Welcome message displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) tab.
+
+- **Type**: `{ title: string, subtitle: string }`
+
+```javascript
+greeting: {
+ title: 'Welcome!',
+ subtitle: 'How can I help you today?'
+}
+```
+
+### `suggestions`
+
+Available in: Standalone script, NPM package, React components
+
+Suggested questions displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) welcome screen.
+
+- **Type**: `string[]`
+
+```javascript
+suggestions: [
+ 'What is GitBook?',
+ 'How do I get started?',
+ 'What can you do?'
+]
+```
+
+### `tools`
+
+Available in: Standalone script, NPM package, React components
+
+Custom AI tools to extend the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant). Tools allow the assistant to execute functions and integrate with your own systems.
+
+**Note**: In addition to custom tools, the Assistant will always have access to any [MCP servers you define](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant#extend-gitbook-assistant-with-mcp-servers) in your site's AI settings.
+
+- **Type**: `GitBookToolDefinition[]`
+- **Properties**:
+ - `name`: `string` - Unique tool identifier
+ - `description`: `string` - Description of what the tool does (used by the AI to decide when and how to use it).
+ - `inputSchema`: `object` - JSON schema defining the tool's input parameters
+ - `execute`: `(input: object) => Promise<{ output: any, summary: string }>` - Async function that executes the tool.
+ - `output`: The result of the tool execution, provided to the AI to continue working with. Not shown to the user.
+ - `summary`: The visual summary of the tool execution, shown in the user's chat window.
+ - `confirmation`: `{ icon?: string, label: string }` (optional) - Confirmation button shown before execution, useful for actions that require the user's express approval.
+
+```javascript
+tools: [
+ {
+ name: 'get_user_info',
+ description: 'Get information about the current user',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ userId: {
+ type: 'string',
+ description: 'The user ID to look up'
+ }
+ },
+ required: ['userId']
+ },
+ execute: async (input) => {
+ const user = await fetch(`/api/users/${input.userId}`).then(r => r.json());
+ return {
+ output: { name: user.name, plan: user.plan },
+ summary: `Retrieved info for user ${user.name}`
+ };
+ }
+ },
+ {
+ name: 'create_ticket',
+ description: 'Create a support ticket',
+ confirmation: {
+ icon: 'circle-question',
+ label: 'Create support ticket?'
+ },
+ inputSchema: {
+ type: 'object',
+ properties: {
+ subject: { type: 'string' },
+ description: { type: 'string' }
+ },
+ required: ['subject', 'description']
+ },
+ execute: async (input) => {
+ const ticket = await fetch('/api/tickets', {
+ method: 'POST',
+ body: JSON.stringify(input)
+ }).then(r => r.json());
+ return {
+ output: { ticketId: ticket.id },
+ summary: `Created ticket #${ticket.id}`
+ };
+ }
+ }
+]
+```
+
+### `visitor` (Authenticated Access)
+
+Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
+
+[Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access) options passed when creating the frame URL. Used for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) and [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access).
+
+**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
+
+**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)`
+**NPM package**: Pass to `getFrameURL({ visitor: {...} })`
+**React components**: Pass as the `visitor` prop on ``
+
+- **Type**: `{ token?: string, unsignedClaims?: Record }`
+- **Properties**:
+ - `token`: `string` (optional) - Signed JWT token for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) or [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access)
+ - `unsignedClaims`: `Record` (optional) - Unsigned claims that can be used in dynamic expressions via `visitor.claims.unsigned.`
+
+```javascript
+visitor: {
+ token: 'your-jwt-token',
+ unsignedClaims: {
+ userId: '123',
+ plan: 'premium',
+ role: 'admin'
+ }
+}
+```
+
+### `button`
+
+Available in: Standalone script only
+
+Configure the widget button for the standalone script. This option is not available when using the NPM package or React components, since they can be customized completely.
+
+- **Type**: `{ label: string, icon: 'assistant' | 'sparkle' | 'help' | 'book' }`
+- **Properties**:
+ - `label`: `string` - Button label text
+ - `icon`: `'assistant' | 'sparkle' | 'help' | 'book'` - Icon displayed on the button. Choose from one of 4 presets.
+
+```javascript
+button: {
+ label: 'Ask',
+ icon: 'assistant'
+}
+```
diff --git a/packages/embed/src/client/createGitBook.ts b/packages/embed/src/client/createGitBook.ts
index bcd57a096..fc39c71bb 100644
--- a/packages/embed/src/client/createGitBook.ts
+++ b/packages/embed/src/client/createGitBook.ts
@@ -40,7 +40,7 @@ export function createGitBook(options: CreateGitBookOptions) {
const client: GitBookClient = {
getFrameURL: (frameOptions) => {
const url = new URL(options.siteURL);
- url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed/assistant`;
+ url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed`;
if (frameOptions.visitor?.token) {
url.searchParams.set('token', frameOptions.visitor.token);
diff --git a/packages/embed/src/client/createGitBookFrame.ts b/packages/embed/src/client/createGitBookFrame.ts
index baaabf647..84908521b 100644
--- a/packages/embed/src/client/createGitBookFrame.ts
+++ b/packages/embed/src/client/createGitBookFrame.ts
@@ -64,8 +64,9 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
const events = new Map void>>();
const configuration: GitBookEmbeddableConfiguration = {
- buttons: [],
- welcomeMessage: '',
+ tabs: ['assistant', 'docs'],
+ actions: [],
+ greeting: { title: '', subtitle: '' },
suggestions: [],
tools: [],
};
diff --git a/packages/embed/src/client/protocol.ts b/packages/embed/src/client/protocol.ts
index f19b400e3..ea7882b72 100644
--- a/packages/embed/src/client/protocol.ts
+++ b/packages/embed/src/client/protocol.ts
@@ -23,7 +23,7 @@ export type GitBookToolDefinition = AIToolDefinition & {
/**
* Custom button definition to be passed to the embeddable GitBook.
*/
-export type GitBookEmbeddableButtonDefinition = {
+export type GitBookEmbeddableActionDefinition = {
/**
* Icon to be displayed in the button.
*/
@@ -41,16 +41,26 @@ export type GitBookEmbeddableButtonDefinition = {
};
/**
- * Overall configuration for the layout of the embeddable GitBook.
+ * Overall configuration for the layout of the GitBook embed.
*/
export type GitBookEmbeddableConfiguration = {
+ /** Tabs to display in the embed (if enabled on the site). */
+ tabs: ('assistant' | 'docs')[];
+
+ /** Additional buttons to be displayed in the header of the GitBook embed. */
+ actions: GitBookEmbeddableActionDefinition[];
+
/**
- * Buttons to be displayed in the header of the embeddable GitBook.
+ * Additional buttons to be displayed in the header of the GitBook embed.
+ * @deprecated Use `actions` instead.
*/
- buttons: GitBookEmbeddableButtonDefinition[];
+ buttons?: GitBookEmbeddableActionDefinition[];
/** Message to be displayed in the welcome page. */
- welcomeMessage: string;
+ greeting: {
+ title: string;
+ subtitle: string;
+ };
/** Suggestions of questions to be displayed in the welcome page. */
suggestions: string[];
diff --git a/packages/embed/src/react/GitBookFrame.tsx b/packages/embed/src/react/GitBookFrame.tsx
index cd0d8f65d..b9f527fea 100644
--- a/packages/embed/src/react/GitBookFrame.tsx
+++ b/packages/embed/src/react/GitBookFrame.tsx
@@ -17,7 +17,7 @@ export type GitBookFrameProps = {
* Render a frame with the GitBook Assistant in it.
*/
export function GitBookFrame(props: GitBookFrameProps) {
- const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props;
+ const { className, visitor, actions, greeting, suggestions, tools } = props;
const frameRef = useRef(null);
const gitbook = useGitBook();
@@ -33,12 +33,13 @@ export function GitBookFrame(props: GitBookFrameProps) {
useEffect(() => {
gitbookFrame?.configure({
- buttons,
- welcomeMessage,
+ tabs: ['assistant', 'docs'],
+ actions,
+ greeting,
suggestions,
tools,
});
- }, [gitbookFrame, buttons, welcomeMessage, suggestions, tools]);
+ }, [gitbookFrame, actions, greeting, suggestions, tools]);
return (
);
@@ -111,13 +114,14 @@ export function AIChat() {
*/
export function AIChatDynamicIcon(props: {
trademark: boolean;
+ className?: string;
}) {
- const { trademark } = props;
+ const { trademark, className } = props;
const chat = useAIChatState();
return (
0 ? (
- {}}
- iconOnly
- icon="ellipsis"
- label={tString(language, 'actions')}
- variant="blank"
- size="default"
- />
- }
- >
- {
- chatController.clear();
- }}
- >
-
- {t(language, 'ai_chat_clear_conversation')}
-
-
+