Filter unindexable spaces

This commit is contained in:
Zeno Kapitein
2025-09-19 15:02:08 +02:00
parent d7948e34b2
commit 158263e557
3 changed files with 111 additions and 5 deletions
@@ -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 (
<header
id="site-header"
@@ -92,7 +102,6 @@ export function Header(props: {
/>
<HeaderLogo context={context} />
</div>
<div
className={tcls(
'flex',
@@ -130,7 +139,7 @@ export function Header(props: {
).length > 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
@@ -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 =
+91 -1
View File
@@ -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<boolean> {
// 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<boolean> {
// 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<T[]> {
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<Array<{ siteSpaces: Array<{ space: Space; indexable: boolean }> }>> {
return Promise.all(
sections.map(async (section) => ({
...section,
siteSpaces: await Promise.all(
section.siteSpaces.map(async (siteSpace) => ({
...siteSpace,
indexable: await isFirstPageIndexable(context, siteSpace.space),
}))
),
}))
);
}