mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 23:55:20 +00:00
Merge branch 'main' into claire/fix-infinite-reload-search-ai
* main: Fix server actions stability (#2728) temporarily add back cloudflare KV cache backend (#2726) Fix padding of ads box (#2724) Try to fix error on og image generation (#2722)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Try to fix error on og image generation
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix server actions stability leading to no results found sometimes on search
|
||||
@@ -41,6 +41,7 @@ jobs:
|
||||
run: bun run build:cloudflare
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
||||
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
|
||||
@@ -10,6 +10,7 @@ module.exports = withSentryConfig(
|
||||
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
||||
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
|
||||
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
|
||||
},
|
||||
|
||||
webpack(config, { dev, webpack }) {
|
||||
|
||||
@@ -5,15 +5,31 @@ import { NextRequest } from 'next/server';
|
||||
import colorContrast from 'postcss-color-contrast/js';
|
||||
import React from 'react';
|
||||
|
||||
import { googleFontsMap } from '@/fonts';
|
||||
import { getGitBookContextFromHeaders } from '@/lib/gitbook-context';
|
||||
import { getAbsoluteHref } from '@/lib/links';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
import { getContentTitle } from '@/lib/utils';
|
||||
|
||||
import { PageIdParams, fetchPageData } from '../../../../fetch';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
async function loadGoogleFont(font: string, text: string) {
|
||||
const url = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`;
|
||||
const css = await (await fetch(url)).text();
|
||||
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
|
||||
|
||||
if (resource) {
|
||||
const response = await fetch(resource[1]);
|
||||
if (response.status == 200) {
|
||||
return await response.arrayBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('failed to load font data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the OpenGraph image for a space.
|
||||
*/
|
||||
@@ -21,30 +37,60 @@ export async function GET(req: NextRequest, { params }: { params: Promise<PageId
|
||||
const ctx = getGitBookContextFromHeaders(req.headers);
|
||||
const { space, page, customization, site } = await fetchPageData(ctx, await params);
|
||||
|
||||
// If user configured a custom social preview, we redirect to it.
|
||||
if (customization.socialPreview.url) {
|
||||
// If user configured a custom social preview, we redirect to it.
|
||||
redirect(customization.socialPreview.url);
|
||||
}
|
||||
|
||||
// TODO: Support all fonts available in GitBook
|
||||
// Right now this is impossible since next/font/google does not expose the cached font file
|
||||
// Another option would be to use the Satori prop `loadAdditionalAsset` [example](https://github.com/vercel/satori/blob/main/playground/pages/index.tsx),
|
||||
// but this prop isn't (yet) exposed through `ImageResponse`.
|
||||
const interRegular = await fetch(
|
||||
new URL('../../../../../../fonts/Inter/Inter-Regular.ttf', import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
const interBold = await fetch(
|
||||
new URL('../../../../../../fonts/Inter/Inter-Bold.ttf', import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
// Compute all text to load only the necessary fonts
|
||||
const contentTitle = customization.header.logo
|
||||
? ''
|
||||
: getContentTitle(space, customization, site ?? null);
|
||||
const pageTitle = page
|
||||
? page.title.length > 64
|
||||
? page.title.slice(0, 64) + '...'
|
||||
: page.title
|
||||
: 'Not found';
|
||||
const pageDescription =
|
||||
page?.description && page?.title.length <= 64
|
||||
? page.description.length > 164
|
||||
? page.description.slice(0, 164) + '...'
|
||||
: page.description
|
||||
: '';
|
||||
|
||||
const fontFamily = googleFontsMap[customization.styling.font] ?? 'Inter';
|
||||
|
||||
const regularText = pageDescription;
|
||||
const boldText = `${contentTitle}${pageTitle}`;
|
||||
|
||||
const fonts = (
|
||||
await Promise.all([
|
||||
regularText
|
||||
? loadGoogleFont(`${fontFamily}:wght@400`, regularText).then((data) => ({
|
||||
name: fontFamily,
|
||||
data,
|
||||
style: 'normal' as const,
|
||||
weight: 400 as const,
|
||||
}))
|
||||
: null,
|
||||
boldText
|
||||
? loadGoogleFont(`${fontFamily}:wght@700`, `${contentTitle}${pageTitle}`).then(
|
||||
(data) => ({
|
||||
name: fontFamily,
|
||||
data,
|
||||
style: 'normal' as const,
|
||||
weight: 700 as const,
|
||||
}),
|
||||
)
|
||||
: null,
|
||||
])
|
||||
).filter(filterOutNullable);
|
||||
|
||||
const theme = customization.themes.default;
|
||||
const useLightTheme = theme === 'light';
|
||||
|
||||
// We have no access to CSS variables, so we'll have to hardcode some values
|
||||
const baseColors = {
|
||||
light: '#ffffff',
|
||||
dark: '#111827',
|
||||
};
|
||||
const baseColors = { light: '#ffffff', dark: '#111827' };
|
||||
|
||||
let colors = {
|
||||
background: baseColors[theme],
|
||||
@@ -92,49 +138,17 @@ export async function GET(req: NextRequest, { params }: { params: Promise<PageId
|
||||
break;
|
||||
}
|
||||
|
||||
const favicon = (() => {
|
||||
if ('icon' in customization.favicon)
|
||||
return (
|
||||
<img
|
||||
src={customization.favicon.icon[theme]}
|
||||
width={40}
|
||||
height={40}
|
||||
tw={tcls('mr-4')}
|
||||
alt="Icon"
|
||||
/>
|
||||
);
|
||||
if ('emoji' in customization.favicon)
|
||||
return (
|
||||
<span tw={tcls('text-4xl', 'mr-4')}>
|
||||
{String.fromCodePoint(parseInt('0x' + customization.favicon.emoji))}
|
||||
</span>
|
||||
);
|
||||
const src = getAbsoluteHref(
|
||||
ctx,
|
||||
`~gitbook/icon?size=medium&theme=${customization.themes.default}`,
|
||||
true,
|
||||
);
|
||||
return <img src={src} alt="Icon" width={40} height={40} tw={tcls('mr-4')} />;
|
||||
})();
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
tw={tcls(
|
||||
'justify-between',
|
||||
'p-20',
|
||||
'relative',
|
||||
'w-full',
|
||||
'h-full',
|
||||
'flex',
|
||||
'flex-col',
|
||||
`bg-[${colors.background}]`,
|
||||
`text-[${colors.body}]`,
|
||||
)}
|
||||
tw={`justify-between p-20 relative w-full h-full flex flex-col bg-[${colors.background}] text-[${colors.body}]`}
|
||||
style={{
|
||||
fontFamily,
|
||||
}}
|
||||
>
|
||||
{/* Gradient */}
|
||||
<div
|
||||
tw={tcls('absolute', 'inset-0')}
|
||||
tw="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(ellipse 100% 100% at top right , ${colors.gradient}, ${colors.gradient}00)`,
|
||||
opacity: 0.5,
|
||||
@@ -142,11 +156,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<PageId
|
||||
></div>
|
||||
|
||||
{/* Grid */}
|
||||
<img
|
||||
tw={tcls('absolute', 'inset-0', 'w-[100vw]', 'h-[100vh]')}
|
||||
src={gridAsset}
|
||||
alt="Grid"
|
||||
/>
|
||||
<img tw="absolute inset-0 w-[100vw] h-[100vh]" src={gridAsset} alt="Grid" />
|
||||
|
||||
{/* Logo */}
|
||||
{customization.header.logo ? (
|
||||
@@ -160,39 +170,46 @@ export async function GET(req: NextRequest, { params }: { params: Promise<PageId
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div tw={tcls('flex')}>
|
||||
{favicon}
|
||||
<h3 tw={tcls('text-4xl', 'my-0')}>
|
||||
{getContentTitle(space, customization, site ?? null)}
|
||||
</h3>
|
||||
<div tw="flex">
|
||||
{(() => {
|
||||
if ('icon' in customization.favicon)
|
||||
return (
|
||||
<img
|
||||
src={customization.favicon.icon[theme]}
|
||||
width={40}
|
||||
height={40}
|
||||
tw="mr-4"
|
||||
alt="Icon"
|
||||
/>
|
||||
);
|
||||
if ('emoji' in customization.favicon)
|
||||
return (
|
||||
<span tw="text-4xl mr-4">
|
||||
{String.fromCodePoint(
|
||||
parseInt('0x' + customization.favicon.emoji),
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
const src = getAbsoluteHref(
|
||||
ctx,
|
||||
`~gitbook/icon?size=medium&theme=${customization.themes.default}`,
|
||||
true,
|
||||
);
|
||||
return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />;
|
||||
})()}
|
||||
<h3 tw="text-4xl my-0 font-bold">{contentTitle}</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title and description */}
|
||||
<div tw={tcls('flex', 'flex-col')}>
|
||||
<div tw="flex flex-col">
|
||||
<h1
|
||||
tw={tcls(
|
||||
'text-8xl',
|
||||
'my-0',
|
||||
'tracking-tight',
|
||||
'leading-none',
|
||||
'text-left',
|
||||
`text-[${colors.title}]`,
|
||||
'font-bold',
|
||||
)}
|
||||
tw={`text-8xl my-0 tracking-light leading-none text-left text-[${colors.title}] font-bold`}
|
||||
>
|
||||
{page
|
||||
? page.title.length > 64
|
||||
? page.title.slice(0, 64) + '...'
|
||||
: page.title
|
||||
: 'Not found'}
|
||||
{pageTitle}
|
||||
</h1>
|
||||
{page?.description && page?.title.length <= 64 ? (
|
||||
<h2 tw={tcls('text-4xl', 'mb-0', 'mt-8', 'w-[75%]', 'font-normal')}>
|
||||
{page.description.length > 164
|
||||
? page.description.slice(0, 164) + '...'
|
||||
: page.description}
|
||||
</h2>
|
||||
{pageDescription ? (
|
||||
<h2 tw="text-4xl mb-0 mt-8 w-[75%] font-normal">{pageDescription}</h2>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,20 +217,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<PageId
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: interRegular,
|
||||
weight: 400,
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'Inter',
|
||||
data: interBold,
|
||||
weight: 700,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
fonts,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ function getTopOffset(props: { sectionsHeader: boolean; topHeader: boolean }) {
|
||||
}
|
||||
|
||||
if (props.topHeader) {
|
||||
return 'lg:top-16 lg:max-h-[calc(100vh_-_3rem)]';
|
||||
return 'lg:top-16 lg:max-h-[calc(100vh_-_4rem)]';
|
||||
}
|
||||
|
||||
return 'lg:top-0 lg:max-h-screen';
|
||||
@@ -237,7 +237,7 @@ export async function PageAside(props: {
|
||||
spaceId={space.id}
|
||||
siteAdsStatus={site?.ads && site.ads.status ? site.ads.status : undefined}
|
||||
ignore={process.env.NODE_ENV !== 'production'}
|
||||
style={tcls(site?.ads && site.ads.status === SiteAdsStatus.Live && ['mt-4'])}
|
||||
style={tcls(site?.ads && site.ads.status === SiteAdsStatus.Live && ['mb-4'])}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -203,3 +203,21 @@ export const fonts: { [fontName in CustomizationFont]: { className: string } } =
|
||||
[CustomizationFont.Ubuntu]: ubuntu,
|
||||
[CustomizationFont.ABCFavorit]: abcFavorit,
|
||||
};
|
||||
|
||||
export const googleFontsMap: { [fontName in CustomizationFont]: string } = {
|
||||
[CustomizationFont.Inter]: 'Inter',
|
||||
[CustomizationFont.FiraSans]: 'Fira Sans Extra Condensed',
|
||||
[CustomizationFont.IBMPlexSerif]: 'IBM Plex Serif',
|
||||
[CustomizationFont.Lato]: 'Lato',
|
||||
[CustomizationFont.Merriweather]: 'Merriweather',
|
||||
[CustomizationFont.NotoSans]: 'Noto Sans',
|
||||
[CustomizationFont.OpenSans]: 'Open Sans',
|
||||
[CustomizationFont.Overpass]: 'Overpass',
|
||||
[CustomizationFont.Poppins]: 'Poppins',
|
||||
[CustomizationFont.Raleway]: 'Raleway',
|
||||
[CustomizationFont.Roboto]: 'Roboto',
|
||||
[CustomizationFont.RobotoSlab]: 'Roboto Slab',
|
||||
[CustomizationFont.SourceSansPro]: 'Source Sans 3',
|
||||
[CustomizationFont.Ubuntu]: 'Ubuntu',
|
||||
[CustomizationFont.ABCFavorit]: 'Inter',
|
||||
};
|
||||
|
||||
+3
@@ -1,5 +1,6 @@
|
||||
import { cloudflareCache } from './cloudflare-cache';
|
||||
import { cloudflareDOCache } from './cloudflare-do';
|
||||
import { cloudflareKVCache } from './cloudflare-kv';
|
||||
import { memoryCache } from './memory';
|
||||
|
||||
export const cacheBackends = [
|
||||
@@ -9,6 +10,8 @@ export const cacheBackends = [
|
||||
// Cache local to the datacenter
|
||||
// It can't be purged globally but it's faster
|
||||
cloudflareCache,
|
||||
// Cache global, but with slow replication
|
||||
cloudflareKVCache,
|
||||
// Global cache with slower performances
|
||||
cloudflareDOCache,
|
||||
];
|
||||
|
||||
+2
-1
@@ -19,7 +19,8 @@
|
||||
},
|
||||
// Build the package for Cloudflare Pages
|
||||
"build:cloudflare": {
|
||||
"dependsOn": ["^build", "generate"]
|
||||
"dependsOn": ["^build", "generate"],
|
||||
"env": ["NEXT_SERVER_ACTIONS_ENCRYPTION_KEY"]
|
||||
},
|
||||
// Check the package for type errors
|
||||
"typecheck": {
|
||||
|
||||
Reference in New Issue
Block a user