From c36679675613a55fb70371f8fa4569a5f552fbb7 Mon Sep 17 00:00:00 2001 From: Peter White <1788320+peterwhite@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:26:04 +0200 Subject: [PATCH] Keep the embed open when switching section from the breadcrumbs (RND-12559) (#4670) --- .changeset/embed-breadcrumb-section-links.md | 5 ++++ .../Embeddable/EmbeddableDocsPage.tsx | 3 ++ .../src/components/Header/SpacesDropdown.tsx | 20 ++++++++++--- .../src/components/PageBody/PageBody.tsx | 7 ++++- .../src/components/PageBody/PageHeader.tsx | 30 +++++++++++++------ .../src/components/SitePage/SitePage.tsx | 4 +-- .../SiteSections/encodeClientSiteSections.ts | 2 +- 7 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 .changeset/embed-breadcrumb-section-links.md diff --git a/.changeset/embed-breadcrumb-section-links.md b/.changeset/embed-breadcrumb-section-links.md new file mode 100644 index 000000000..bb86dcd15 --- /dev/null +++ b/.changeset/embed-breadcrumb-section-links.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Keep the docs embed open when switching section, variant or language. diff --git a/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx b/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx index ca3390932..d63e44128 100644 --- a/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx +++ b/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx @@ -97,6 +97,7 @@ export async function EmbeddableDocsPage( ) ?? context.siteSpace } siteSpaces={variants.translations} + asEmbeddable className="my-1.5 ml-2 self-start" /> ) : null} @@ -125,6 +126,7 @@ export async function EmbeddableDocsPage( ) ?? context.siteSpace } siteSpaces={variants.translations} + asEmbeddable className="max-md:[&_.button-content]:block" /> ) : null @@ -137,6 +139,7 @@ export async function EmbeddableDocsPage( context={context} siteSpace={context.siteSpace} siteSpaces={variants.generic} + asEmbeddable className="w-full px-3" /> ) : null} diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx index befded446..cf353c86d 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx @@ -2,9 +2,10 @@ import type { SiteSpace } from '@gitbook/api'; import type { IconName } from '@gitbook/icons'; import type { ButtonProps } from '../primitives'; +import { getTargetURLForSiteSpace } from '../SiteSections/encodeClientSiteSections'; import { SpacesDropdownClient } from './SpacesDropdownClient'; import type { GitBookSiteContext } from '@/lib/context'; -import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites'; +import { getLocalizedTitle } from '@/lib/sites'; import { tcls } from '@/lib/tailwind'; // Memoized regex for checking if a string starts with an emoji @@ -20,9 +21,18 @@ export function SpacesDropdown( siteSpace: SiteSpace; siteSpaces: SiteSpace[]; className?: string; + /** Keep the links inside the embed rather than sending readers to the full site. */ + asEmbeddable?: boolean; } & ButtonProps ) { - const { context, siteSpace, siteSpaces, className, ...buttonProps } = props; + const { + context, + siteSpace, + siteSpaces, + className, + asEmbeddable = false, + ...buttonProps + } = props; const currentLanguage = context.locale; const dropdownClassName = tcls( @@ -33,7 +43,7 @@ export function SpacesDropdown( const slimSpaces = siteSpaces.map((siteSp) => ({ id: siteSp.id, title: getLocalizedTitle(siteSp, currentLanguage), - url: getSiteSpaceURL(context, siteSp), + url: getTargetURLForSiteSpace(context, siteSp, asEmbeddable), isActive: siteSp.id === siteSpace.id, spaceId: siteSp.space.id, })); @@ -57,9 +67,10 @@ export function TranslationsDropdown( siteSpace: SiteSpace; siteSpaces: SiteSpace[]; className?: string; + asEmbeddable?: boolean; } & ButtonProps ) { - const { context, siteSpace, siteSpaces, className, ...buttonProps } = props; + const { context, siteSpace, siteSpaces, className, asEmbeddable, ...buttonProps } = props; const title = getLocalizedTitle(siteSpace, context.locale); const hasEmojiPrefix = startsWithEmoji(title); @@ -70,6 +81,7 @@ export function TranslationsDropdown( context={context} siteSpace={siteSpace} siteSpaces={siteSpaces} + asEmbeddable={asEmbeddable} variant="blank" className={tcls( 'bg-transparent lg:max-w-64 max-md:[&_.button-content]:hidden', diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 481afc937..1d8e83d2b 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -1,4 +1,8 @@ -import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api'; +import { + type JSONDocument, + type RevisionPageDocument, + SiteInsightsDisplayContext, +} from '@gitbook/api'; import { DocumentView, DocumentViewSkeleton } from '../DocumentView'; import { CurrentPageProvider } from '../hooks/useCurrentPage'; @@ -127,6 +131,7 @@ export async function PageBody(props: { ancestors={ancestors} withRSSFeed={contentHasUpdates} hasAPIBlocks={hasAPIBlocks} + asEmbeddable={insightsDisplayContext === SiteInsightsDisplayContext.Embed} /> {document && !isNodeEmpty(document) ? ( 0; @@ -82,7 +87,10 @@ export async function PageHeader(props: { // Section groups have no URL of their own; only sections are navigable. Resolve the // section to the site space matching the current variant so switching sections keeps // the reader on their current variant (same logic as the section tabs). - href: node.object === 'site-section' ? findBestTargetURL(context, node) : undefined, + href: + node.object === 'site-section' + ? findBestTargetURL(context, node, asEmbeddable) + : undefined, label: getLocalizedTitle(node, context.locale), icon: node.icon, siblings: siblings @@ -92,7 +100,7 @@ export async function PageHeader(props: { // Don't offer hidden sections/groups as switch targets. .filter((sibling) => !visibleSectionIds || visibleSectionIds.has(sibling.id)) .map((sibling) => { - const href = getSectionNodeURL(context, sibling); + const href = getSectionNodeURL(context, sibling, asEmbeddable); // Keep siblings whose URL is "" (the site's first page); only drop the ones // with no resolvable URL at all. return href !== undefined @@ -112,7 +120,7 @@ export async function PageHeader(props: { if (currentSiteSpace) { contextCrumbs.push({ key: `variant-${currentSiteSpace.id}`, - href: getSiteSpaceURL(context, currentSiteSpace), + href: getTargetURLForSiteSpace(context, currentSiteSpace, asEmbeddable), label: getLocalizedTitle(currentSiteSpace, context.locale), siblings: [], // Reuse the header's variant switcher rather than plain per-variant links, so each entry @@ -122,7 +130,7 @@ export async function PageHeader(props: { slimSpaces: variantSpaces.map((siteSpace) => ({ id: siteSpace.id, title: getLocalizedTitle(siteSpace, context.locale), - url: getSiteSpaceURL(context, siteSpace), + url: getTargetURLForSiteSpace(context, siteSpace, asEmbeddable), isActive: siteSpace.id === currentSiteSpace.id, spaceId: siteSpace.space.id, })), @@ -448,9 +456,13 @@ function collectSectionNodeIds(list: SiteStructureNode[]): Set { * The URL resolves to the site space matching the current variant (same logic as the section tabs), * so switching sections keeps the reader on their current variant. */ -function getSectionNodeURL(context: GitBookSiteContext, node: SectionNode): string | undefined { +function getSectionNodeURL( + context: GitBookSiteContext, + node: SectionNode, + asEmbeddable: boolean +): string | undefined { const section = findFirstSection(node); - return section ? findBestTargetURL(context, section) : undefined; + return section ? findBestTargetURL(context, section, asEmbeddable) : undefined; } /** diff --git a/packages/gitbook/src/components/SitePage/SitePage.tsx b/packages/gitbook/src/components/SitePage/SitePage.tsx index 49f313eec..e4fabd70e 100644 --- a/packages/gitbook/src/components/SitePage/SitePage.tsx +++ b/packages/gitbook/src/components/SitePage/SitePage.tsx @@ -284,11 +284,11 @@ export async function getSitePageData(props: SitePageProps) { if (pathname !== null) { // If the pathname was not normalized, redirect to the normalized version // before trying to resolve the page again - redirect(context.linker.toPathInSpace(pathname)); + redirect(context.linker.toPathForPagePath({ path: pathname })); } else { // If the page is not found and we are in fallback mode, return a redirect to the basepath if (context.isFallback) { - redirect(context.linker.toPathInSpace('/')); + redirect(context.linker.toPathForPagePath({ path: '/' })); } notFound(); } diff --git a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts index 8ee71f269..04150c635 100644 --- a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts +++ b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts @@ -201,7 +201,7 @@ function getTargetURLForSection( return getSectionURL(context, section); } -function getTargetURLForSiteSpace( +export function getTargetURLForSiteSpace( context: GitBookSiteContext, siteSpace: SiteSpace, asEmbeddable: boolean