Fix embeddable search links (#4196)

This commit is contained in:
Zeno Kapitein
2026-04-18 00:43:07 +02:00
committed by GitHub
parent 045f603849
commit e3aca9e4f6
5 changed files with 126 additions and 17 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix embeddable search links
@@ -82,7 +82,9 @@ export async function EmbeddableDocsPage(
{context.sections ? (
<SiteSectionTabs
className="not-theme-bold:-mt-2 theme-bold:bg-tint-base"
sections={encodeClientSiteSections(context, context.sections)}
sections={encodeClientSiteSections(context, context.sections, {
asEmbeddable: true,
})}
>
{variants.translations.length > 1 ? (
<TranslationsDropdown
@@ -1,4 +1,5 @@
import type { GitBookSiteContext, SiteSections } from '@/lib/context';
import { toEmbeddableLinkForPublishedContent } from '@/lib/embeddable-linker';
import {
getLocalizedDescription,
getLocalizedTitle,
@@ -27,16 +28,21 @@ export type ClientSiteSectionGroup = Pick<SiteSectionGroup, 'id' | 'title' | 'ic
/**
* Encode the list of site sections into the data to be rendered in the client.
*/
export function encodeClientSiteSections(context: GitBookSiteContext, sections: SiteSections) {
export function encodeClientSiteSections(
context: GitBookSiteContext,
sections: SiteSections,
options?: { asEmbeddable?: boolean }
) {
const { list, current } = sections;
const currentLanguage = context.locale;
const asEmbeddable = Boolean(options?.asEmbeddable);
const clientSections: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
for (const item of list) {
switch (item.object) {
case 'site-section-group': {
const children = encodeChildren(context, item.children);
const children = encodeChildren(context, item.children, asEmbeddable);
// Skip empty groups
if (children.length === 0) {
@@ -53,7 +59,7 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
continue;
}
case 'site-section': {
clientSections.push(encodeSection(context, item));
clientSections.push(encodeSection(context, item, asEmbeddable));
continue;
}
default:
@@ -63,13 +69,14 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
return {
list: clientSections,
current: encodeSection(context, current),
current: encodeSection(context, current, asEmbeddable),
};
}
function encodeChildren(
context: GitBookSiteContext,
children: (SiteSection | SiteSectionGroup)[]
children: (SiteSection | SiteSectionGroup)[],
asEmbeddable: boolean
): (ClientSiteSection | ClientSiteSectionGroup)[] {
const clientChildren: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
const currentLanguage = context.locale;
@@ -77,11 +84,11 @@ function encodeChildren(
for (const child of children) {
switch (child.object) {
case 'site-section': {
clientChildren.push(encodeSection(context, child));
clientChildren.push(encodeSection(context, child, asEmbeddable));
break;
}
case 'site-section-group': {
const nestedChildren = encodeChildren(context, child.children);
const nestedChildren = encodeChildren(context, child.children, asEmbeddable);
// Skip empty groups
if (nestedChildren.length === 0) {
@@ -105,7 +112,7 @@ function encodeChildren(
return clientChildren;
}
function encodeSection(context: GitBookSiteContext, section: SiteSection) {
function encodeSection(context: GitBookSiteContext, section: SiteSection, asEmbeddable: boolean) {
const currentLanguage = context.locale;
return {
id: section.id,
@@ -113,7 +120,7 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) {
description: getLocalizedDescription(section, currentLanguage),
icon: section.icon,
object: section.object,
url: findBestTargetURL(context, section),
url: findBestTargetURL(context, section, asEmbeddable),
};
}
@@ -125,11 +132,15 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) {
* 4. Otherwise, return the default first language match.
* 5. Otherwise, return the default one.
*/
function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
function findBestTargetURL(
context: GitBookSiteContext,
section: SiteSection,
asEmbeddable: boolean
) {
const { siteSpace: currentSiteSpace } = context;
if (section.siteSpaces.length === 1 || currentSiteSpace.default) {
return getSectionURL(context, section);
return getTargetURLForSection(context, section, asEmbeddable);
}
const possibleMatches =
@@ -142,12 +153,36 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
possibleMatches[0];
if (bestMatch) {
return getSiteSpaceURL(context, bestMatch);
return getTargetURLForSiteSpace(context, bestMatch, asEmbeddable);
}
return getTargetURLForSection(context, section, asEmbeddable);
}
function getTargetURLForSection(
context: GitBookSiteContext,
section: SiteSection,
asEmbeddable: boolean
) {
if (asEmbeddable && section.urls.published) {
return toEmbeddableLinkForPublishedContent(context.linker, section.urls.published, '');
}
return getSectionURL(context, section);
}
function getTargetURLForSiteSpace(
context: GitBookSiteContext,
siteSpace: SiteSpace,
asEmbeddable: boolean
) {
if (asEmbeddable && siteSpace.urls.published) {
return toEmbeddableLinkForPublishedContent(context.linker, siteSpace.urls.published, '');
}
return getSiteSpaceURL(context, siteSpace);
}
/**
* Test if 2 site spaces are equivalent.
*/
+17 -4
View File
@@ -2,12 +2,25 @@ import type { GitBookLinker } from '@/lib/links';
import { getPagePath } from '@/lib/pages';
import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
const EMBED_PAGE_PATH = '~gitbook/embed/page';
function createLocalURL(href: string) {
return new URL(href, 'https://gitbook.local');
}
function toEmbeddablePath(pathname: string, contentPath: string) {
return joinPath(pathname, '~gitbook/embed/page', contentPath);
const normalizedPath = removeEmbeddablePath(pathname);
return contentPath
? joinPath(normalizedPath, EMBED_PAGE_PATH, contentPath)
: joinPath(normalizedPath, EMBED_PAGE_PATH);
}
function removeEmbeddablePath(pathname: string) {
return stripEmbeddablePath(pathname).replace(/\/~gitbook\/embed\/page\/?/, '/');
}
function stripEmbeddablePath(pathname: string) {
return pathname.replace(/\/~gitbook\/embed\/page\/?$/, '');
}
export function toEmbeddableLinkForPublishedContent(
@@ -32,7 +45,7 @@ export function getEmbeddableLinker(linker: GitBookLinker): GitBookLinker {
...linker,
toPathForPage({ pages, page, anchor }) {
const pagePath = getPagePath(pages, page);
const embedPagePath = joinPath('~gitbook/embed/page', pagePath);
const embedPagePath = joinPath(EMBED_PAGE_PATH, pagePath);
return `${linker.toPathInSpace(embedPagePath)}${anchor ? `#${anchor}` : ''}`;
},
@@ -40,7 +53,7 @@ export function getEmbeddableLinker(linker: GitBookLinker): GitBookLinker {
withOtherSiteSpace(override: { spaceBasePath: string }): GitBookLinker {
return linker.withOtherSiteSpace({
// We make sure that links in the other site space will be shown in the embeddable view.
spaceBasePath: joinPath(override.spaceBasePath, '~gitbook/embed/page'),
spaceBasePath: joinPath(override.spaceBasePath, EMBED_PAGE_PATH),
});
},
@@ -54,7 +67,7 @@ export function getEmbeddableLinker(linker: GitBookLinker): GitBookLinker {
const url = createLocalURL(result);
if (url.pathname.startsWith(linker.spaceBasePath)) {
const contentPath = url.pathname.slice(linker.spaceBasePath.length);
return `${linker.toPathInSpace(joinPath('~gitbook/embed/page', contentPath))}${url.search}${url.hash}`;
return `${linker.toPathInSpace(joinPath(EMBED_PAGE_PATH, contentPath))}${url.search}${url.hash}`;
}
return result;
@@ -52,6 +52,60 @@ describe('getEmbeddableLinker', () => {
)
).toBe('/api/python/~gitbook/embed/page/getting-started');
});
it('toEmbeddableLinkForPublishedContent should not duplicate the embed path for embeddable linkers', () => {
const root = createLinker({
host: 'docs.company.com',
spaceBasePath: '/docs',
siteBasePath: '/',
});
const embeddableLinker = getEmbeddableLinker(root);
expect(
toEmbeddableLinkForPublishedContent(
embeddableLinker,
'https://docs.company.com/docs',
'nested/page-path'
)
).toBe('/docs/~gitbook/embed/page/nested/page-path');
});
it('toEmbeddableLinkForPublishedContent should place section paths before the embed path', () => {
const root = createLinker({
host: 'docs.company.com',
spaceBasePath: '/docs',
siteBasePath: '/',
});
const embeddableLinker = getEmbeddableLinker(root);
expect(
toEmbeddableLinkForPublishedContent(
embeddableLinker,
'https://docs.company.com/docs/guides',
''
)
).toBe('/docs/guides/~gitbook/embed/page');
});
it('toEmbeddableLinkForPublishedContent should keep page slugs after the embed path in other sections', () => {
const root = createLinker({
host: 'docs.company.com',
spaceBasePath: '/docs',
siteBasePath: '/',
});
const embeddableLinker = getEmbeddableLinker(root);
expect(
toEmbeddableLinkForPublishedContent(
embeddableLinker,
'https://docs.company.com/docs/guides',
'getting-started'
)
).toBe('/docs/guides/~gitbook/embed/page/getting-started');
});
});
describe('resolveEmbeddableTheme', () => {