diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx
index dcd8e8fb7f..89d6cce7a0 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 1b210ac669..a43c06d6aa 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 2f2f2870b8..36097a3bc8 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 94df27104f..81064f59fd 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 320c8e7c8d..f619b39a6b 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);
+}