Compare commits

...

21 Commits

Author SHA1 Message Date
Samy Pessé fc49a3d899 Remove site 2025-04-08 01:17:27 +02:00
Samy Pessé db880d647f Cleanup 2025-04-08 01:09:48 +02:00
Samy Pessé 6f888387b4 Fix context passed to server actions 2025-04-08 01:06:22 +02:00
Samy Pessé 0dd9a65cae Debug more 2025-04-08 01:00:31 +02:00
Samy Pessé f39ad015b4 Debug image resizing 2025-04-08 00:54:45 +02:00
Samy Pessé a1438cb678 Use commit sha 2025-04-08 00:48:45 +02:00
Samy Pessé 97a5dd3138 Escape it 2025-04-08 00:44:20 +02:00
Samy Pessé 6f2cab2f07 Fix it 2025-04-08 00:39:57 +02:00
Samy Pessé be6329a64a Add tag for icon 2025-04-08 00:38:14 +02:00
Samy Pessé 967b65300d Add cache tag 2025-04-08 00:37:28 +02:00
Samy Pessé 581ceb2b3f Cleanup 2025-04-08 00:36:08 +02:00
Samy Pessé 9ad5951afc Test improvement of workflow 2025-04-08 00:35:02 +02:00
Samy Pessé 58c4cb2f9e Continue 2025-04-08 00:24:12 +02:00
Samy Pessé ac3643001c Fix path 2025-04-08 00:17:11 +02:00
Samy Pessé dd8c53da9a Log more 2025-04-08 00:09:39 +02:00
Samy Pessé e149e618fb Try again 2025-04-08 00:07:39 +02:00
Samy Pessé 39d2828af9 Try binding 2025-04-07 23:58:38 +02:00
Samy Pessé 321e07d9fd Try more 2025-04-07 23:46:02 +02:00
Samy Pessé a36a939aef Try removing grid 2025-04-07 23:38:18 +02:00
Samy Pessé c75151e2dc Merge branch 'main' into v1-fix-ogimage 2025-04-07 23:33:31 +02:00
Samy Pessé d06bedbbe1 Try to fix ogimage on cloudflare 2025-04-07 12:26:20 +02:00
10 changed files with 124 additions and 29 deletions
@@ -19,6 +19,12 @@ inputs:
deploy:
description: 'Deploy as main version for all traffic instead of uploading versions'
required: true
commitTag:
description: 'Commit branch to associate with the deployment'
required: true
commitMessage:
description: 'Commit message to associate with the deployment'
required: true
outputs:
deployment-url:
description: "Deployment URL"
@@ -65,7 +71,7 @@ runs:
workingDirectory: ./
wranglerVersion: '3.112.0'
environment: ${{ inputs.environment }}
command: ${{ fromJSON(inputs.deploy) == true && 'deploy' || 'versions upload' }} --config ./packages/gitbook-v2/wrangler.jsonc
command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc
- name: Outputs
shell: bash
env:
+3
View File
@@ -40,6 +40,7 @@ jobs:
run: bun run turbo gitbook#build:cloudflare
env:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
GITBOOK_RUNTIME: cloudflare
- id: deploy
name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0
@@ -95,6 +96,8 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
opItem: op://gitbook-open/2c-preview
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
commitTag: ${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
commitMessage: ${{ github.sha }}
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
+2
View File
@@ -48,6 +48,8 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
opItem: op://gitbook-open/2c-production
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
commitTag: main
commitMessage: ${{ github.sha }}
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
+2
View File
@@ -48,6 +48,8 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
opItem: op://gitbook-open/2c-staging
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
commitTag: main
commitMessage: ${{ github.sha }}
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
@@ -34,7 +34,9 @@ export async function verifyImageSignature(
const generated = await generator(input);
// biome-ignore lint/suspicious/noConsole: we want to log the signature comparison
console.log(`comparing image signature "${generated}" (expected) === "${signature}" (actual)`);
console.log(
`comparing image signature for "${input.url}" on identifier "${input.imagesContextId}": "${generated}" (expected) === "${signature}" (actual)`
);
return generated === signature;
}
@@ -69,7 +71,8 @@ const generateSignatureV2: SignFn = async (input) => {
.filter(Boolean)
.join(':');
return fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16);
const signature = fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16);
return signature;
};
// Reused buffer for FNV-1a hashing in the v1 algorithm
+18 -18
View File
@@ -167,6 +167,23 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
// (customization override, theme, etc)
let routeType: 'dynamic' | 'static' = 'static';
// We pick only stable data from the siteURL data to prevent re-rendering of
// the root layout when changing pages..
const stableSiteURLData: SiteURLData = {
site: siteURLData.site,
siteSection: siteURLData.siteSection,
siteSpace: siteURLData.siteSpace,
siteBasePath: siteURLData.siteBasePath,
basePath: siteURLData.basePath,
space: siteURLData.space,
organization: siteURLData.organization,
changeRequest: siteURLData.changeRequest,
revision: siteURLData.revision,
shareKey: siteURLData.shareKey,
apiToken: siteURLData.apiToken,
imagesContextId: imagesContextId,
};
const requestHeaders = new Headers(request.headers);
requestHeaders.set(MiddlewareHeaders.RouteType, routeType);
requestHeaders.set(MiddlewareHeaders.URLMode, mode);
@@ -174,7 +191,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
MiddlewareHeaders.SiteURL,
`${siteCanonicalURL.origin}${siteURLData.basePath}`
);
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(siteURLData));
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(stableSiteURLData));
// Preview of customization/theme
const customization = siteRequestURL.searchParams.get('customization');
@@ -204,23 +221,6 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
);
routeType = routeTypeFromPathname ?? routeType;
// We pick only stable data from the siteURL data to prevent re-rendering of
// the root layout when changing pages..
const stableSiteURLData: SiteURLData = {
site: siteURLData.site,
siteSection: siteURLData.siteSection,
siteSpace: siteURLData.siteSpace,
siteBasePath: siteURLData.siteBasePath,
basePath: siteURLData.basePath,
space: siteURLData.space,
organization: siteURLData.organization,
changeRequest: siteURLData.changeRequest,
revision: siteURLData.revision,
shareKey: siteURLData.shareKey,
apiToken: siteURLData.apiToken,
imagesContextId: imagesContextId,
};
const route = [
'sites',
routeType,
-5
View File
@@ -92,11 +92,6 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://book.character.ai',
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
},
{
name: 'docs.tradeonnova.io',
contentBaseURL: 'https://docs.tradeonnova.io',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'azcoiner.gitbook.io',
contentBaseURL: 'https://azcoiner.gitbook.io',
+1
View File
@@ -5,6 +5,7 @@ module.exports = {
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,
GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME,
},
webpack(config) {
+9
View File
@@ -3,6 +3,7 @@ import { ImageResponse } from 'next/og';
import { getEmojiForCode } from '@/lib/emojis';
import { tcls } from '@/lib/tailwind';
import { getCacheTag } from '@gitbook/cache-tags';
import type { GitBookSiteContext } from '@v2/lib/context';
import { getResizedImageURL } from '@v2/lib/images';
@@ -73,6 +74,14 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
{
width: size.width,
height: size.height,
headers: {
'cache-tag': [
getCacheTag({
tag: 'site',
site: context.site.id,
}),
].join(','),
},
}
);
}
+77 -3
View File
@@ -7,7 +7,9 @@ import { type PageParams, fetchPageData } from '@/components/SitePage';
import { getFontSourcesToPreload } from '@/fonts/custom';
import { getAssetURL } from '@/lib/assets';
import { filterOutNullable } from '@/lib/typescript';
import { getCacheTag } from '@gitbook/cache-tags';
import type { GitBookSiteContext } from '@v2/lib/context';
import { getCloudflareContext } from '@v2/lib/data/cloudflare';
import { getResizedImageURL } from '@v2/lib/images';
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
@@ -169,8 +171,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
{String.fromCodePoint(Number.parseInt(`0x${customization.favicon.emoji}`))}
</span>
);
const src = linker.toAbsoluteURL(
linker.toPathInSpace(`~gitbook/icon?size=medium&theme=${customization.themes.default}`)
const src = await readSelfImage(
linker.toAbsoluteURL(
linker.toPathInSpace(
`~gitbook/icon?size=medium&theme=${customization.themes.default}`
)
)
);
return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />;
})();
@@ -192,7 +198,11 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
/>
{/* Grid */}
<img tw="absolute inset-0 w-[100vw] h-[100vh]" src={gridAsset} alt="Grid" />
<img
tw="absolute inset-0 w-[100vw] h-[100vh]"
src={await readStaticImage(gridAsset)}
alt="Grid"
/>
{/* Logo */}
{customization.header.logo ? (
@@ -228,6 +238,18 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
width: 1200,
height: 630,
fonts: fonts.length ? fonts : undefined,
headers: {
'cache-tag': [
getCacheTag({
tag: 'site',
site: baseContext.site.id,
}),
getCacheTag({
tag: 'space',
space: baseContext.space.id,
}),
].join(','),
},
}
);
}
@@ -285,3 +307,55 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
weight,
};
}
/**
* Fetch a resource from the function itself.
* To avoid error with worker to worker requests in the same zone, we use the `WORKER_SELF_REFERENCE` binding.
*/
async function fetchSelf(url: string) {
const cloudflare = getCloudflareContext();
if (cloudflare?.env.WORKER_SELF_REFERENCE) {
return await cloudflare.env.WORKER_SELF_REFERENCE.fetch(url);
}
return await fetch(url);
}
/**
* Read an image from a response as a base64 encoded string.
*/
async function readImage(response: Response) {
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.startsWith('image/')) {
throw new Error(`Invalid content type: ${contentType}`);
}
const arrayBuffer = await response.arrayBuffer();
const base64 = Buffer.from(arrayBuffer).toString('base64');
return `data:${contentType};base64,${base64}`;
}
const staticImagesCache = new Map<string, string>();
/**
* Read a static image and cache it in memory.
*/
async function readStaticImage(url: string) {
const cached = staticImagesCache.get(url);
if (cached) {
return cached;
}
const image = await readSelfImage(url);
staticImagesCache.set(url, image);
return image;
}
/**
* Read an image from GitBook itself.
*/
async function readSelfImage(url: string) {
const response = await fetchSelf(url);
const image = await readImage(response);
return image;
}