diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml
index 01fe92523..63119f340 100644
--- a/.github/composite/deploy-cloudflare/action.yaml
+++ b/.github/composite/deploy-cloudflare/action.yaml
@@ -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:
diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml
index e7ce2a6a7..4926cf07f 100644
--- a/.github/workflows/deploy-preview.yaml
+++ b/.github/workflows/deploy-preview.yaml
@@ -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 }}"
diff --git a/.github/workflows/deploy-production.yaml b/.github/workflows/deploy-production.yaml
index 5b6fc8b09..9d5bb3abb 100644
--- a/.github/workflows/deploy-production.yaml
+++ b/.github/workflows/deploy-production.yaml
@@ -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 }}"
\ No newline at end of file
diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml
index 1a77ebc3a..ed2f290c7 100644
--- a/.github/workflows/deploy-staging.yaml
+++ b/.github/workflows/deploy-staging.yaml
@@ -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 }}"
\ No newline at end of file
diff --git a/packages/gitbook-v2/src/lib/images/signatures.ts b/packages/gitbook-v2/src/lib/images/signatures.ts
index a56b4a7ce..21834a69c 100644
--- a/packages/gitbook-v2/src/lib/images/signatures.ts
+++ b/packages/gitbook-v2/src/lib/images/signatures.ts
@@ -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
diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts
index ae5aa4a36..ef46c17fb 100644
--- a/packages/gitbook-v2/src/middleware.ts
+++ b/packages/gitbook-v2/src/middleware.ts
@@ -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,
diff --git a/packages/gitbook/e2e/customers.spec.ts b/packages/gitbook/e2e/customers.spec.ts
index 7b3b0ce26..73d5df162 100644
--- a/packages/gitbook/e2e/customers.spec.ts
+++ b/packages/gitbook/e2e/customers.spec.ts
@@ -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',
diff --git a/packages/gitbook/next.config.js b/packages/gitbook/next.config.js
index 8aec4f2e7..23c575ec1 100644
--- a/packages/gitbook/next.config.js
+++ b/packages/gitbook/next.config.js
@@ -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) {
diff --git a/packages/gitbook/src/routes/icon.tsx b/packages/gitbook/src/routes/icon.tsx
index 425bb4b84..03a6f3522 100644
--- a/packages/gitbook/src/routes/icon.tsx
+++ b/packages/gitbook/src/routes/icon.tsx
@@ -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(','),
+ },
}
);
}
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index 59a7a66f1..f6f04db6f 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -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}`))}
);
- 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
;
})();
@@ -192,7 +198,11 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
/>
{/* 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();
+
+/**
+ * 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;
+}