Skip to content

Commit 2b501f1

Browse files
authored
Serve the correct status code for not found and redirects 👌 (#2582)
1 parent aa2ed0f commit 2b501f1

File tree

4 files changed

+95
-80
lines changed

4 files changed

+95
-80
lines changed

‎bun.lockb‎

4.17 KB
Binary file not shown.

‎packages/gitbook/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"katex": "^0.16.9",
4242
"mathjax": "^3.2.2",
4343
"memoizee": "^0.4.15",
44-
"next": "14.2.15",
44+
"next": "14.2.18",
4545
"next-themes": "^0.2.1",
4646
"nuqs": "^1.17.4",
4747
"object-hash": "^3.0.0",

‎packages/gitbook/src/app/(site)/(content)/[[...pathname]]/loading.tsx‎

Lines changed: 0 additions & 28 deletions
This file was deleted.

‎packages/gitbook/src/app/(site)/(content)/[[...pathname]]/page.tsx‎

Lines changed: 94 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { CustomizationHeaderPreset, CustomizationThemeMode } from '@gitbook/api';
22
import { Metadata, Viewport } from 'next';
3+
import { headers } from 'next/headers';
34
import { notFound, redirect } from 'next/navigation';
4-
import React from 'react';
5+
import React, { Suspense } from 'react';
56

67
import { PageAside } from '@/components/PageAside';
78
import { PageBody, PageCover } from '@/components/PageBody';
9+
import { SkeletonHeading, SkeletonParagraph } from '@/components/primitives';
810
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
911
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
1012
import { ContentRefContext } from '@/lib/references';
@@ -17,15 +19,23 @@ import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } fr
1719

1820
export const runtime = 'edge';
1921

20-
/**
21-
* Fetch and render a page.
22-
*/
23-
export default async function Page(props: {
22+
type PageProps = {
2423
params: PagePathParams;
2524
searchParams: { fallback?: string };
26-
}) {
27-
const { params, searchParams } = props;
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+
}
2836

37+
async function PageContent(props: PageProps) {
38+
const data = await getPageDataWithFallback(props, { redirectOnFallback: true });
2939
const {
3040
content: contentPointer,
3141
contentTarget,
@@ -36,26 +46,7 @@ export default async function Page(props: {
3646
pages,
3747
page,
3848
document,
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-
}
49+
} = data;
5950

6051
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
6152
const withFullPageCover = !!(
@@ -78,6 +69,8 @@ export default async function Page(props: {
7869

7970
return (
8071
<>
72+
{/* Title is displayed by the browser, except in navigation mode */}
73+
<title>{getTitle(data)}</title>
8174
{withFullPageCover && page.cover ? (
8275
<PageCover as="full" page={page} cover={page.cover} context={contentRefContext} />
8376
) : null}
@@ -117,7 +110,30 @@ export default async function Page(props: {
117110
);
118111
}
119112

120-
export async function generateViewport({ params }: { params: PagePathParams }): Promise<Viewport> {
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> {
121137
const { customization } = await fetchPageData(params);
122138
return {
123139
colorScheme: customization.themes.toggeable
@@ -128,26 +144,25 @@ export async function generateViewport({ params }: { params: PagePathParams }):
128144
};
129145
}
130146

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-
});
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+
}
142153

143-
if (!page) {
144-
notFound();
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 {};
145158
}
146159

160+
const data = await getPageDataWithFallback(props, { redirectOnFallback: false });
161+
162+
const { page, pages, space, customization, site, ancestors } = data;
163+
147164
return {
148-
title: [page.title, getContentTitle(space, customization, site ?? null)]
149-
.filter(Boolean)
150-
.join(' | '),
165+
title: getTitle(data),
151166
description: page.description ?? '',
152167
alternates: {
153168
canonical: absoluteHref(getPagePath(pages, page), true),
@@ -165,17 +180,30 @@ export async function generateMetadata({
165180
};
166181
}
167182

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+
168193
/**
169194
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
170195
*/
171-
async function getPageDataWithFallback(args: {
172-
pagePathParams: PagePathParams;
173-
searchParams: { fallback?: string };
174-
redirectOnFallback?: boolean;
175-
}) {
176-
const { pagePathParams, searchParams, redirectOnFallback = false } = args;
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;
177205

178-
const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams);
206+
const { pages, page: targetPage, ...otherPageData } = await fetchPageData(params);
179207

180208
let page = targetPage;
181209
const canFallback = !!searchParams.fallback;
@@ -189,6 +217,21 @@ async function getPageDataWithFallback(args: {
189217
page = rootPage?.page;
190218
}
191219

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+
192235
return {
193236
...otherPageData,
194237
pages,

0 commit comments

Comments
 (0)