Ensure all content links are resolved relatively to preview (#3450)

This commit is contained in:
Samy Pessé
2025-07-08 13:05:31 +02:00
committed by GitHub
parent 4c89aa60eb
commit 8daede5427
5 changed files with 125 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Ensure all content links are resolved relatively to preview.
+22
View File
@@ -415,6 +415,28 @@ const testCases: TestsCase[] = [
await expect(page.locator('[data-testid="table-of-contents"]')).toBeVisible();
},
},
{
name: 'With sections',
url: async () => {
const data = await getSiteAPIToken('https://gitbook.com/docs');
const searchParams = new URLSearchParams();
searchParams.set('token', data.apiToken);
return `url/preview/${data.site}/?${searchParams.toString()}`;
},
screenshot: false,
run: async (page) => {
const sectionTabs = page.getByLabel('Sections');
await expect(sectionTabs).toBeVisible();
const sectionTabLinks = sectionTabs.getByRole('link');
for (const link of await sectionTabLinks.all()) {
const href = await link.getAttribute('href');
expect(href).toMatch(/^\/url\/preview\/site_p4Xo4\/?/);
}
},
},
],
},
{
+4 -1
View File
@@ -24,7 +24,7 @@ import { notFound } from 'next/navigation';
import { assert } from 'ts-essentials';
import { GITBOOK_URL } from './env';
import { type ImageResizer, createImageResizer } from './images';
import { type GitBookLinker, createLinker } from './links';
import { type GitBookLinker, createLinker, linkerForPublishedURL } from './links';
/**
* Data about the site URL. Provided by the middleware.
@@ -301,6 +301,9 @@ export async function fetchSiteContextByIds(
return {
...spaceContext,
linker: site.urls.published
? linkerForPublishedURL(spaceContext.linker, site.urls.published)
: spaceContext.linker,
organizationId: ids.organization,
site,
siteSpaces,
+62 -1
View File
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'bun:test';
import { createLinker, linkerWithAbsoluteURLs, linkerWithOtherSpaceBasePath } from './links';
import {
createLinker,
linkerForPublishedURL,
linkerWithAbsoluteURLs,
linkerWithOtherSpaceBasePath,
} from './links';
const root = createLinker({
host: 'docs.company.com',
@@ -19,6 +24,12 @@ const siteGitBookIO = createLinker({
siteBasePath: '/sitename/',
});
const preview = createLinker({
host: 'preview',
spaceBasePath: '/site_abc/section/space/',
siteBasePath: '/site_abc/',
});
describe('toPathInSpace', () => {
it('should return the correct path', () => {
expect(root.toPathInSpace('some/path')).toBe('/some/path');
@@ -88,6 +99,12 @@ describe('toLinkForContent', () => {
);
});
it('should preserve the search and hash', () => {
expect(root.toLinkForContent('https://docs.company.com/some/path?a=b#c')).toBe(
'/some/path?a=b#c'
);
});
it('should preserve an absolute URL if the site is not the same', () => {
expect(siteGitBookIO.toLinkForContent('https://org.gitbook.io/anothersite/some/path')).toBe(
'https://org.gitbook.io/anothersite/some/path'
@@ -138,3 +155,47 @@ describe('linkerWithOtherSpaceBasePath', () => {
expect(otherSpaceBasePathLinker.toPathInSpace('some/path')).toBe('/sitename/a/b/some/path');
});
});
describe('linkerForPublishedURL', () => {
describe('Root custom domain', () => {
it('should rewrite links that belongs to the published site to be part of the preview site', () => {
const previewLinker = linkerForPublishedURL(preview, 'https://docs.company.com/');
expect(previewLinker.toLinkForContent('https://docs.company.com/some/path')).toBe(
'/site_abc/some/path'
);
expect(
previewLinker.toLinkForContent('https://docs.company.com/section/variant/some/path')
).toBe('/site_abc/section/variant/some/path');
expect(previewLinker.toLinkForContent('https://www.google.com')).toBe(
'https://www.google.com'
);
});
});
describe('gitbook.io domain', () => {
it('should rewrite links that belongs to the published site to be part of the preview site', () => {
const previewLinker = linkerForPublishedURL(
preview,
'https://org.gitbook.io/sitename/'
);
expect(
previewLinker.toLinkForContent('https://org.gitbook.io/sitename/some/path')
).toBe('/site_abc/some/path');
expect(
previewLinker.toLinkForContent(
'https://org.gitbook.io/sitename/section/variant/some/path'
)
).toBe('/site_abc/section/variant/some/path');
expect(previewLinker.toLinkForContent('https://www.google.com')).toBe(
'https://www.google.com'
);
});
});
it('should should preserve hash and search', () => {
const previewLinker = linkerForPublishedURL(preview, 'https://docs.company.com/');
expect(previewLinker.toLinkForContent('https://docs.company.com/some/path?a=b#c')).toBe(
'/site_abc/some/path?a=b#c'
);
});
});
+32 -1
View File
@@ -115,7 +115,7 @@ export function createLinker(
// If the link points to a content in the same site, we return an absolute path
// instead of a full URL; it makes it possible to use router navigation
if (url.hostname === servedOn.host && url.pathname.startsWith(servedOn.siteBasePath)) {
return url.pathname;
return url.pathname + url.search + url.hash;
}
return rawURL;
@@ -125,6 +125,37 @@ export function createLinker(
return linker;
}
/**
* Create a new linker that intercepts links that belongs to the published site and rewrite them
* relative to the URL being served.
*
* It is needed for preview of site where the served URL (http://preview/site_abc)
* is different from the actual published site URL (https://docs.company.com).
*/
export function linkerForPublishedURL(linker: GitBookLinker, rawSitePublishedURL: string) {
const sitePublishedURL = new URL(rawSitePublishedURL);
return {
...linker,
toLinkForContent(rawURL: string): string {
const url = new URL(rawURL);
// If the link is part of the published site, we rewrite it to be part of the preview site.
if (
url.hostname === sitePublishedURL.hostname &&
url.pathname.startsWith(sitePublishedURL.pathname)
) {
// When detecting that the url has been computed as apart of the published site,
// we rewrite it to be part of the preview site.
const extractedPath = url.pathname.slice(sitePublishedURL.pathname.length);
return linker.toPathInSite(extractedPath) + url.search + url.hash;
}
return linker.toLinkForContent(rawURL);
},
};
}
/**
* Create a new linker that always returns absolute URLs.
*/