11import { CustomizationHeaderPreset , CustomizationThemeMode } from '@gitbook/api' ;
22import { Metadata , Viewport } from 'next' ;
3- import { headers } from 'next/headers' ;
43import { notFound , redirect } from 'next/navigation' ;
5- import React , { Suspense } from 'react' ;
4+ import React from 'react' ;
65
76import { PageAside } from '@/components/PageAside' ;
87import { PageBody , PageCover } from '@/components/PageBody' ;
9- import { SkeletonHeading , SkeletonParagraph } from '@/components/primitives' ;
108import { PageHrefContext , absoluteHref , pageHref } from '@/lib/links' ;
119import { getPagePath , resolveFirstDocument } from '@/lib/pages' ;
1210import { ContentRefContext } from '@/lib/references' ;
@@ -19,23 +17,15 @@ import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } fr
1917
2018export const runtime = 'edge' ;
2119
22- type PageProps = {
20+ /**
21+ * Fetch and render a page.
22+ */
23+ export default async function Page ( props : {
2324 params : PagePathParams ;
2425 searchParams : { fallback ?: string } ;
25- } ;
26-
27- export default async function Page ( props : PageProps ) {
28- // We wrap the page in Suspense to enable streaming at page level
29- // it's only enabled in "navigation" mode
30- return (
31- < Suspense fallback = { < PageSkeleton /> } >
32- < PageContent { ...props } />
33- </ Suspense >
34- ) ;
35- }
26+ } ) {
27+ const { params, searchParams } = props ;
3628
37- async function PageContent ( props : PageProps ) {
38- const data = await getPageDataWithFallback ( props , { redirectOnFallback : true } ) ;
3929 const {
4030 content : contentPointer ,
4131 contentTarget,
@@ -46,7 +36,26 @@ async function PageContent(props: PageProps) {
4636 pages,
4737 page,
4838 document,
49- } = data ;
39+ } = await getPageDataWithFallback ( {
40+ pagePathParams : params ,
41+ searchParams,
42+ redirectOnFallback : true ,
43+ } ) ;
44+
45+ const linksContext : PageHrefContext = { } ;
46+ const rawPathname = getPathnameParam ( params ) ;
47+ if ( ! page ) {
48+ const pathname = normalizePathname ( rawPathname ) ;
49+ if ( pathname !== rawPathname ) {
50+ // If the pathname was not normalized, redirect to the normalized version
51+ // before trying to resolve the page again
52+ redirect ( absoluteHref ( pathname ) ) ;
53+ } else {
54+ notFound ( ) ;
55+ }
56+ } else if ( getPagePath ( pages , page ) !== rawPathname ) {
57+ redirect ( pageHref ( pages , page , linksContext ) ) ;
58+ }
5059
5160 const withTopHeader = customization . header . preset !== CustomizationHeaderPreset . None ;
5261 const withFullPageCover = ! ! (
@@ -69,8 +78,6 @@ async function PageContent(props: PageProps) {
6978
7079 return (
7180 < >
72- { /* Title is displayed by the browser, except in navigation mode */ }
73- < title > { getTitle ( data ) } </ title >
7481 { withFullPageCover && page . cover ? (
7582 < PageCover as = "full" page = { page } cover = { page . cover } context = { contentRefContext } />
7683 ) : null }
@@ -110,30 +117,7 @@ async function PageContent(props: PageProps) {
110117 ) ;
111118}
112119
113- function PageSkeleton ( ) {
114- return (
115- < div
116- className = { tcls (
117- 'flex' ,
118- 'flex-row' ,
119- 'flex-1' ,
120- 'relative' ,
121- 'py-8' ,
122- 'lg:px-16' ,
123- 'xl:mr-56' ,
124- 'items-center' ,
125- 'lg:items-start' ,
126- ) }
127- >
128- < div className = { tcls ( 'flex-1' , 'max-w-3xl' , 'mx-auto' , 'page-full-width:mx-0' ) } >
129- < SkeletonHeading style = { tcls ( 'mb-8' ) } />
130- < SkeletonParagraph style = { tcls ( 'mb-4' ) } />
131- </ div >
132- </ div >
133- ) ;
134- }
135-
136- export async function generateViewport ( { params } : PageProps ) : Promise < Viewport > {
120+ export async function generateViewport ( { params } : { params : PagePathParams } ) : Promise < Viewport > {
137121 const { customization } = await fetchPageData ( params ) ;
138122 return {
139123 colorScheme : customization . themes . toggeable
@@ -144,25 +128,26 @@ export async function generateViewport({ params }: PageProps): Promise<Viewport>
144128 } ;
145129}
146130
147- function getTitle ( input : Awaited < ReturnType < typeof getPageDataWithFallback > > ) {
148- const { page, space, customization, site } = input ;
149- return [ page . title , getContentTitle ( space , customization , site ?? null ) ]
150- . filter ( Boolean )
151- . join ( ' | ' ) ;
152- }
131+ export async function generateMetadata ( {
132+ params,
133+ searchParams,
134+ } : {
135+ params : PagePathParams ;
136+ searchParams : { fallback ?: string } ;
137+ } ) : Promise < Metadata > {
138+ const { space, pages, page, customization, site, ancestors } = await getPageDataWithFallback ( {
139+ pagePathParams : params ,
140+ searchParams,
141+ } ) ;
153142
154- export async function generateMetadata ( props : PageProps ) : Promise < Metadata > {
155- // We only generate metadata in navigation mode. Else we let the browser handle it.
156- if ( await checkIsInAppNavigation ( ) ) {
157- return { } ;
143+ if ( ! page ) {
144+ notFound ( ) ;
158145 }
159146
160- const data = await getPageDataWithFallback ( props , { redirectOnFallback : false } ) ;
161-
162- const { page, pages, space, customization, site, ancestors } = data ;
163-
164147 return {
165- title : getTitle ( data ) ,
148+ title : [ page . title , getContentTitle ( space , customization , site ?? null ) ]
149+ . filter ( Boolean )
150+ . join ( ' | ' ) ,
166151 description : page . description ?? '' ,
167152 alternates : {
168153 canonical : absoluteHref ( getPagePath ( pages , page ) , true ) ,
@@ -180,30 +165,17 @@ export async function generateMetadata(props: PageProps): Promise<Metadata> {
180165 } ;
181166}
182167
183- /**
184- * Check if the navigation is in-app, meaning the user clicks on a link.
185- */
186- async function checkIsInAppNavigation ( ) {
187- const headerList = await headers ( ) ;
188- const fetchMode = headerList . get ( 'sec-fetch-mode' ) ;
189-
190- return fetchMode === 'cors' ;
191- }
192-
193168/**
194169 * Fetches the page data matching the requested pathname and fallback to root page when page is not found.
195170 */
196- async function getPageDataWithFallback (
197- props : PageProps ,
198- behaviour : {
199- redirectOnFallback : boolean ;
200- } ,
201- ) {
202- await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
203- const { params, searchParams } = props ;
204- const { redirectOnFallback } = behaviour ;
171+ async function getPageDataWithFallback ( args : {
172+ pagePathParams : PagePathParams ;
173+ searchParams : { fallback ?: string } ;
174+ redirectOnFallback ?: boolean ;
175+ } ) {
176+ const { pagePathParams, searchParams, redirectOnFallback = false } = args ;
205177
206- const { pages, page : targetPage , ...otherPageData } = await fetchPageData ( params ) ;
178+ const { pages, page : targetPage , ...otherPageData } = await fetchPageData ( pagePathParams ) ;
207179
208180 let page = targetPage ;
209181 const canFallback = ! ! searchParams . fallback ;
@@ -217,21 +189,6 @@ async function getPageDataWithFallback(
217189 page = rootPage ?. page ;
218190 }
219191
220- const linksContext : PageHrefContext = { } ;
221- const rawPathname = getPathnameParam ( params ) ;
222- if ( ! page ) {
223- const pathname = normalizePathname ( rawPathname ) ;
224- if ( pathname !== rawPathname ) {
225- // If the pathname was not normalized, redirect to the normalized version
226- // before trying to resolve the page again
227- redirect ( absoluteHref ( pathname ) ) ;
228- } else {
229- notFound ( ) ;
230- }
231- } else if ( getPagePath ( pages , page ) !== rawPathname ) {
232- redirect ( pageHref ( pages , page , linksContext ) ) ;
233- }
234-
235192 return {
236193 ...otherPageData ,
237194 pages,
0 commit comments