mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-14 06:35:17 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc49a3d899 | |||
| db880d647f | |||
| 6f888387b4 | |||
| 0dd9a65cae | |||
| f39ad015b4 | |||
| a1438cb678 | |||
| 97a5dd3138 | |||
| 6f2cab2f07 | |||
| be6329a64a | |||
| 967b65300d | |||
| 581ceb2b3f | |||
| 9ad5951afc | |||
| 58c4cb2f9e | |||
| ac3643001c | |||
| dd8c53da9a | |||
| e149e618fb | |||
| 39d2828af9 | |||
| 321e07d9fd | |||
| a36a939aef | |||
| c75151e2dc | |||
| d06bedbbe1 |
@@ -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:
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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 }}"
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(','),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user