From 46091b65d1390aff07eafcea86fdac773020e115 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Mon, 15 Jun 2026 13:40:27 +0200 Subject: [PATCH] Add Structure Preview component and related functionality --- bun.lock | 2 +- .../[siteData]/~gitbook/structure/layout.tsx | 16 + .../[siteData]/~gitbook/structure/page.tsx | 65 ++ .../StructurePreview/StructurePreview.tsx | 679 ++++++++++++++++++ .../src/components/StructurePreview/index.ts | 2 + .../components/StructurePreview/state.test.ts | 157 ++++ .../src/components/StructurePreview/state.ts | 272 +++++++ .../src/components/StructurePreview/types.ts | 45 ++ packages/gitbook/src/middleware.ts | 1 + 9 files changed, 1238 insertions(+), 1 deletion(-) create mode 100644 packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/structure/layout.tsx create mode 100644 packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/structure/page.tsx create mode 100644 packages/gitbook/src/components/StructurePreview/StructurePreview.tsx create mode 100644 packages/gitbook/src/components/StructurePreview/index.ts create mode 100644 packages/gitbook/src/components/StructurePreview/state.test.ts create mode 100644 packages/gitbook/src/components/StructurePreview/state.ts create mode 100644 packages/gitbook/src/components/StructurePreview/types.ts diff --git a/bun.lock b/bun.lock index a01983909..c7377600d 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.15", + "turbo": "^2.9.18", "vercel": "50.37.3", }, }, diff --git a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/structure/layout.tsx b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/structure/layout.tsx new file mode 100644 index 000000000..a14e64fcd --- /dev/null +++ b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/structure/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/structure/page.tsx b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/structure/page.tsx new file mode 100644 index 000000000..4cd841675 --- /dev/null +++ b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/structure/page.tsx @@ -0,0 +1,65 @@ +import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils'; +import { CustomizationRootLayout } from '@/components/RootLayout'; +import { SiteLayoutClientContexts } from '@/components/SiteLayout'; +import { StructurePreview, type StructurePreviewSnapshot } from '@/components/StructurePreview'; +import { getThemeFromMiddleware } from '@/lib/middleware'; + +type PageProps = { + params: Promise; +}; + +export default async function Page(props: PageProps) { + const { context } = await getDynamicSiteContext(await props.params); + const forcedTheme = await getThemeFromMiddleware(); + const snapshot: StructurePreviewSnapshot = { + site: { + id: context.site.id, + title: context.site.title, + proxyOrigin: context.site.proxy?.origin, + }, + contextId: context.contextId, + locale: context.locale, + customization: context.customization, + structure: context.structure, + siteSpace: context.siteSpace, + siteSpaces: context.siteSpaces, + visibleSiteSpaces: context.visibleSiteSpaces, + sections: context.sections, + visibleSections: context.visibleSections, + revision: { + pages: context.revision.pages, + tags: context.revision.tags, + }, + icons: { + large: { + light: context.linker.toPathInSpace('~gitbook/icon?size=large&theme=light'), + dark: context.linker.toPathInSpace('~gitbook/icon?size=large&theme=dark'), + }, + }, + }; + + return ( + + + + + + ); +} diff --git a/packages/gitbook/src/components/StructurePreview/StructurePreview.tsx b/packages/gitbook/src/components/StructurePreview/StructurePreview.tsx new file mode 100644 index 000000000..e99568ad8 --- /dev/null +++ b/packages/gitbook/src/components/StructurePreview/StructurePreview.tsx @@ -0,0 +1,679 @@ +'use client'; + +import type { CustomizationContentLink, CustomizationHeaderItem, SiteSpace } from '@gitbook/api'; +import { CustomizationHeaderPreset, CustomizationSearchStyle } from '@gitbook/api'; +import { Icon, type IconName } from '@gitbook/icons'; +import * as React from 'react'; + +import { HeaderMobileMenu } from '@/components/Header/HeaderMobileMenu'; +import { SiteSectionTabs } from '@/components/SiteSections'; +import { PagesList } from '@/components/TableOfContents/PagesList'; +import { getLocalizedTitle } from '@/lib/sites'; +import { tcls } from '@/lib/tailwind'; + +import { tString, useLanguage } from '@/intl/client'; +import headerLinksStyles from '../Header/headerLinks.module.css'; +import { CONTAINER_STYLE, HEADER_HEIGHT_DESKTOP } from '../layout'; +import { Button, ToggleChevron } from '../primitives'; +import { DropdownMenu, DropdownMenuItem, DropdownSubMenu } from '../primitives/DropdownMenu'; +import { ScrollContainer } from '../primitives/ScrollContainer'; +import { SideSheet } from '../primitives/SideSheet'; +import { + SOCIAL_PLATFORM_ICONS, + encodePreviewSiteSections, + encodePreviewTableOfContents, + getContentRefKey, + getHeaderSocialAccounts, + getPreviewVariants, + getStructurePreviewViewportMode, + isStructurePreviewMessage, +} from './state'; +import type { StructurePreviewSnapshot, StructurePreviewViewportMode } from './types'; + +export function StructurePreview(props: { initialSnapshot: StructurePreviewSnapshot }) { + const { initialSnapshot } = props; + const [snapshot, setSnapshot] = React.useState(initialSnapshot); + const [localMode, setLocalMode] = React.useState( + getStructurePreviewViewportMode(initialSnapshot.viewportMode) + ); + + React.useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.source !== window.parent || !isStructurePreviewMessage(event.data)) { + return; + } + + setSnapshot(event.data.payload); + setLocalMode(getStructurePreviewViewportMode(event.data.payload.viewportMode)); + }; + + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, []); + + const mode = getStructurePreviewViewportMode(localMode); + + const preventNavigation = (event: React.MouseEvent) => { + const target = event.target; + if (!(target instanceof Element)) { + return; + } + + const anchor = target.closest('a'); + if (!anchor) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + }; + + return ( +
+ + + +
+ ); +} + +function StructurePreviewModeToggle(props: { + mode: StructurePreviewViewportMode; + onModeChange: (mode: StructurePreviewViewportMode) => void; +}) { + const { mode, onModeChange } = props; + return ( +
+ {(['auto', 'desktop', 'mobile'] as const).map((nextMode) => ( + + ))} +
+ ); +} + +function StructurePreviewHeader(props: { + snapshot: StructurePreviewSnapshot; + viewportMode: StructurePreviewViewportMode; +}) { + const { snapshot, viewportMode } = props; + const { customization } = snapshot; + const language = useLanguage(); + const variants = getPreviewVariants(snapshot); + const sections = encodePreviewSiteSections(snapshot); + const headerSocialAccounts = getHeaderSocialAccounts(customization); + const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None; + const withSections = Boolean( + sections && + (sections.list.length > 1 || + sections.list.some((section) => section.object === 'site-section-group')) + ); + const forceMobile = viewportMode === 'mobile'; + const forceDesktop = viewportMode === 'desktop'; + + return ( +
+
+
+
+
+ + +
+ +
+ +
+ + {(customization.header.links.length > 0 || + headerSocialAccounts.length > 0 || + (!withSections && variants.translations.length > 1)) && + !forceMobile ? ( +
.button+.button]:-ml-2 z-20 ml-auto flex min-w-9 shrink grow @7xl:grow-0 items-center justify-end @4xl:gap-x-6 gap-x-4', + forceMobile ? 'hidden' : null + )} + > + {customization.header.links.map((link, index) => ( + + ))} + {headerSocialAccounts.length > 0 ? ( +
+ {headerSocialAccounts.map((account) => { + const icon = SOCIAL_PLATFORM_ICONS[account.platform]; + return icon ? ( +
+ ) : null} + {customization.header.links.length > 0 || + headerSocialAccounts.length > 0 ? ( + + ) : null} + {!withSections && variants.translations.length > 1 ? ( + + ) : null} +
+ ) : null} +
+
+
+ + {sections && withSections && !forceMobile ? ( +
+ + {variants.translations.length > 1 ? ( + + ) : null} + +
+ ) : null} +
+ ); +} + +function StructurePreviewLogo(props: { snapshot: StructurePreviewSnapshot }) { + const { snapshot } = props; + const { customization } = snapshot; + + return ( +
+ {customization.header.logo ? ( + + {customization.header.logo.dark ? ( + + ) : null} + Logo + + ) : ( + <> + {'emoji' in customization.favicon && customization.favicon.emoji ? ( + {customization.favicon.emoji} + ) : ( + + + + + )} +
+ {snapshot.site.title} +
+ + )} +
+ ); +} + +function StructurePreviewSearch(props: { style: CustomizationSearchStyle; mobile: boolean }) { + const language = useLanguage(); + const label = tString(language, 'search'); + + if (props.mobile) { + return + ); +} + +function StructurePreviewMoreMenu(props: { + snapshot: StructurePreviewSnapshot; + links: CustomizationHeaderItem[]; + label: React.ReactNode; +}) { + const { snapshot, links, label } = props; + return ( +
+ + {label} + + + + } + > + {links.map((link, index) => ( + + ))} + +
+ ); +} + +function StructurePreviewMenuLink(props: { + snapshot: StructurePreviewSnapshot; + link: CustomizationHeaderItem | CustomizationContentLink; +}) { + const { snapshot, link } = props; + const title = getLocalizedTitle(link, snapshot.locale); + + return 'links' in link && link.links.length > 0 ? ( + + {link.links.map((subLink, index) => ( + + ))} + + ) : ( + {title} + ); +} + +function StructurePreviewSpacesDropdown(props: { + snapshot: StructurePreviewSnapshot; + siteSpace: SiteSpace; + siteSpaces: SiteSpace[]; + className?: string; + variant?: React.ComponentProps['variant']; + icon?: IconName; +}) { + const { snapshot, siteSpace, siteSpaces, className, variant = 'secondary', icon } = props; + const title = getLocalizedTitle(siteSpace, snapshot.locale); + + return ( + } + className={tcls('bg-tint-base', className)} + > + {title} + + } + > + {siteSpaces.map((space) => ( + + {getLocalizedTitle(space, snapshot.locale)} + + ))} + + ); +} + +function StructurePreviewMobileNavigation(props: { snapshot: StructurePreviewSnapshot }) { + const { snapshot } = props; + const pages = React.useMemo(() => encodePreviewTableOfContents(snapshot), [snapshot]); + const variants = getPreviewVariants(snapshot); + + return ( + +
+ + {variants.translations.length > 1 ? ( + + ) : null} +
+
+ + + +
+
+ ); +} diff --git a/packages/gitbook/src/components/StructurePreview/index.ts b/packages/gitbook/src/components/StructurePreview/index.ts new file mode 100644 index 000000000..3a90a4a52 --- /dev/null +++ b/packages/gitbook/src/components/StructurePreview/index.ts @@ -0,0 +1,2 @@ +export * from './StructurePreview'; +export * from './types'; diff --git a/packages/gitbook/src/components/StructurePreview/state.test.ts b/packages/gitbook/src/components/StructurePreview/state.test.ts new file mode 100644 index 000000000..8193b10dd --- /dev/null +++ b/packages/gitbook/src/components/StructurePreview/state.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, it } from 'bun:test'; + +import { defaultCustomization } from '@/lib/utils'; + +import { + encodePreviewSiteSections, + encodePreviewTableOfContents, + getStructurePreviewViewportMode, + isStructurePreviewMessage, +} from './state'; +import type { StructurePreviewSnapshot } from './types'; + +function createSnapshot( + overrides: Partial = {} +): StructurePreviewSnapshot { + const siteSpace = { + id: 'site-space-1', + title: 'Docs', + path: '', + default: true, + hidden: false, + urls: {}, + space: { + id: 'space-1', + revision: 'revision-1', + language: 'en', + }, + }; + + return { + site: { + id: 'site-1', + title: 'Acme Docs', + }, + customization: defaultCustomization(), + structure: { + type: 'siteSpaces', + structure: [siteSpace], + }, + siteSpace, + siteSpaces: [siteSpace], + visibleSiteSpaces: [siteSpace], + sections: null, + visibleSections: null, + revision: { + pages: [], + tags: [], + }, + icons: { + large: { + light: '/~gitbook/icon?size=large&theme=light', + dark: '/~gitbook/icon?size=large&theme=dark', + }, + }, + ...overrides, + } as StructurePreviewSnapshot; +} + +describe('structure preview state', () => { + it('validates full snapshot update messages', () => { + const snapshot = createSnapshot(); + + expect( + isStructurePreviewMessage({ + type: 'gitbook.structure.update', + payload: snapshot, + }) + ).toBe(true); + expect(isStructurePreviewMessage({ type: 'gitbook.structure.update' })).toBe(false); + expect(isStructurePreviewMessage({ type: 'other', payload: snapshot })).toBe(false); + }); + + it('normalizes viewport mode', () => { + expect(getStructurePreviewViewportMode('desktop')).toBe('desktop'); + expect(getStructurePreviewViewportMode('mobile')).toBe('mobile'); + expect(getStructurePreviewViewportMode('auto')).toBe('auto'); + expect(getStructurePreviewViewportMode(undefined)).toBe('auto'); + }); + + it('encodes section structures with inert URLs', () => { + const section = { + object: 'site-section', + id: 'section-1', + title: 'Guides', + localizedTitle: { fr: 'Guides FR' }, + description: 'Learn', + path: 'guides', + default: true, + siteSpaces: [], + urls: {}, + }; + const snapshot = createSnapshot({ + locale: 'fr', + sections: { + list: [ + { + object: 'site-section-group', + id: 'group-1', + title: 'Products', + children: [section], + }, + ], + current: section, + }, + } as unknown as Partial); + + const encoded = encodePreviewSiteSections(snapshot); + + expect(encoded?.current.title).toBe('Guides FR'); + expect(encoded?.current.url).toBe('#'); + expect(encoded?.list[0]?.object).toBe('site-section-group'); + }); + + it('encodes revision pages for the mobile menu without navigable URLs', () => { + const snapshot = createSnapshot({ + revision: { + tags: [], + pages: [ + { + type: 'document', + id: 'page-1', + title: 'Introduction', + path: 'intro', + pages: [ + { + type: 'document', + id: 'page-2', + title: 'Nested', + path: 'nested', + pages: [], + }, + ], + }, + { + type: 'document', + id: 'hidden-page', + title: 'Hidden', + hidden: true, + path: 'hidden', + pages: [], + }, + ], + }, + } as unknown as Partial); + + const encoded = encodePreviewTableOfContents(snapshot); + + expect(encoded).toHaveLength(1); + expect(encoded[0]).toMatchObject({ + type: 'document', + title: 'Introduction', + href: '#', + }); + const nested = encoded[0]?.type === 'document' ? encoded[0].descendants?.[0] : null; + expect(nested?.type === 'document' ? nested.href : null).toBe('#'); + }); +}); diff --git a/packages/gitbook/src/components/StructurePreview/state.ts b/packages/gitbook/src/components/StructurePreview/state.ts new file mode 100644 index 000000000..ae354a133 --- /dev/null +++ b/packages/gitbook/src/components/StructurePreview/state.ts @@ -0,0 +1,272 @@ +import type { + ContentRef, + RevisionPage, + RevisionPageTag, + RevisionTag, + SiteCustomizationSettings, + SiteSection, + SiteSectionGroup, + SiteSocialAccountPlatform, +} from '@gitbook/api'; +import type { IconName } from '@gitbook/icons'; +import assertNever from 'assert-never'; + +import type { + ClientSiteSection, + ClientSiteSectionGroup, + ClientSiteSections, +} from '@/components/SiteSections/encodeClientSiteSections'; +import type { ClientTOCPage } from '@/components/TableOfContents/encodeClientTableOfContents'; +import { getLocalizedDescription, getLocalizedTitle } from '@/lib/sites'; +import { resolveTag } from '@/lib/tags'; + +import type { + StructurePreviewMessage, + StructurePreviewSnapshot, + StructurePreviewViewportMode, +} from './types'; + +export function isStructurePreviewMessage(value: unknown): value is StructurePreviewMessage { + if (!value || typeof value !== 'object') { + return false; + } + + const message = value as Partial; + return ( + message.type === 'gitbook.structure.update' && isStructurePreviewSnapshot(message.payload) + ); +} + +export function isStructurePreviewSnapshot(value: unknown): value is StructurePreviewSnapshot { + if (!value || typeof value !== 'object') { + return false; + } + + const snapshot = value as Partial; + return Boolean( + snapshot.site && + typeof snapshot.site.title === 'string' && + snapshot.customization && + snapshot.structure && + snapshot.siteSpace && + Array.isArray(snapshot.siteSpaces) && + Array.isArray(snapshot.visibleSiteSpaces) && + snapshot.revision && + Array.isArray(snapshot.revision.pages) && + Array.isArray(snapshot.revision.tags) + ); +} + +export function getStructurePreviewViewportMode( + mode: StructurePreviewViewportMode | undefined +): StructurePreviewViewportMode { + return mode === 'desktop' || mode === 'mobile' ? mode : 'auto'; +} + +export function encodePreviewSiteSections( + snapshot: StructurePreviewSnapshot +): ClientSiteSections | null { + const sections = snapshot.visibleSections ?? snapshot.sections; + if (!sections) { + return null; + } + + return { + list: sections.list.flatMap((item) => encodePreviewSectionItem(snapshot, item)), + current: encodePreviewSection(snapshot, sections.current), + }; +} + +function encodePreviewSectionItem( + snapshot: StructurePreviewSnapshot, + item: SiteSection | SiteSectionGroup +): (ClientSiteSection | ClientSiteSectionGroup)[] { + switch (item.object) { + case 'site-section': { + return [encodePreviewSection(snapshot, item)]; + } + case 'site-section-group': { + const children = item.children.flatMap((child) => + encodePreviewSectionItem(snapshot, child) + ); + if (children.length === 0) { + return []; + } + + return [ + { + id: item.id, + title: getLocalizedTitle(item, snapshot.locale), + icon: item.icon, + object: item.object, + children, + }, + ]; + } + default: + assertNever(item); + } +} + +function encodePreviewSection( + snapshot: StructurePreviewSnapshot, + section: SiteSection +): ClientSiteSection { + return { + id: section.id, + title: getLocalizedTitle(section, snapshot.locale), + description: getLocalizedDescription(section, snapshot.locale), + icon: section.icon, + object: section.object, + url: '#', + }; +} + +export function encodePreviewTableOfContents(snapshot: StructurePreviewSnapshot): ClientTOCPage[] { + return encodePreviewPages( + snapshot.revision.pages, + snapshot.revision.pages, + snapshot.revision.tags + ); +} + +function encodePreviewPages( + rootPages: RevisionPage[], + pages: RevisionPage[], + tags: RevisionTag[] +): ClientTOCPage[] { + return pages.flatMap((page): ClientTOCPage[] => { + if (page.type === 'computed' || page.hidden) { + return []; + } + + switch (page.type) { + case 'document': { + return [ + removeUndefined({ + id: page.id, + title: page.linkTitle || page.title, + href: '#', + emoji: page.emoji, + icon: page.icon, + pathnames: [], + descendants: hasVisibleDescendant(page.pages) + ? encodePreviewPages(rootPages, page.pages, tags) + : undefined, + primaryTag: resolvePrimaryPageTag(page.tags, tags), + type: 'document' as const, + }), + ]; + } + case 'link': { + return [ + removeUndefined({ + id: page.id, + title: page.title, + href: '#', + emoji: page.emoji, + icon: page.icon, + target: page.target, + type: 'link' as const, + }), + ]; + } + case 'group': { + return [ + removeUndefined({ + id: page.id, + title: page.title, + emoji: page.emoji, + icon: page.icon, + descendants: hasVisibleDescendant(page.pages) + ? encodePreviewPages(rootPages, page.pages, tags) + : undefined, + type: 'group' as const, + }), + ]; + } + default: + assertNever(page); + } + }); +} + +function hasVisibleDescendant(pages: RevisionPage[]) { + return pages.some((page) => page.type !== 'computed' && !page.hidden); +} + +function resolvePrimaryPageTag( + pageTags: RevisionPageTag[] | undefined, + tags: RevisionTag[] +): RevisionTag | undefined { + if (!pageTags || pageTags.length === 0) { + return undefined; + } + + const primary = pageTags.find((tag) => tag.primary); + if (!primary) { + return undefined; + } + + return resolveTag(primary.tag.tag, tags); +} + +function removeUndefined>(object: T): T { + return Object.fromEntries( + Object.entries(object).filter(([, value]) => value !== undefined) + ) as T; +} + +export function getPreviewVariants(snapshot: StructurePreviewSnapshot) { + const siteSpaces = + snapshot.visibleSiteSpaces.length > 0 ? snapshot.visibleSiteSpaces : snapshot.siteSpaces; + const currentLanguage = snapshot.locale ?? snapshot.siteSpace.space.language ?? 'en'; + const languages = [...new Set(siteSpaces.map((space) => space.space.language ?? 'en'))]; + const isMultiLanguage = languages.length > 1; + + return { + generic: isMultiLanguage + ? siteSpaces.filter( + (space) => + space.id === snapshot.siteSpace.id || + (space.space.language ?? 'en') === currentLanguage + ) + : siteSpaces, + translations: isMultiLanguage + ? siteSpaces.filter( + (space) => + space.id === snapshot.siteSpace.id || + (space.space.language ?? 'en') !== currentLanguage + ) + : [], + }; +} + +export function getHeaderSocialAccounts(customization: SiteCustomizationSettings) { + return customization.socialAccounts.filter((account) => account.display.header === true); +} + +export const SOCIAL_PLATFORM_ICONS: Partial> = { + twitter: 'x-twitter', + instagram: 'instagram', + facebook: 'facebook', + linkedin: 'linkedin', + github: 'github', + discord: 'discord', + slack: 'slack', + youtube: 'youtube', + tiktok: 'tiktok', + reddit: 'reddit', + bluesky: 'bluesky', + mastodon: 'mastodon', + threads: 'threads', + medium: 'medium', +}; + +export function getContentRefKey(ref: ContentRef | null | undefined) { + if (!ref) { + return 'empty'; + } + + return JSON.stringify(ref); +} diff --git a/packages/gitbook/src/components/StructurePreview/types.ts b/packages/gitbook/src/components/StructurePreview/types.ts new file mode 100644 index 000000000..23078d5b7 --- /dev/null +++ b/packages/gitbook/src/components/StructurePreview/types.ts @@ -0,0 +1,45 @@ +import type { + RevisionPage, + RevisionTag, + SiteCustomizationSettings, + SiteSpace, + SiteStructure, + TranslationLanguage, +} from '@gitbook/api'; + +import type { SiteSections } from '@/lib/context'; + +export type StructurePreviewViewportMode = 'auto' | 'desktop' | 'mobile'; + +export type StructurePreviewSnapshot = { + site: { + id: string; + title: string; + proxyOrigin?: string; + }; + contextId?: string; + locale?: TranslationLanguage; + customization: SiteCustomizationSettings; + structure: SiteStructure; + siteSpace: SiteSpace; + siteSpaces: SiteSpace[]; + visibleSiteSpaces: SiteSpace[]; + sections: SiteSections | null; + visibleSections: SiteSections | null; + revision: { + pages: RevisionPage[]; + tags: RevisionTag[]; + }; + icons: { + large: { + light: string; + dark: string; + }; + }; + viewportMode?: StructurePreviewViewportMode; +}; + +export type StructurePreviewMessage = { + type: 'gitbook.structure.update'; + payload: StructurePreviewSnapshot; +}; diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index dcf2804ab..c8cadea88 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -803,6 +803,7 @@ function encodePathInSiteContent( case '~gitbook/auth/login': case '~gitbook/auth/logout': case '~scalar/proxy': + case '~gitbook/structure': // PDF, search and auth routes are always dynamic as they depend on the request. return { pathname, routeType: 'dynamic' }; default: {