Skip to content

Commit 2d01653

Browse files
authored
Fix ogimage on Cloudflare and resizing of ads image (#3107)
1 parent aa3357a commit 2d01653

File tree

10 files changed

+124
-29
lines changed

10 files changed

+124
-29
lines changed

.github/composite/deploy-cloudflare/action.yaml

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ inputs:
1919
deploy:
2020
description: 'Deploy as main version for all traffic instead of uploading versions'
2121
required: true
22+
commitTag:
23+
description: 'Commit branch to associate with the deployment'
24+
required: true
25+
commitMessage:
26+
description: 'Commit message to associate with the deployment'
27+
required: true
2228
outputs:
2329
deployment-url:
2430
description: "Deployment URL"
@@ -65,7 +71,7 @@ runs:
6571
workingDirectory: ./
6672
wranglerVersion: '3.112.0'
6773
environment: ${{ inputs.environment }}
68-
command: ${{ fromJSON(inputs.deploy) == true && 'deploy' || 'versions upload' }} --config ./packages/gitbook-v2/wrangler.jsonc
74+
command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc
6975
- name: Outputs
7076
shell: bash
7177
env:

.github/workflows/deploy-preview.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ jobs:
4040
run: bun run turbo gitbook#build:cloudflare
4141
env:
4242
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
43+
GITBOOK_RUNTIME: cloudflare
4344
- id: deploy
4445
name: Deploy to Cloudflare
4546
uses: cloudflare/[email protected]
@@ -95,6 +96,8 @@ jobs:
9596
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
9697
opItem: op://gitbook-open/2c-preview
9798
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
99+
commitTag: ${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
100+
commitMessage: ${{ github.sha }}
98101
- name: Outputs
99102
run: |
100103
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"

.github/workflows/deploy-production.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ jobs:
4848
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
4949
opItem: op://gitbook-open/2c-production
5050
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
51+
commitTag: main
52+
commitMessage: ${{ github.sha }}
5153
- name: Outputs
5254
run: |
5355
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"

.github/workflows/deploy-staging.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ jobs:
4848
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
4949
opItem: op://gitbook-open/2c-staging
5050
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
51+
commitTag: main
52+
commitMessage: ${{ github.sha }}
5153
- name: Outputs
5254
run: |
5355
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"

packages/gitbook-v2/src/lib/images/signatures.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export async function verifyImageSignature(
3434
const generated = await generator(input);
3535

3636
// biome-ignore lint/suspicious/noConsole: we want to log the signature comparison
37-
console.log(`comparing image signature "${generated}" (expected) === "${signature}" (actual)`);
37+
console.log(
38+
`comparing image signature for "${input.url}" on identifier "${input.imagesContextId}": "${generated}" (expected) === "${signature}" (actual)`
39+
);
3840
return generated === signature;
3941
}
4042

@@ -69,7 +71,8 @@ const generateSignatureV2: SignFn = async (input) => {
6971
.filter(Boolean)
7072
.join(':');
7173

72-
return fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16);
74+
const signature = fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16);
75+
return signature;
7376
};
7477

7578
// Reused buffer for FNV-1a hashing in the v1 algorithm

packages/gitbook-v2/src/middleware.ts

+18-18
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,31 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
167167
// (customization override, theme, etc)
168168
let routeType: 'dynamic' | 'static' = 'static';
169169

170+
// We pick only stable data from the siteURL data to prevent re-rendering of
171+
// the root layout when changing pages..
172+
const stableSiteURLData: SiteURLData = {
173+
site: siteURLData.site,
174+
siteSection: siteURLData.siteSection,
175+
siteSpace: siteURLData.siteSpace,
176+
siteBasePath: siteURLData.siteBasePath,
177+
basePath: siteURLData.basePath,
178+
space: siteURLData.space,
179+
organization: siteURLData.organization,
180+
changeRequest: siteURLData.changeRequest,
181+
revision: siteURLData.revision,
182+
shareKey: siteURLData.shareKey,
183+
apiToken: siteURLData.apiToken,
184+
imagesContextId: imagesContextId,
185+
};
186+
170187
const requestHeaders = new Headers(request.headers);
171188
requestHeaders.set(MiddlewareHeaders.RouteType, routeType);
172189
requestHeaders.set(MiddlewareHeaders.URLMode, mode);
173190
requestHeaders.set(
174191
MiddlewareHeaders.SiteURL,
175192
`${siteCanonicalURL.origin}${siteURLData.basePath}`
176193
);
177-
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(siteURLData));
194+
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(stableSiteURLData));
178195

179196
// Preview of customization/theme
180197
const customization = siteRequestURL.searchParams.get('customization');
@@ -204,23 +221,6 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
204221
);
205222
routeType = routeTypeFromPathname ?? routeType;
206223

