Skip to content

Commit

Permalink
Cleanup headers usage (#2717)
Browse files Browse the repository at this point in the history
  • Loading branch information
gregberge authored Jan 10, 2025
1 parent 08acea6 commit ecfdb97
Show file tree
Hide file tree
Showing 61 changed files with 901 additions and 610 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-trains-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gitbook': patch
---

Fix multiple bugs due to headers read in an anarchic way in the app.
4 changes: 3 additions & 1 deletion packages/gitbook/src/app/(global)/~gitbook/image/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';

import { getGitBookContextFromHeaders } from '@/lib/gitbook-context';
import {
CURRENT_SIGNATURE_VERSION,
isSignatureVersion,
Expand Down Expand Up @@ -39,7 +40,8 @@ export async function GET(request: NextRequest) {
}

// Verify the signature
const verified = await verifyImageSignature(url, { signature, version: signatureVersion });
const ctx = getGitBookContextFromHeaders(request.headers);
const verified = await verifyImageSignature(ctx, url, { signature, version: signatureVersion });
if (!verified) {
return new Response(`Invalid signature "${signature ?? ''}" for "${url}"`, { status: 400 });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { headers } from 'next/headers';

import { TrackPageViewEvent } from '@/components/Insights';
import { getSpaceLanguage, t } from '@/intl/server';
import { getSiteData, getSpaceContentData } from '@/lib/api';
import { getGitBookContextFromHeaders } from '@/lib/gitbook-context';
import { getSiteContentPointer } from '@/lib/pointer';
import { tcls } from '@/lib/tailwind';

export default async function NotFound() {
const pointer = await getSiteContentPointer();
const ctx = getGitBookContextFromHeaders(await headers());
const pointer = getSiteContentPointer(ctx);
const [{ space }, { customization }] = await Promise.all([
getSpaceContentData(pointer, pointer.siteShareKey),
getSiteData(pointer),
getSpaceContentData(ctx, pointer, pointer.siteShareKey),
getSiteData(ctx, pointer),
]);

const language = getSpaceLanguage(customization);
Expand Down
87 changes: 48 additions & 39 deletions packages/gitbook/src/app/(site)/(content)/[[...pathname]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CustomizationHeaderPreset, CustomizationThemeMode } from '@gitbook/api';
import { Metadata, Viewport } from 'next';
import { headers } from 'next/headers';
import { notFound, redirect } from 'next/navigation';
import React from 'react';

import { PageAside } from '@/components/PageAside';
import { PageBody, PageCover } from '@/components/PageBody';
import { getGitBookContextFromHeaders, GitBookContext } from '@/lib/gitbook-context';
import { PageHrefContext, getAbsoluteHref, getPageHref } from '@/lib/links';
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
import { ContentRefContext } from '@/lib/references';
Expand All @@ -16,17 +18,21 @@ import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } fr

export const runtime = 'edge';

/**
* Fetch and render a page.
*/
export default async function Page(props: {
type Props = {
params: Promise<PagePathParams>;
searchParams: Promise<{ fallback?: string }>;
}) {
const { params: rawParams, searchParams: rawSearchParams } = props;
};

const params = await rawParams;
const searchParams = await rawSearchParams;
/**
* Fetch and render a page.
*/
export default async function Page(props: Props) {
const [headersList, params, searchParams] = await Promise.all([
headers(),
props.params,
props.searchParams,
]);
const ctx = getGitBookContextFromHeaders(headersList);

const {
content: contentPointer,
Expand All @@ -39,7 +45,7 @@ export default async function Page(props: {
page,
ancestors,
document,
} = await getPageDataWithFallback({
} = await getPageDataWithFallback(ctx, {
pagePathParams: params,
searchParams,
redirectOnFallback: true,
Expand All @@ -52,12 +58,12 @@ export default async function Page(props: {
if (pathname !== rawPathname) {
// If the pathname was not normalized, redirect to the normalized version
// before trying to resolve the page again
redirect(await getAbsoluteHref(pathname));
redirect(getAbsoluteHref(ctx, pathname));
} else {
notFound();
}
} else if (getPagePath(pages, page) !== rawPathname) {
redirect(await getPageHref(pages, page, linksContext));
redirect(getPageHref(ctx, pages, page, linksContext));
}

const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
Expand Down Expand Up @@ -121,12 +127,10 @@ export default async function Page(props: {
);
}

export async function generateViewport({
params,
}: {
params: Promise<PagePathParams>;
}): Promise<Viewport> {
const { customization } = await fetchPageData(await params);
export async function generateViewport(props: Props): Promise<Viewport> {
const [params, headersList] = await Promise.all([props.params, headers()]);
const ctx = getGitBookContextFromHeaders(headersList);
const { customization } = await fetchPageData(ctx, params);
return {
colorScheme: customization.themes.toggeable
? customization.themes.default === CustomizationThemeMode.Dark
Expand All @@ -136,17 +140,20 @@ export async function generateViewport({
};
}

export async function generateMetadata({
params,
searchParams,
}: {
params: Promise<PagePathParams>;
searchParams: Promise<{ fallback?: string }>;
}): Promise<Metadata> {
const { space, pages, page, customization, site, ancestors } = await getPageDataWithFallback({
pagePathParams: await params,
searchParams: await searchParams,
});
export async function generateMetadata(props: Props): Promise<Metadata> {
const [params, searchParams, headersList] = await Promise.all([
props.params,
props.searchParams,
headers(),
]);
const ctx = getGitBookContextFromHeaders(headersList);
const { space, pages, page, customization, site, ancestors } = await getPageDataWithFallback(
ctx,
{
pagePathParams: params,
searchParams: searchParams,
},
);

if (!page) {
notFound();
Expand All @@ -159,17 +166,16 @@ export async function generateMetadata({
description: page.description ?? '',
alternates: {
// Trim trailing slashes in canonical URL to match the redirect behavior
canonical: (await getAbsoluteHref(getPagePath(pages, page), true)).replace(/\/+$/, ''),
canonical: getAbsoluteHref(ctx, getPagePath(pages, page), true).replace(/\/+$/, ''),
},
openGraph: {
images: [
customization.socialPreview.url ??
(await getAbsoluteHref(`~gitbook/ogimage/${page.id}`, true)),
getAbsoluteHref(ctx, `~gitbook/ogimage/${page.id}`, true),
],
},
robots:
(await isSpaceIndexable({ space, site: site ?? null })) &&
isPageIndexable(ancestors, page)
isSpaceIndexable(ctx, { space, site: site ?? null }) && isPageIndexable(ancestors, page)
? 'index, follow'
: 'noindex, nofollow',
};
Expand All @@ -178,22 +184,25 @@ export async function generateMetadata({
/**
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
*/
async function getPageDataWithFallback(args: {
pagePathParams: PagePathParams;
searchParams: { fallback?: string };
redirectOnFallback?: boolean;
}) {
async function getPageDataWithFallback(
ctx: GitBookContext,
args: {
pagePathParams: PagePathParams;
searchParams: { fallback?: string };
redirectOnFallback?: boolean;
},
) {
const { pagePathParams, searchParams, redirectOnFallback = false } = args;

const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams);
const { pages, page: targetPage, ...otherPageData } = await fetchPageData(ctx, pagePathParams);

let page = targetPage;
const canFallback = !!searchParams.fallback;
if (!page && canFallback) {
const rootPage = resolveFirstDocument(pages, []);

if (redirectOnFallback && rootPage?.page) {
redirect(await getPageHref(pages, rootPage?.page));
redirect(getPageHref(ctx, pages, rootPage?.page));
}

page = rootPage?.page;
Expand Down
35 changes: 17 additions & 18 deletions packages/gitbook/src/app/(site)/(content)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { api } from '@/lib/api';
import { assetsDomain } from '@/lib/assets';
import { buildVersion } from '@/lib/build';
import { getContentSecurityPolicyNonce } from '@/lib/csp';
import { GitBookContext, getGitBookContextFromHeaders } from '@/lib/gitbook-context';
import { getAbsoluteHref, getBaseUrl } from '@/lib/links';
import { isSpaceIndexable } from '@/lib/seo';
import { getContentTitle } from '@/lib/utils';
Expand All @@ -27,9 +28,10 @@ export const runtime = 'edge';
* Layout when rendering the content.
*/
export default async function ContentLayout(props: { children: React.ReactNode }) {
const ctx = getGitBookContextFromHeaders(await headers());
const { children } = props;

const nonce = await getContentSecurityPolicyNonce();
const nonce = getContentSecurityPolicyNonce(ctx);
const {
content,
space,
Expand All @@ -41,10 +43,9 @@ export default async function ContentLayout(props: { children: React.ReactNode }
ancestors,
scripts,
sections,
} = await fetchContentData();
} = await fetchContentData(ctx);

const apiCtx = await api();
ReactDOM.preconnect(apiCtx.client.endpoint);
ReactDOM.preconnect(api(ctx).client.endpoint);
if (assetsDomain) {
ReactDOM.preconnect(assetsDomain);
}
Expand All @@ -56,7 +57,7 @@ export default async function ContentLayout(props: { children: React.ReactNode }
});
});

const queryStringTheme = await getQueryStringTheme();
const queryStringTheme = getQueryStringTheme(ctx);

return (
<NuqsAdapter>
Expand Down Expand Up @@ -105,7 +106,8 @@ export default async function ContentLayout(props: { children: React.ReactNode }
}

export async function generateViewport(): Promise<Viewport> {
const { customization } = await fetchContentData();
const ctx = getGitBookContextFromHeaders(await headers());
const { customization } = await fetchContentData(ctx);
return {
colorScheme: customization.themes.toggeable
? customization.themes.default === CustomizationThemeMode.Dark
Expand All @@ -116,46 +118,43 @@ export async function generateViewport(): Promise<Viewport> {
}

export async function generateMetadata(): Promise<Metadata> {
const { space, site, customization } = await fetchContentData();
const ctx = getGitBookContextFromHeaders(await headers());
const { space, site, customization } = await fetchContentData(ctx);
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;

return {
title: getContentTitle(space, customization, site),
generator: `GitBook (${buildVersion()})`,
metadataBase: new URL(await getBaseUrl()),
metadataBase: new URL(getBaseUrl(ctx)),
icons: {
icon: [
{
url:
customIcon?.light ??
(await getAbsoluteHref('~gitbook/icon?size=small&theme=light', true)),
getAbsoluteHref(ctx, '~gitbook/icon?size=small&theme=light', true),
type: 'image/png',
media: '(prefers-color-scheme: light)',
},
{
url:
customIcon?.dark ??
(await getAbsoluteHref('~gitbook/icon?size=small&theme=dark', true)),
getAbsoluteHref(ctx, '~gitbook/icon?size=small&theme=dark', true),
type: 'image/png',
media: '(prefers-color-scheme: dark)',
},
],
},
robots: (await isSpaceIndexable({ space, site })) ? 'index, follow' : 'noindex, nofollow',
robots: isSpaceIndexable(ctx, { space, site }) ? 'index, follow' : 'noindex, nofollow',
};
}

/**
* For preview, the theme can be set via query string (?theme=light).
*/
async function getQueryStringTheme() {
const headersList = await headers();
const queryStringTheme = headersList.get('x-gitbook-theme');
if (!queryStringTheme) {
function getQueryStringTheme(ctx: GitBookContext) {
if (!ctx.theme) {
return null;
}

return queryStringTheme === 'light'
? CustomizationThemeMode.Light
: CustomizationThemeMode.Dark;
return ctx.theme === 'light' ? CustomizationThemeMode.Light : CustomizationThemeMode.Dark;
}
12 changes: 7 additions & 5 deletions packages/gitbook/src/app/(site)/(core)/robots.txt/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest } from 'next/server';

import { getSpace, getSite } from '@/lib/api';
import { getGitBookContextFromHeaders } from '@/lib/gitbook-context';
import { getAbsoluteHref } from '@/lib/links';
import { getSiteContentPointer } from '@/lib/pointer';
import { isSpaceIndexable } from '@/lib/seo';
Expand All @@ -11,17 +12,18 @@ export const runtime = 'edge';
* Generate a robots.txt for the current space.
*/
export async function GET(req: NextRequest) {
const pointer = await getSiteContentPointer();
const ctx = getGitBookContextFromHeaders(req.headers);
const pointer = getSiteContentPointer(ctx);
const [site, space] = await Promise.all([
getSite(pointer.organizationId, pointer.siteId),
getSpace(pointer.spaceId, pointer.siteShareKey),
getSite(ctx, pointer.organizationId, pointer.siteId),
getSpace(ctx, pointer.spaceId, pointer.siteShareKey),
]);

const lines = [
`User-agent: *`,
'Disallow: /~gitbook/',
...((await isSpaceIndexable({ space, site }))
? [`Allow: /`, `Sitemap: ${await getAbsoluteHref(`/sitemap.xml`, true)}`]
...(isSpaceIndexable(ctx, { space, site })
? [`Allow: /`, `Sitemap: ${getAbsoluteHref(ctx, `/sitemap.xml`, true)}`]
: [`Disallow: /`]),
];
const content = lines.join('\n');
Expand Down
Loading

0 comments on commit ecfdb97

Please sign in to comment.