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,