diff --git a/src/lib/middleware.test.ts b/src/lib/middleware.test.ts index 5c362dee0..939bb972e 100644 --- a/src/lib/middleware.test.ts +++ b/src/lib/middleware.test.ts @@ -64,4 +64,30 @@ describe('getURLLookupAlternatives', () => { }, ]); }); + + it('should normalize duplicated slashes', () => { + expect(getURLLookupAlternatives(new URL('https://docs.mycompany.com//hello'))).toEqual([ + { + extraPath: 'hello', + url: 'https://docs.mycompany.com/', + }, + { + extraPath: '', + url: 'https://docs.mycompany.com/hello', + }, + ]); + }); + + it('should normalize trailing slash', () => { + expect(getURLLookupAlternatives(new URL('https://docs.mycompany.com/hello/'))).toEqual([ + { + extraPath: 'hello', + url: 'https://docs.mycompany.com/', + }, + { + extraPath: '', + url: 'https://docs.mycompany.com/hello', + }, + ]); + }); }); diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 24fa08ce8..b3a2eaeda 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -2,7 +2,8 @@ * For a given GitBook URL, return a list of alternative URLs that could be matched against to lookup the content. * The approach is optimized to aim at reusing cached lookup results as much as possible. */ -export function getURLLookupAlternatives(url: URL) { +export function getURLLookupAlternatives(input: URL) { + const url = normalizeURL(input); const alternatives: Array<{ url: string; extraPath: string }> = []; const pushAlternative = (url: URL, extraPath: string) => { @@ -85,3 +86,12 @@ export function getURLLookupAlternatives(url: URL) { return alternatives; } + +/** + * Normalize a URL to remove duplicate slashes and trailing slashes + */ +export function normalizeURL(url: URL) { + const result = new URL(url); + result.pathname = url.pathname.replace(/\/{2,}/g, '/').replace(/\/$/, ''); + return result; +} diff --git a/src/middleware.ts b/src/middleware.ts index 16668687f..6c9a45c0c 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -12,10 +12,9 @@ import { userAgent, withAPI, } from '@/lib/api'; +import { buildVersion } from '@/lib/build'; import { createContentSecurityPolicyNonce, getContentSecurityPolicy } from '@/lib/csp'; - -import { buildVersion } from './lib/build'; -import { getURLLookupAlternatives } from './lib/middleware'; +import { getURLLookupAlternatives, normalizeURL } from '@/lib/middleware'; export const config = { matcher: @@ -73,6 +72,12 @@ export async function middleware(request: NextRequest) { userAgent: userAgent(), }); + // Redirect to normalize the URL + const normalized = normalizeURL(url); + if (normalized.toString() !== url.toString()) { + return NextResponse.redirect(normalized.toString()); + } + // The visitor authentication can either be passed as a query parameter // or can be stored in a cookie after the initial auth. const visitorAuthToken =