Keep the embed open when switching section from the breadcrumbs (RND-12559) (#4670)

This commit is contained in:
Peter White
2026-10-08 11:26:04 +02:00
committed by GitHub
parent f35be52c80
commit c366796756
7 changed files with 54 additions and 17 deletions
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Keep the docs embed open when switching section, variant or language.
@@ -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}
@@ -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',
@@ -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) ? (
<OptionalSuspense
@@ -19,14 +19,17 @@ import {
import { PageAsideToggleButton } from '../PageAside/PageAsideButton';
import { PageIcon } from '../PageIcon';
import { getPDFURLSearchParams } from '../PDF';
import { findBestTargetURL } from '../SiteSections/encodeClientSiteSections';
import {
findBestTargetURL,
getTargetURLForSiteSpace,
} from '../SiteSections/encodeClientSiteSections';
import { categorizeVariants } from '../SpaceLayout/categorizeVariants';
import { BreadcrumbItemDropdown, type BreadcrumbSibling } from './BreadcrumbItemDropdown';
import { PageTags } from './PageTags';
import type { GitBookSiteContext, SiteStructureNode } from '@/lib/context';
import { hasAdaptiveMcpEndpoint } from '@/lib/mcp/endpoints';
import { type AncestorRevisionPage, resolveFirstDocument } from '@/lib/pages';
import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
import { getLocalizedTitle } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { getPageRSSURL } from '@/routes/rss';
@@ -41,8 +44,10 @@ export async function PageHeader(props: {
* exactly as-is.
*/
hasAPIBlocks: boolean;
/** Keep the context crumbs' links inside the embed rather than sending readers to the full site. */
asEmbeddable: boolean;
}) {
const { context, page, ancestors, withRSSFeed, hasAPIBlocks } = props;
const { context, page, ancestors, withRSSFeed, hasAPIBlocks, asEmbeddable } = props;
const { revision, linker } = context;
const hasAncestors = ancestors.length > 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<string> {
* 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;
}
/**
@@ -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();
}
@@ -201,7 +201,7 @@ function getTargetURLForSection(
return getSectionURL(context, section);
}
function getTargetURLForSiteSpace(
export function getTargetURLForSiteSpace(
context: GitBookSiteContext,
siteSpace: SiteSpace,
asEmbeddable: boolean