Match site redirects whose source has non-ASCII characters (#4689)

This commit is contained in:
Peter White
2026-10-15 16:59:20 +02:00
committed by GitHub
parent 30fa152f0f
commit d839d9dc57
3 changed files with 36 additions and 4 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Site redirects whose source path has accented or non-Latin characters now redirect visitors instead of returning 404.
@@ -97,11 +97,11 @@ describe('fetchPageData', () => {
describe('resolveMissingPagePath', () => {
function createRedirectContext(options: {
siteRedirect?: { target: string; permanent?: boolean };
siteRedirect?: { source?: string; target: string; permanent?: boolean };
spaceRedirectPageId?: string;
}) {
const getSiteRedirectBySource = mock(async ({ source }: { source: string }) =>
options.siteRedirect && source === '/old'
options.siteRedirect && source === (options.siteRedirect.source ?? '/old')
? {
data: {
target: options.siteRedirect.target,
@@ -143,6 +143,24 @@ describe('resolveMissingPagePath', () => {
});
});
it('resolves a site redirect whose source has non-ASCII characters', async () => {
const context = createRedirectContext({
siteRedirect: {
source: '/de/kategorien-hinzuf%C3%BCgen',
target: 'https://docs.example.com/new',
},
});
const pathname = normalizeURL(
new URL('https://docs.example.com/de/kategorien-hinzufügen')
).pathname.slice(1);
expect(await resolveMissingPagePath(context, pathname)).toEqual({
type: 'redirect',
destination: '/new',
permanent: false,
});
});
it('resolves a space redirect to a page', async () => {
const context = createRedirectContext({ spaceRedirectPageId: page.id });
@@ -186,7 +186,8 @@ export async function resolveMissingPagePath(
/**
* Transform a pathname into a candidate source for site redirect matching.
* We also encode each segment to handle special characters in redirects.
* The pathname is already percent-encoded, so each segment is decoded before being encoded
* again: redirect sources store special characters percent-encoded exactly once.
*/
function toSiteRedirectSourceCandidate(pathname: string): string {
const normalized = withLeadingSlash(pathname);
@@ -194,11 +195,19 @@ function toSiteRedirectSourceCandidate(pathname: string): string {
normalized
.slice(1)
.split('/')
.map((segment) => encodeURIComponent(segment))
.map((segment) => encodeURIComponent(safeDecodeURIComponent(segment)))
.join('/')
);
}
function safeDecodeURIComponent(segment: string): string {
try {
return decodeURIComponent(segment);
} catch {
return segment;
}
}
/**
* Get the page path from the params.
*/