diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx
index b952f5d59..681a262e1 100644
--- a/packages/gitbook/src/components/Header/Header.tsx
+++ b/packages/gitbook/src/components/Header/Header.tsx
@@ -2,7 +2,9 @@ import type { GitBookSiteContext } from '@/lib/context';
import { CONTAINER_STYLE, HEADER_HEIGHT_DESKTOP } from '@/components/layout';
import { getSpaceLanguage, t } from '@/intl/server';
+import { filterSectionsWithIndexableSpaces } from '@/lib/seo';
import { tcls } from '@/lib/tailwind';
+import { flattenSectionsFromGroup } from '@/lib/utils';
import { SearchContainer } from '../Search';
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
import { HeaderLink } from './HeaderLink';
@@ -15,7 +17,7 @@ import { TranslationsDropdown } from './SpacesDropdown';
/**
* Render the header for the space.
*/
-export function Header(props: {
+export async function Header(props: {
context: GitBookSiteContext;
withTopHeader?: boolean;
withVariants?: 'generic' | 'translations';
@@ -23,6 +25,14 @@ export function Header(props: {
const { context, withTopHeader, withVariants } = props;
const { siteSpace, siteSpaces, sections, customization } = context;
+ // Filter sections to only include those with indexable spaces
+ const filteredSections = sections?.list
+ ? await filterSectionsWithIndexableSpaces(
+ context,
+ flattenSectionsFromGroup(sections.list).filter((s) => s.object === 'site-section')
+ )
+ : [];
+
return (
-
1
) ?? false
}
- withSections={!!sections}
+ withSections={filteredSections.length > 1}
section={
sections
? // Client-encode to avoid a serialisation issue that was causing the language selector to disappear
diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
index a9be339da..4fc364434 100644
--- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
+++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
@@ -15,6 +15,8 @@ import { tcls } from '@/lib/tailwind';
import { getSpaceLanguage } from '@/intl/server';
import type { VisitorAuthClaims } from '@/lib/adaptive';
import { GITBOOK_APP_URL } from '@/lib/env';
+import { hasIndexableSpaces } from '@/lib/seo';
+import { flattenSectionsFromGroup } from '@/lib/utils';
import { AIChatProvider } from '../AI';
import type { RenderAIMessageOptions } from '../AI';
import { AIChat } from '../AIChat';
@@ -103,7 +105,12 @@ export function SpaceLayout(props: SpaceLayoutProps) {
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
- const withSections = Boolean(sections && sections.list.length > 1);
+ const withSections = Boolean(
+ sections &&
+ flattenSectionsFromGroup(sections?.list ?? []).filter(
+ (s) => s.object === 'site-section' && hasIndexableSpaces(s)
+ ).length > 1
+ );
const currentLanguage = getSpaceLanguage(context);
const withVariants: 'generic' | 'translations' | undefined =
diff --git a/packages/gitbook/src/lib/seo.ts b/packages/gitbook/src/lib/seo.ts
index b1aa30dca..6743876d2 100644
--- a/packages/gitbook/src/lib/seo.ts
+++ b/packages/gitbook/src/lib/seo.ts
@@ -1,5 +1,11 @@
import type { GitBookSiteContext } from '@/lib/context';
-import { type RevisionPageDocument, type RevisionPageGroup, SiteVisibility } from '@gitbook/api';
+import { resolveFirstDocument } from '@/lib/pages';
+import {
+ type RevisionPageDocument,
+ type RevisionPageGroup,
+ SiteVisibility,
+ type Space,
+} from '@gitbook/api';
import { headers } from 'next/headers';
/**
@@ -45,3 +51,87 @@ export async function isSiteIndexable(context: GitBookSiteContext) {
function shouldIndexVisibility(visibility: SiteVisibility) {
return visibility === SiteVisibility.Public;
}
+
+/**
+ * Return true if the first page of a site space is indexable.
+ */
+export async function isFirstPageIndexable(
+ context: GitBookSiteContext,
+ space: Space
+): Promise
{
+ // Get the revision for the site space
+ const revision = await context.dataFetcher.getRevision({
+ spaceId: space.id,
+ revisionId: space.revision,
+ });
+
+ if (revision.error) {
+ return false;
+ }
+
+ // Find the first document page in the revision
+ const firstDocument = resolveFirstDocument(revision.data.pages, []);
+
+ if (!firstDocument) {
+ return false;
+ }
+
+ // Check if the first page is indexable
+ return isPageIndexable(firstDocument.ancestors, firstDocument.page);
+}
+
+/**
+ * Check if a section has any site spaces with indexable first pages.
+ * This function actually checks the indexability by calling the async function.
+ */
+export async function hasIndexableSpaces(
+ context: GitBookSiteContext,
+ section: { siteSpaces: Array<{ space: Space }> }
+): Promise {
+ // Check if any site space in the section has an indexable first page
+ const results = await Promise.allSettled(
+ section.siteSpaces.map((siteSpace) => isFirstPageIndexable(context, siteSpace.space))
+ );
+
+ // Return true if any of the checks succeeded and returned true
+ return results.some((result) => result.status === 'fulfilled' && result.value === true);
+}
+
+/**
+ * Filter sections to only include those that have at least one indexable site space.
+ */
+export async function filterSectionsWithIndexableSpaces<
+ T extends { siteSpaces: Array<{ space: Space }> },
+>(context: GitBookSiteContext, sections: T[]): Promise {
+ const filteredSections: T[] = [];
+
+ for (const section of sections) {
+ const hasIndexable = await hasIndexableSpaces(context, section);
+ if (hasIndexable) {
+ filteredSections.push(section);
+ }
+ }
+
+ return filteredSections;
+}
+
+/**
+ * Pre-compute indexable status for all site spaces in sections.
+ * This should be called during context creation to avoid async operations in components.
+ */
+export async function computeSectionsIndexableStatus(
+ context: GitBookSiteContext,
+ sections: Array<{ siteSpaces: Array<{ space: Space }> }>
+): Promise }>> {
+ return Promise.all(
+ sections.map(async (section) => ({
+ ...section,
+ siteSpaces: await Promise.all(
+ section.siteSpaces.map(async (siteSpace) => ({
+ ...siteSpace,
+ indexable: await isFirstPageIndexable(context, siteSpace.space),
+ }))
+ ),
+ }))
+ );
+}