@@ -5,9 +5,15 @@ import { NextResponse } from 'next/server';
55import { getContentSecurityPolicy } from '@/lib/csp' ;
66import { validateSerializedCustomization } from '@/lib/customization' ;
77import { removeLeadingSlash , removeTrailingSlash } from '@/lib/paths' ;
8- import { getResponseCookiesForVisitorAuth , getVisitorToken } from '@/lib/visitor-token' ;
8+ import {
9+ type ResponseCookies ,
10+ getResponseCookiesForVisitorAuth ,
11+ getVisitorToken ,
12+ normalizeVisitorAuthURL ,
13+ } from '@/lib/visitor-token' ;
914import { serveResizedImage } from '@/routes/image' ;
10- import { getPublishedContentByURL } from '@v2/lib/data' ;
15+ import { getLinkerForSiteURL } from '@v2/lib/context' ;
16+ import { getPublishedContentByURL , normalizeURL } from '@v2/lib/data' ;
1117import { isGitBookAssetsHostURL , isGitBookHostURL } from '@v2/lib/env' ;
1218import { MiddlewareHeaders } from '@v2/lib/middleware' ;
1319
@@ -21,6 +27,14 @@ type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
2127
2228export async function middleware ( request : NextRequest ) {
2329 try {
30+ const requestURL = new URL ( request . url ) ;
31+
32+ // Redirect to normalize the URL
33+ const normalized = normalizeURL ( requestURL ) ;
34+ if ( normalized . toString ( ) !== requestURL . toString ( ) ) {
35+ return NextResponse . redirect ( normalized . toString ( ) ) ;
36+ }
37+
2438 // Route all requests to a site
2539 const extracted = getSiteURLFromRequest ( request ) ;
2640 if ( extracted ) {
@@ -38,7 +52,7 @@ export async function middleware(request: NextRequest) {
3852 } ) ;
3953 }
4054
41- return await serveSiteByURL ( request , extracted ) ;
55+ return await serveSiteByURL ( requestURL , request , extracted ) ;
4256 }
4357
4458 // Handle the rest with the router default logic
@@ -51,7 +65,7 @@ export async function middleware(request: NextRequest) {
5165/**
5266 * Serve site by URL.
5367 */
54- async function serveSiteByURL ( request : NextRequest , urlWithMode : URLWithMode ) {
68+ async function serveSiteByURL ( requestURL : URL , request : NextRequest , urlWithMode : URLWithMode ) {
5569 const { url, mode } = urlWithMode ;
5670
5771 // Visitor authentication
@@ -72,11 +86,55 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
7286 }
7387
7488 const { data } = result ;
89+ let cookies : ResponseCookies = { } ;
7590
91+ //
92+ // Handle redirects
93+ //
7694 if ( 'redirect' in data ) {
95+ // biome-ignore lint/suspicious/noConsole: we want to log the redirect
96+ console . log ( 'redirect' , data . redirect ) ;
97+ if ( data . target === 'content' ) {
98+ // For content redirects, we use the linker to redirect the optimal URL
99+ // during development and testing in 'url' mode.
100+ const linker = getLinkerForSiteURL ( {
101+ siteURL : url ,
102+ urlMode : mode ,
103+ } ) ;
104+
105+ const contentRedirect = new URL ( linker . toLinkForContent ( data . redirect ) , request . url ) ;
106+
107+ // Keep the same search params as the original request
108+ // as it might contain a VA token
109+ contentRedirect . search = request . nextUrl . search ;
110+
111+ return NextResponse . redirect ( contentRedirect ) ;
112+ }
113+
77114 return NextResponse . redirect ( data . redirect ) ;
78115 }
79116
117+ cookies = {
118+ ...cookies ,
119+ ...getResponseCookiesForVisitorAuth ( data . basePath , visitorToken ) ,
120+ } ;
121+
122+ //
123+ // Make sure the URL is clean of any va token after a successful lookup
124+ // The token is stored in a cookie that is set on the redirect response
125+ //
126+ const requestURLWithoutToken = normalizeVisitorAuthURL ( requestURL ) ;
127+ if ( requestURLWithoutToken . toString ( ) !== requestURL . toString ( ) ) {
128+ return writeResponseCookies (
129+ NextResponse . redirect ( requestURLWithoutToken . toString ( ) ) ,
130+ cookies
131+ ) ;
132+ }
133+
134+ //
135+ // Render and serve the content
136+ //
137+
80138 // When visitor has authentication (adaptive content or VA), we serve dynamic routes.
81139 let routeType = visitorToken ? 'dynamic' : 'static' ;
82140
@@ -108,13 +166,13 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
108166 requestHeaders . set ( 'x-forwarded-host' , request . nextUrl . host ) ;
109167 requestHeaders . set ( 'origin' , request . nextUrl . origin ) ;
110168
111- const siteURL = `${ url . host } ${ data . basePath } ` ;
169+ const siteURLWithoutProtocol = `${ url . host } ${ data . basePath } ` ;
112170
113171 const route = [
114172 'sites' ,
115173 routeType ,
116174 mode ,
117- encodeURIComponent ( siteURL ) ,
175+ encodeURIComponent ( siteURLWithoutProtocol ) ,
118176 encodePathInSiteContent ( data . pathname ) ,
119177 ] . join ( '/' ) ;
120178
@@ -135,16 +193,9 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
135193 response . headers . set ( 'x-content-type-options' , 'nosniff' ) ;
136194 // Debug header
137195 response . headers . set ( 'x-gitbook-route-type' , routeType ) ;
138- response . headers . set ( 'x-gitbook-site-url ' , siteURL ) ;
196+ response . headers . set ( 'x-gitbook-route-site ' , siteURLWithoutProtocol ) ;
139197
140- if ( visitorToken ) {
141- const cookies = getResponseCookiesForVisitorAuth ( data . basePath , visitorToken ) ;
142- for ( const [ key , value ] of Object . entries ( cookies ) ) {
143- response . cookies . set ( key , value . value , value . options ) ;
144- }
145- }
146-
147- return response ;
198+ return writeResponseCookies ( response , cookies ) ;
148199}
149200
150201/**
@@ -248,3 +299,14 @@ function appendQueryParams(url: URL, from: URLSearchParams) {
248299
249300 return url ;
250301}
302+
303+ /**
304+ * Write the cookies to a response.
305+ */
306+ function writeResponseCookies < R extends NextResponse > ( response : R , cookies : ResponseCookies ) : R {
307+ Object . entries ( cookies ) . forEach ( ( [ key , { value, options } ] ) => {
308+ response . cookies . set ( key , value , options ) ;
309+ } ) ;
310+
311+ return response ;
312+ }
0 commit comments