1
1
import { CustomizationHeaderPreset , CustomizationThemeMode } from '@gitbook/api' ;
2
2
import { Metadata , Viewport } from 'next' ;
3
- import { headers } from 'next/headers' ;
4
3
import { notFound , redirect } from 'next/navigation' ;
5
- import React , { Suspense } from 'react' ;
4
+ import React from 'react' ;
6
5
7
6
import { PageAside } from '@/components/PageAside' ;
8
7
import { PageBody , PageCover } from '@/components/PageBody' ;
9
- import { SkeletonHeading , SkeletonParagraph } from '@/components/primitives' ;
10
8
import { PageHrefContext , absoluteHref , pageHref } from '@/lib/links' ;
11
9
import { getPagePath , resolveFirstDocument } from '@/lib/pages' ;
12
10
import { ContentRefContext } from '@/lib/references' ;
@@ -19,23 +17,15 @@ import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } fr
19
17
20
18
export const runtime = 'edge' ;
21
19
22
- type PageProps = {
20
+ /**
21
+ * Fetch and render a page.
22
+ */
23
+ export default async function Page ( props : {
23
24
params : PagePathParams ;
24
25
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 ;
36
28
37
- async function PageContent ( props : PageProps ) {
38
- const data = await getPageDataWithFallback ( props , { redirectOnFallback : true } ) ;
39
29
const {
40
30
content : contentPointer ,
41
31
contentTarget,
@@ -46,7 +36,26 @@ async function PageContent(props: PageProps) {
46
36
pages,
47
37
page,
48
38
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
+ }
50
59
51
60
const withTopHeader = customization . header . preset !== CustomizationHeaderPreset . None ;
52
61
const withFullPageCover = ! ! (
@@ -69,8 +78,6 @@ async function PageContent(props: PageProps) {
69
78
70
79
return (
71
80
< >
72
- { /* Title is displayed by the browser, except in navigation mode */ }
73
- < title > { getTitle ( data ) } </ title >
74
81
{ withFullPageCover && page . cover ? (
75
82
< PageCover as = "full" page = { page } cover = { page . cover } context = { contentRefContext } />
76
83
) : null }
@@ -110,30 +117,7 @@ async function PageContent(props: PageProps) {
110
117
) ;
111
118
}
112
119
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 > {
137
121
const { customization } = await fetchPageData ( params ) ;
138
122
return {
139
123
colorScheme : customization . themes . toggeable
@@ -144,25 +128,26 @@ export async function generateViewport({ params }: PageProps): Promise<Viewport>
144
128
} ;
145
129
}
146
130
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
+ } ) ;
153
142
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 ( ) ;
158
145
}
159
146
160
- const data = await getPageDataWithFallback ( props , { redirectOnFallback : false } ) ;
161
-
162
- const { page, pages, space, customization, site, ancestors } = data ;
163
-
164
147
return {
165
- title : getTitle ( data ) ,
148
+ title : [ page . title , getContentTitle ( space , customization , site ?? null ) ]
149
+ . filter ( Boolean )
150
+ . join ( ' | ' ) ,
166
151
description : page . description ?? '' ,
167
152
alternates : {
168
153
canonical : absoluteHref ( getPagePath ( pages , page ) , true ) ,
@@ -180,30 +165,17 @@ export async function generateMetadata(props: PageProps): Promise<Metadata> {
180
165
} ;
181
166
}
182
167
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
-
193
168
/**
194
169
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
195
170
*/
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 ;
205
177
206
- const { pages, page : targetPage , ...otherPageData } = await fetchPageData ( params ) ;
178
+ const { pages, page : targetPage , ...otherPageData } = await fetchPageData ( pagePathParams ) ;
207
179
208
180
let page = targetPage ;
209
181
const canFallback = ! ! searchParams . fallback ;
@@ -217,21 +189,6 @@ async function getPageDataWithFallback(
217
189
page = rootPage ?. page ;
218
190
}
219
191
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
-
235
192
return {
236
193
...otherPageData ,
237
194
pages,
0 commit comments