@@ -44,6 +44,12 @@ type URLLookupMode =
44
44
* This mode is useful when self-hosting a single space.
45
45
*/
46
46
| 'single'
47
+ /**
48
+ * Mode when a site is being proxied on a different base URL.
49
+ * - x-gitbook-site-url is used to determine the site to serve.
50
+ * - host / x-forwarded-host / x-gitbook-host + x-gitbook-basepath is used to determine the base URL.
51
+ */
52
+ | 'proxy'
47
53
/**
48
54
* Spaces are located using the incoming URL (using forwarded host headers).
49
55
* This mode is the default one when serving on the GitBook infrastructure.
@@ -77,11 +83,14 @@ export type LookupResult = PublishedContentWithCache & {
77
83
} ;
78
84
79
85
/**
80
- * Middleware to lookup the space to render.
86
+ * Middleware to lookup the site to render.
81
87
* It takes as input a request with an URL, and a set of headers:
82
88
* - x-gitbook-api: the API endpoint to use, if undefined, the default one is used
83
89
* - x-gitbook-basepath: base in the path that should be ignored for routing
84
90
*
91
+ * Once the site has been looked-up, the middleware passes the info to the rendering
92
+ * using a rewrite with a set of headers. This is the only way in next.js to do this (basically similar to AsyncLocalStorage).
93
+ *
85
94
* The middleware also takes care of persisting the visitor authentication state.
86
95
*/
87
96
export async function middleware ( request : NextRequest ) {
@@ -106,7 +115,7 @@ export async function middleware(request: NextRequest) {
106
115
let apiEndpoint = request . headers . get ( 'x-gitbook-api' ) ?? DEFAULT_API_ENDPOINT ;
107
116
const originBasePath = request . headers . get ( 'x-gitbook-basepath' ) ?? '' ;
108
117
109
- const inputURL = stripURLBasePath ( url , originBasePath ) ;
118
+ const inputURL = mode === 'proxy' ? url : stripURLBasePath ( url , originBasePath ) ;
110
119
111
120
const resolved = await withAPI (
112
121
{
@@ -117,7 +126,7 @@ export async function middleware(request: NextRequest) {
117
126
} ) ,
118
127
contextId : undefined ,
119
128
} ,
120
- ( ) => lookupSpaceForURL ( mode , request , inputURL ) ,
129
+ ( ) => lookupSiteForURL ( mode , request , inputURL ) ,
121
130
) ;
122
131
if ( 'error' in resolved ) {
123
132
return new NextResponse ( resolved . error . message , {
@@ -211,7 +220,10 @@ export async function middleware(request: NextRequest) {
211
220
}
212
221
headers . set ( 'x-gitbook-mode' , mode ) ;
213
222
headers . set ( 'x-gitbook-origin-basepath' , originBasePath ) ;
214
- headers . set ( 'x-gitbook-basepath' , joinPath ( originBasePath , resolved . basePath ) ) ;
223
+ headers . set (
224
+ 'x-gitbook-basepath' ,
225
+ mode === 'proxy' ? originBasePath : joinPath ( originBasePath , resolved . basePath ) ,
226
+ ) ;
215
227
headers . set ( 'x-gitbook-content-space' , resolved . space ) ;
216
228
if ( 'site' in resolved ) {
217
229
headers . set ( 'x-gitbook-content-organization' , resolved . organization ) ;
@@ -302,7 +314,10 @@ export async function middleware(request: NextRequest) {
302
314
/**
303
315
* Compute the input URL the user is trying to access.
304
316
*/
305
- function getInputURL ( request : NextRequest ) : { url : URL ; mode : URLLookupMode } {
317
+ function getInputURL ( request : NextRequest ) : {
318
+ url : URL ;
319
+ mode : URLLookupMode ;
320
+ } {
306
321
const url = new URL ( request . url ) ;
307
322
let mode : URLLookupMode =
308
323
( process . env . GITBOOK_MODE as URLLookupMode | undefined ) ?? 'multi-path' ;
@@ -332,27 +347,36 @@ function getInputURL(request: NextRequest): { url: URL; mode: URLLookupMode } {
332
347
mode = 'multi-id' ;
333
348
}
334
349
350
+ // When passing a x-gitbook-site-url header, this URL is used instead of the request URL
351
+ // to determine the site to serve.
352
+ const xGitbookSite = request . headers . get ( 'x-gitbook-site-url' ) ;
353
+ if ( xGitbookSite ) {
354
+ mode = 'proxy' ;
355
+ }
356
+
335
357
return { url, mode } ;
336
358
}
337
359
338
- async function lookupSpaceForURL (
360
+ async function lookupSiteForURL (
339
361
mode : URLLookupMode ,
340
362
request : NextRequest ,
341
363
url : URL ,
342
364
) : Promise < LookupResult > {
343
365
switch ( mode ) {
344
366
case 'single' : {
345
- return await lookupSpaceInSingleMode ( url ) ;
367
+ return await lookupSiteInSingleMode ( url ) ;
346
368
}
347
369
case 'multi' : {
348
- return await lookupSpaceInMultiMode ( request , url ) ;
370
+ return await lookupSiteInMultiMode ( request , url ) ;
349
371
}
350
372
case 'multi-path' : {
351
- return await lookupSpaceInMultiPathMode ( request , url ) ;
373
+ return await lookupSiteInMultiPathMode ( request , url ) ;
352
374
}
353
375
case 'multi-id' : {
354
376
return await lookupSiteOrSpaceInMultiIdMode ( request , url ) ;
355
377
}
378
+ case 'proxy' :
379
+ return await lookupSiteInProxy ( request , url ) ;
356
380
default :
357
381
assertNever ( mode ) ;
358
382
}
@@ -362,7 +386,7 @@ async function lookupSpaceForURL(
362
386
* GITBOOK_MODE=single
363
387
* When serving a single space, configured using GITBOOK_SPACE_ID and GITBOOK_TOKEN.
364
388
*/
365
- async function lookupSpaceInSingleMode ( url : URL ) : Promise < LookupResult > {
389
+ async function lookupSiteInSingleMode ( url : URL ) : Promise < LookupResult > {
366
390
const spaceId = process . env . GITBOOK_SPACE_ID ;
367
391
if ( ! spaceId ) {
368
392
throw new Error (
@@ -386,13 +410,31 @@ async function lookupSpaceInSingleMode(url: URL): Promise<LookupResult> {
386
410
} ;
387
411
}
388
412
413
+ /**
414
+ * GITBOOK_MODE=proxy
415
+ * When proxying a site on a different base URL.
416
+ */
417
+ async function lookupSiteInProxy ( request : NextRequest , url : URL ) : Promise < LookupResult > {
418
+ const rawSiteUrl = request . headers . get ( 'x-gitbook-site-url' ) ;
419
+ if ( ! rawSiteUrl ) {
420
+ throw new Error (
421
+ `Missing x-gitbook-site-url header. It should be passed when using GITBOOK_MODE=proxy.` ,
422
+ ) ;
423
+ }
424
+
425
+ const siteUrl = new URL ( rawSiteUrl ) ;
426
+ siteUrl . pathname = joinPath ( siteUrl . pathname , url . pathname ) ;
427
+
428
+ return await lookupSiteInMultiMode ( request , siteUrl ) ;
429
+ }
430
+
389
431
/**
390
432
* GITBOOK_MODE=multi
391
433
* When serving multi spaces based on the current URL.
392
434
*/
393
- async function lookupSpaceInMultiMode ( request : NextRequest , url : URL ) : Promise < LookupResult > {
435
+ async function lookupSiteInMultiMode ( request : NextRequest , url : URL ) : Promise < LookupResult > {
394
436
const visitorAuthToken = getVisitorAuthToken ( request , url ) ;
395
- const lookup = await lookupSpaceByAPI ( url , visitorAuthToken ) ;
437
+ const lookup = await lookupSiteByAPI ( url , visitorAuthToken ) ;
396
438
return {
397
439
...lookup ,
398
440
...( 'basePath' in lookup && visitorAuthToken
@@ -557,7 +599,7 @@ async function lookupSiteOrSpaceInMultiIdMode(
557
599
* GITBOOK_MODE=multi-path
558
600
* When serving multi spaces with the url passed in the path.
559
601
*/
560
- async function lookupSpaceInMultiPathMode ( request : NextRequest , url : URL ) : Promise < LookupResult > {
602
+ async function lookupSiteInMultiPathMode ( request : NextRequest , url : URL ) : Promise < LookupResult > {
561
603
// Skip useless requests
562
604
if (
563
605
url . pathname === '/favicon.ico' ||
@@ -596,7 +638,7 @@ async function lookupSpaceInMultiPathMode(request: NextRequest, url: URL): Promi
596
638
597
639
const visitorAuthToken = getVisitorAuthToken ( request , target ) ;
598
640
599
- const lookup = await lookupSpaceByAPI ( target , visitorAuthToken ) ;
641
+ const lookup = await lookupSiteByAPI ( target , visitorAuthToken ) ;
600
642
if ( 'error' in lookup ) {
601
643
return lookup ;
602
644
}
@@ -632,7 +674,7 @@ async function lookupSpaceInMultiPathMode(request: NextRequest, url: URL): Promi
632
674
* Lookup a space by its URL using the GitBook API.
633
675
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
634
676
*/
635
- async function lookupSpaceByAPI (
677
+ async function lookupSiteByAPI (
636
678
lookupURL : URL ,
637
679
visitorAuthToken : ReturnType < typeof getVisitorAuthToken > ,
638
680
) : Promise < LookupResult > {
0 commit comments