207-
// We pick only stable data from the siteURL data to prevent re-rendering of
208-
// the root layout when changing pages..
209-
const stableSiteURLData: SiteURLData = {
210-
site: siteURLData.site,
211-
siteSection: siteURLData.siteSection,
212-
siteSpace: siteURLData.siteSpace,
213-
siteBasePath: siteURLData.siteBasePath,
214-
basePath: siteURLData.basePath,
215-
space: siteURLData.space,
216-
organization: siteURLData.organization,
217-
changeRequest: siteURLData.changeRequest,
218-
revision: siteURLData.revision,
219-
shareKey: siteURLData.shareKey,
220-
apiToken: siteURLData.apiToken,
221-
imagesContextId: imagesContextId,
222-
};
223-
224224
const route = [
225225
'sites',
226226
routeType,

packages/gitbook/e2e/customers.spec.ts

-5
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,6 @@ const testCases: TestsCase[] = [
9292
contentBaseURL: 'https://book.character.ai',
9393
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
9494
},
95-
{
96-
name: 'docs.tradeonnova.io',
97-
contentBaseURL: 'https://docs.tradeonnova.io',
98-
tests: [{ name: 'Home', url: '/' }],
99-
},
10095
{
10196
name: 'azcoiner.gitbook.io',
10297
contentBaseURL: 'https://azcoiner.gitbook.io',

packages/gitbook/next.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module.exports = {
55
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
66
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
77
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
8+
GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME,
89
},
910

1011
webpack(config) {

packages/gitbook/src/routes/icon.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ImageResponse } from 'next/og';
33

44
import { getEmojiForCode } from '@/lib/emojis';
55
import { tcls } from '@/lib/tailwind';
6+
import { getCacheTag } from '@gitbook/cache-tags';
67
import type { GitBookSiteContext } from '@v2/lib/context';
78
import { getResizedImageURL } from '@v2/lib/images';
89

@@ -73,6 +74,14 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
7374
{
7475
width: size.width,
7576
height: size.height,
77+
headers: {
78+
'cache-tag': [
79+
getCacheTag({
80+
tag: 'site',
81+
site: context.site.id,
82+
}),
83+
].join(','),
84+
},
7685
}
7786
);
7887
}

packages/gitbook/src/routes/ogimage.tsx

+77-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { type PageParams, fetchPageData } from '@/components/SitePage';
77
import { getFontSourcesToPreload } from '@/fonts/custom';
88
import { getAssetURL } from '@/lib/assets';
99
import { filterOutNullable } from '@/lib/typescript';
10+
import { getCacheTag } from '@gitbook/cache-tags';
1011
import type { GitBookSiteContext } from '@v2/lib/context';
12+
import { getCloudflareContext } from '@v2/lib/data/cloudflare';
1113
import { getResizedImageURL } from '@v2/lib/images';
1214

1315
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
@@ -169,8 +171,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
169171
{String.fromCodePoint(Number.parseInt(`0x${customization.favicon.emoji}`))}
170172
</span>
171173
);
172-
const src = linker.toAbsoluteURL(
173-
linker.toPathInSpace(`~gitbook/icon?size=medium&theme=${customization.themes.default}`)
174+
const src = await readSelfImage(
175+
linker.toAbsoluteURL(
176+
linker.toPathInSpace(
177+
`~gitbook/icon?size=medium&theme=${customization.themes.default}`
178+
)
179+
)
174180
);
175181
return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />;
176182
})();
@@ -192,7 +198,11 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
192198
/>
193199

194200
{/* Grid */}
195-
<img tw="absolute inset-0 w-[100vw] h-[100vh]" src={gridAsset} alt="Grid" />
201+
<img
202+
tw="absolute inset-0 w-[100vw] h-[100vh]"
203+
src={await readStaticImage(gridAsset)}
204+
alt="Grid"
205+
/>
196206

197207
{/* Logo */}
198208
{customization.header.logo ? (
@@ -228,6 +238,18 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
228238
width: 1200,
229239
height: 630,
230240
fonts: fonts.length ? fonts : undefined,
241+
headers: {
242+
'cache-tag': [
243+
getCacheTag({
244+
tag: 'site',
245+
site: baseContext.site.id,
246+
}),
247+
getCacheTag({
248+
tag: 'space',
249+
space: baseContext.space.id,
250+
}),
251+
].join(','),
252+
},
231253
}
232254
);
233255
}
@@ -285,3 +307,55 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
285307
weight,
286308
};
287309
}
310+
311+
/**
312+
* Fetch a resource from the function itself.
313+
* To avoid error with worker to worker requests in the same zone, we use the `WORKER_SELF_REFERENCE` binding.
314+
*/
315+
async function fetchSelf(url: string) {
316+
const cloudflare = getCloudflareContext();
317+
if (cloudflare?.env.WORKER_SELF_REFERENCE) {
318+
return await cloudflare.env.WORKER_SELF_REFERENCE.fetch(url);
319+
}
320+
321+
return await fetch(url);
322+
}
323+
324+
/**
325+
* Read an image from a response as a base64 encoded string.
326+
*/
327+
async function readImage(response: Response) {
328+
const contentType = response.headers.get('content-type');
329+
if (!contentType || !contentType.startsWith('image/')) {
330+
throw new Error(`Invalid content type: ${contentType}`);
331+
}
332+
333+
const arrayBuffer = await response.arrayBuffer();
334+
const base64 = Buffer.from(arrayBuffer).toString('base64');
335+
return `data:${contentType};base64,${base64}`;
336+
}
337+
338+
const staticImagesCache = new Map<string, string>();
339+
340+
/**
341+
* Read a static image and cache it in memory.
342+
*/
343+
async function readStaticImage(url: string) {
344+
const cached = staticImagesCache.get(url);
345+
if (cached) {
346+
return cached;
347+
}
348+
349+
const image = await readSelfImage(url);
350+
staticImagesCache.set(url, image);
351+
return image;
352+
}
353+
354+
/**
355+
* Read an image from GitBook itself.
356+
*/
357+
async function readSelfImage(url: string) {
358+
const response = await fetchSelf(url);
359+
const image = await readImage(response);
360+
return image;
361+
}

0 commit comments

Comments
 (0)