diff --git a/bun.lockb b/bun.lockb
index c51581464d..c08095915f 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 80e9363091..7ec5c1f862 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -41,7 +41,7 @@
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"memoizee": "^0.4.15",
- "next": "14.2.18",
+ "next": "14.2.15",
"next-themes": "^0.2.1",
"nuqs": "^1.17.4",
"object-hash": "^3.0.0",
diff --git a/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/loading.tsx b/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/loading.tsx
new file mode 100644
index 0000000000..4b269d89e5
--- /dev/null
+++ b/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/loading.tsx
@@ -0,0 +1,28 @@
+import { SkeletonHeading, SkeletonParagraph } from '@/components/primitives';
+import { tcls } from '@/lib/tailwind';
+
+/**
+ * Placeholder when loading a page.
+ */
+export default function PageSkeleton() {
+ return (
+
+ );
+}
diff --git a/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/page.tsx
index b944061cb7..095e9841a6 100644
--- a/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/page.tsx
+++ b/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/page.tsx
@@ -1,12 +1,10 @@
import { CustomizationHeaderPreset, CustomizationThemeMode } from '@gitbook/api';
import { Metadata, Viewport } from 'next';
-import { headers } from 'next/headers';
import { notFound, redirect } from 'next/navigation';
-import React, { Suspense } from 'react';
+import React from 'react';
import { PageAside } from '@/components/PageAside';
import { PageBody, PageCover } from '@/components/PageBody';
-import { SkeletonHeading, SkeletonParagraph } from '@/components/primitives';
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
import { ContentRefContext } from '@/lib/references';
@@ -19,23 +17,15 @@ import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } fr
export const runtime = 'edge';
-type PageProps = {
+/**
+ * Fetch and render a page.
+ */
+export default async function Page(props: {
params: PagePathParams;
searchParams: { fallback?: string };
-};
-
-export default async function Page(props: PageProps) {
- // We wrap the page in Suspense to enable streaming at page level
- // it's only enabled in "navigation" mode
- return (
- }>
-
-
- );
-}
+}) {
+ const { params, searchParams } = props;
-async function PageContent(props: PageProps) {
- const data = await getPageDataWithFallback(props, { redirectOnFallback: true });
const {
content: contentPointer,
contentTarget,
@@ -46,7 +36,26 @@ async function PageContent(props: PageProps) {
pages,
page,
document,
- } = data;
+ } = await getPageDataWithFallback({
+ pagePathParams: params,
+ searchParams,
+ redirectOnFallback: true,
+ });
+
+ const linksContext: PageHrefContext = {};
+ const rawPathname = getPathnameParam(params);
+ if (!page) {
+ const pathname = normalizePathname(rawPathname);
+ if (pathname !== rawPathname) {
+ // If the pathname was not normalized, redirect to the normalized version
+ // before trying to resolve the page again
+ redirect(absoluteHref(pathname));
+ } else {
+ notFound();
+ }
+ } else if (getPagePath(pages, page) !== rawPathname) {
+ redirect(pageHref(pages, page, linksContext));
+ }
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
const withFullPageCover = !!(
@@ -69,8 +78,6 @@ async function PageContent(props: PageProps) {
return (
<>
- {/* Title is displayed by the browser, except in navigation mode */}
- {getTitle(data)}
{withFullPageCover && page.cover ? (
) : null}
@@ -110,30 +117,7 @@ async function PageContent(props: PageProps) {
);
}
-function PageSkeleton() {
- return (
-
- );
-}
-
-export async function generateViewport({ params }: PageProps): Promise {
+export async function generateViewport({ params }: { params: PagePathParams }): Promise {
const { customization } = await fetchPageData(params);
return {
colorScheme: customization.themes.toggeable
@@ -144,25 +128,26 @@ export async function generateViewport({ params }: PageProps): Promise
};
}
-function getTitle(input: Awaited>) {
- const { page, space, customization, site } = input;
- return [page.title, getContentTitle(space, customization, site ?? null)]
- .filter(Boolean)
- .join(' | ');
-}
+export async function generateMetadata({
+ params,
+ searchParams,
+}: {
+ params: PagePathParams;
+ searchParams: { fallback?: string };
+}): Promise {
+ const { space, pages, page, customization, site, ancestors } = await getPageDataWithFallback({
+ pagePathParams: params,
+ searchParams,
+ });
-export async function generateMetadata(props: PageProps): Promise {
- // We only generate metadata in navigation mode. Else we let the browser handle it.
- if (await checkIsInAppNavigation()) {
- return {};
+ if (!page) {
+ notFound();
}
- const data = await getPageDataWithFallback(props, { redirectOnFallback: false });
-
- const { page, pages, space, customization, site, ancestors } = data;
-
return {
- title: getTitle(data),
+ title: [page.title, getContentTitle(space, customization, site ?? null)]
+ .filter(Boolean)
+ .join(' | '),
description: page.description ?? '',
alternates: {
canonical: absoluteHref(getPagePath(pages, page), true),
@@ -180,30 +165,17 @@ export async function generateMetadata(props: PageProps): Promise {
};
}
-/**
- * Check if the navigation is in-app, meaning the user clicks on a link.
- */
-async function checkIsInAppNavigation() {
- const headerList = await headers();
- const fetchMode = headerList.get('sec-fetch-mode');
-
- return fetchMode === 'cors';
-}
-
/**
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
*/
-async function getPageDataWithFallback(
- props: PageProps,
- behaviour: {
- redirectOnFallback: boolean;
- },
-) {
- await new Promise((resolve) => setTimeout(resolve, 2000));
- const { params, searchParams } = props;
- const { redirectOnFallback } = behaviour;
+async function getPageDataWithFallback(args: {
+ pagePathParams: PagePathParams;
+ searchParams: { fallback?: string };
+ redirectOnFallback?: boolean;
+}) {
+ const { pagePathParams, searchParams, redirectOnFallback = false } = args;
- const { pages, page: targetPage, ...otherPageData } = await fetchPageData(params);
+ const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams);
let page = targetPage;
const canFallback = !!searchParams.fallback;
@@ -217,21 +189,6 @@ async function getPageDataWithFallback(
page = rootPage?.page;
}
- const linksContext: PageHrefContext = {};
- const rawPathname = getPathnameParam(params);
- if (!page) {
- const pathname = normalizePathname(rawPathname);
- if (pathname !== rawPathname) {
- // If the pathname was not normalized, redirect to the normalized version
- // before trying to resolve the page again
- redirect(absoluteHref(pathname));
- } else {
- notFound();
- }
- } else if (getPagePath(pages, page) !== rawPathname) {
- redirect(pageHref(pages, page, linksContext));
- }
-
return {
...otherPageData,
pages,