@@ -44,6 +44,12 @@ type URLLookupMode =
4444 * This mode is useful when self-hosting a single space.
4545 */
4646 | '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'
4753 /**
4854 * Spaces are located using the incoming URL (using forwarded host headers).
4955 * This mode is the default one when serving on the GitBook infrastructure.
@@ -77,11 +83,14 @@ export type LookupResult = PublishedContentWithCache & {
7783} ;
7884
7985/**
80- * Middleware to lookup the space to render.
86+ * Middleware to lookup the site to render.
8187 * It takes as input a request with an URL, and a set of headers:
8288 * - x-gitbook-api: the API endpoint to use, if undefined, the default one is used
8389 * - x-gitbook-basepath: base in the path that should be ignored for routing
8490 *
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+ *
8594 * The middleware also takes care of persisting the visitor authentication state.
8695 */
8796export async function middleware ( request : NextRequest ) {
@@ -106,7 +115,7 @@ export async function middleware(request: NextRequest) {
106115 let apiEndpoint = request . headers . get ( 'x-gitbook-api' ) ?? DEFAULT_API_ENDPOINT ;
107116 const originBasePath = request . headers . get ( 'x-gitbook-basepath' ) ?? '' ;
108117
109- const inputURL = stripURLBasePath ( url , originBasePath ) ;
118+ const inputURL = mode === 'proxy' ? url : stripURLBasePath ( url , originBasePath ) ;
110119
111120 const resolved = await withAPI (
112121 {
@@ -117,7 +126,7 @@ export async function middleware(request: NextRequest) {
117126 } ) ,
118127 contextId : undefined ,
119128 } ,
120- ( ) => lookupSpaceForURL ( mode , request , inputURL ) ,
129+ ( ) => lookupSiteForURL ( mode , request , inputURL ) ,
121130 ) ;
122131 if ( 'error' in resolved ) {
123132 return new NextResponse ( resolved . error . message , {
@@ -211,7 +220,10 @@ export async function middleware(request: NextRequest) {
211220 }
212221 headers . set ( 'x-gitbook-mode' , mode ) ;
213222 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+ ) ;
215227 headers . set ( 'x-gitbook-content-space' , resolved . space ) ;
216228 if ( 'site' in resolved ) {
217229 headers . set ( 'x-gitbook-content-organization' , resolved . organization ) ;
@@ -302,7 +314,10 @@ export async function middleware(request: NextRequest) {
302314/**
303315 * Compute the input URL the user is trying to access.
304316 */
305- function getInputURL ( request : NextRequest ) : { url : URL ; mode : URLLookupMode } {
317+ function getInputURL ( request : NextRequest ) : {
318+ url : URL ;
319+ mode : URLLookupMode ;
320+ } {
306321 const url = new URL ( request . url ) ;
307322 let mode : URLLookupMode =
308323 ( process . env . GITBOOK_MODE as URLLookupMode | undefined ) ?? 'multi-path' ;
@@ -332,27 +347,36 @@ function getInputURL(request: NextRequest): { url: URL; mode: URLLookupMode } {
332347 mode = 'multi-id' ;
333348 }
334349
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+
335357 return { url, mode } ;
336358}
337359
338- async function lookupSpaceForURL (
360+ async function lookupSiteForURL (
339361 mode : URLLookupMode ,
340362 request : NextRequest ,
341363 url : URL ,
342364) : Promise < LookupResult > {
343365 switch ( mode ) {
344366 case 'single' : {
345- return await lookupSpaceInSingleMode ( url ) ;
367+ return await lookupSiteInSingleMode ( url ) ;
346368 }
347369 case 'multi' : {
348- return await lookupSpaceInMultiMode ( request , url ) ;
370+ return await lookupSiteInMultiMode ( request , url ) ;
349371 }
350372 case 'multi-path' : {
351- return await lookupSpaceInMultiPathMode ( request , url ) ;
373+ return await lookupSiteInMultiPathMode ( request , url ) ;
352374 }
353375 case 'multi-id' : {
354376 return await lookupSiteOrSpaceInMultiIdMode ( request , url ) ;
355377 }
378+ case 'proxy' :
379+ return await lookupSiteInProxy ( request , url ) ;
356380 default :
357381 assertNever ( mode ) ;
358382 }
@@ -362,7 +386,7 @@ async function lookupSpaceForURL(
362386 * GITBOOK_MODE=single
363387 * When serving a single space, configured using GITBOOK_SPACE_ID and GITBOOK_TOKEN.
364388 */
365- async function lookupSpaceInSingleMode ( url : URL ) : Promise < LookupResult > {
389+ async function lookupSiteInSingleMode ( url : URL ) : Promise < LookupResult > {
366390 const spaceId = process . env . GITBOOK_SPACE_ID ;
367391 if ( ! spaceId ) {
368392 throw new Error (
@@ -386,13 +410,31 @@ async function lookupSpaceInSingleMode(url: URL): Promise<LookupResult> {
386410 } ;
387411}
388412
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+
389431/**
390432 * GITBOOK_MODE=multi
391433 * When serving multi spaces based on the current URL.
392434 */
393- async function lookupSpaceInMultiMode ( request : NextRequest , url : URL ) : Promise < LookupResult > {
435+ async function lookupSiteInMultiMode ( request : NextRequest , url : URL ) : Promise < LookupResult > {
394436 const visitorAuthToken = getVisitorAuthToken ( request , url ) ;
395- const lookup = await lookupSpaceByAPI ( url , visitorAuthToken ) ;
437+ const lookup = await lookupSiteByAPI ( url , visitorAuthToken ) ;
396438 return {
397439 ...lookup ,
398440 ...( 'basePath' in lookup && visitorAuthToken
@@ -557,7 +599,7 @@ async function lookupSiteOrSpaceInMultiIdMode(
557599 * GITBOOK_MODE=multi-path
558600 * When serving multi spaces with the url passed in the path.
559601 */
560- async function lookupSpaceInMultiPathMode ( request : NextRequest , url : URL ) : Promise < LookupResult > {
602+ async function lookupSiteInMultiPathMode ( request : NextRequest , url : URL ) : Promise < LookupResult > {
561603 // Skip useless requests
562604 if (
563605 url . pathname === '/favicon.ico' ||
@@ -596,7 +638,7 @@ async function lookupSpaceInMultiPathMode(request: NextRequest, url: URL): Promi
596638
597639 const visitorAuthToken = getVisitorAuthToken ( request , target ) ;
598640
599- const lookup = await lookupSpaceByAPI ( target , visitorAuthToken ) ;
641+ const lookup = await lookupSiteByAPI ( target , visitorAuthToken ) ;
600642 if ( 'error' in lookup ) {
601643 return lookup ;
602644 }
@@ -632,7 +674,7 @@ async function lookupSpaceInMultiPathMode(request: NextRequest, url: URL): Promi
632674 * Lookup a space by its URL using the GitBook API.
633675 * To optimize caching, we try multiple lookup alternatives and return the first one that matches.
634676 */
635- async function lookupSpaceByAPI (
677+ async function lookupSiteByAPI (
636678 lookupURL : URL ,
637679 visitorAuthToken : ReturnType < typeof getVisitorAuthToken > ,
638680) : Promise < LookupResult > {
0 commit comments