mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33c30ba7a1 | |||
| 139a8050a5 | |||
| da7b369ffd | |||
| 432da64919 | |||
| 7d0b422200 | |||
| fb90eb0acc | |||
| b647538e41 | |||
| e50797a5c1 | |||
| 5b1e01c786 | |||
| 813b2af06b | |||
| 6f71da88d8 | |||
| fa91eb7eaf | |||
| c756761591 | |||
| 434af90366 | |||
| 0f41e194cb | |||
| bd353489e9 | |||
| 77fd393f15 | |||
| 8701b5e078 | |||
| 61db166f92 | |||
| 0d9b9cea0f | |||
| 7bb37c7f0a | |||
| 5b2bf828e3 | |||
| c85d3f9752 | |||
| 4d7088bbf0 | |||
| dd6ab6044c | |||
| d236bf029c | |||
| d70d566abe | |||
| a25fdedea9 | |||
| 373183a2cd | |||
| e9fa50dfb5 | |||
| 57ca4e0e88 | |||
| bc90adb73a | |||
| 1505ddbd5a | |||
| cd99ed57fb | |||
| 27de1cd01d | |||
| ae78fc5394 | |||
| 40e8e69c6d | |||
| e84a46a558 | |||
| 18a72d4466 | |||
| d7aa5fa003 |
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix missing headers in OpenAPIResponses
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix OpenAPI enum display
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"packages/colors": {
|
||||
"name": "@gitbook/colors",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
@@ -80,7 +80,7 @@
|
||||
"katex": "^0.16.9",
|
||||
"mathjax": "^3.2.2",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"memoizee": "^0.4.15",
|
||||
"memoizee": "^0.4.17",
|
||||
"next": "14.2.25",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^2.2.3",
|
||||
@@ -136,13 +136,14 @@
|
||||
},
|
||||
"packages/gitbook-v2": {
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.3",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "^15.2.3",
|
||||
"p-memoize": "^7.1.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"rison": "^0.1.1",
|
||||
@@ -179,7 +180,7 @@
|
||||
},
|
||||
"packages/openapi-parser": {
|
||||
"name": "@gitbook/openapi-parser",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.2",
|
||||
"dependencies": {
|
||||
"@scalar/openapi-parser": "^0.10.10",
|
||||
"@scalar/openapi-types": "^0.1.9",
|
||||
@@ -233,7 +234,7 @@
|
||||
},
|
||||
"packages/react-openapi": {
|
||||
"name": "@gitbook/react-openapi",
|
||||
"version": "1.1.5",
|
||||
"version": "1.1.8",
|
||||
"dependencies": {
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@scalar/api-client-react": "^1.2.5",
|
||||
@@ -261,7 +262,7 @@
|
||||
},
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@gitbook/api": "0.102.0",
|
||||
"@gitbook/api": "0.106.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
},
|
||||
@@ -622,7 +623,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.102.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-9wPr5kyCHhTTwkaCYEMT4q3JjEl1q9mytgvpeusmeREfG4E0RZHYPEL8bmumO7T9wH/0vHjpfW2LSTUUs60AwQ=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.106.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-2qA/w18JwHe2fwR2A45q1pEdCZArxqUloegfNibu0xngDhea+iKTXSBrN94wD6Lwkh7cqBp9MvtxC+YMz3hx1g=="],
|
||||
|
||||
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
|
||||
|
||||
@@ -2280,7 +2281,7 @@
|
||||
|
||||
"mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
|
||||
|
||||
"mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
|
||||
|
||||
@@ -2392,6 +2393,8 @@
|
||||
|
||||
"p-map": ["p-map@7.0.2", "", {}, "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q=="],
|
||||
|
||||
"p-memoize": ["p-memoize@7.1.1", "", { "dependencies": { "mimic-fn": "^4.0.0", "type-fest": "^3.0.0" } }, "sha512-DZ/bONJILHkQ721hSr/E9wMz5Am/OTJ9P6LhLFo2Tu+jL8044tgc9LwHO8g4PiaYePnlVVRAJcKmgy8J9MVFrA=="],
|
||||
|
||||
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
||||
|
||||
"package-json": ["package-json@6.5.0", "", { "dependencies": { "got": "^9.6.0", "registry-auth-token": "^4.0.0", "registry-url": "^5.0.0", "semver": "^6.2.0" } }, "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ=="],
|
||||
@@ -2802,7 +2805,7 @@
|
||||
|
||||
"type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
|
||||
|
||||
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||
"type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
|
||||
|
||||
"type-is": ["type-is@2.0.0", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw=="],
|
||||
|
||||
@@ -4042,6 +4045,8 @@
|
||||
|
||||
"mdast-util-gfm-task-list-item/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
|
||||
|
||||
"meow/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||
|
||||
"micro/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
|
||||
|
||||
"micro/content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="],
|
||||
@@ -4070,6 +4075,8 @@
|
||||
|
||||
"normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||
|
||||
"onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"p-filter/p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="],
|
||||
|
||||
"package-json/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"@codemirror/state": "6.4.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"@gitbook/api": "0.102.0"
|
||||
"@gitbook/api": "0.106.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -60,7 +60,7 @@ export function getCacheTag(
|
||||
| {
|
||||
tag: 'computed-document';
|
||||
space: string;
|
||||
integration: string;
|
||||
sourceType: string;
|
||||
}
|
||||
/**
|
||||
* All data related to the URL of a content
|
||||
@@ -84,6 +84,14 @@ export function getCacheTag(
|
||||
organization: string;
|
||||
openAPISpec: string;
|
||||
}
|
||||
/**
|
||||
* All data related to a translation
|
||||
*/
|
||||
| {
|
||||
tag: 'translation';
|
||||
organization: string;
|
||||
translationSettings: string;
|
||||
}
|
||||
): string {
|
||||
switch (spec.tag) {
|
||||
case 'user':
|
||||
@@ -99,13 +107,15 @@ export function getCacheTag(
|
||||
case 'document':
|
||||
return `space:${spec.space}:document:${spec.document}`;
|
||||
case 'computed-document':
|
||||
return `space:${spec.space}:computed-document:${spec.integration}`;
|
||||
return `space:${spec.space}:computed-document:${spec.sourceType}`;
|
||||
case 'site':
|
||||
return `site:${spec.site}`;
|
||||
case 'integration':
|
||||
return `integration:${spec.integration}`;
|
||||
case 'openapi':
|
||||
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
|
||||
case 'translation':
|
||||
return `organization:${spec.organization}:translation:${spec.translationSettings}`;
|
||||
default:
|
||||
assertNever(spec);
|
||||
}
|
||||
@@ -157,6 +167,15 @@ export function getComputedContentSourceCacheTags(
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'translation-language':
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'translation',
|
||||
organization: inContext.organizationId,
|
||||
translationSettings: dependency.ref.translationSettings,
|
||||
})
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Do not throw for unknown dependency types
|
||||
// as it might mean we are lacking behind the API version
|
||||
@@ -169,18 +188,22 @@ export function getComputedContentSourceCacheTags(
|
||||
getCacheTag({
|
||||
tag: 'computed-document',
|
||||
space: inContext.spaceId,
|
||||
integration: source.integration,
|
||||
sourceType: source.type,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// We invalidate the computed content when a new version of the integration is deployed.
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
integration: source.integration,
|
||||
})
|
||||
);
|
||||
|
||||
if (source.type.startsWith('integration:')) {
|
||||
const integration = source.type.split(':')[1];
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
integration,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/colors
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fb90eb0: Reduce chroma of first color scale step
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
|
||||
@@ -220,7 +220,7 @@ export function colorScale(
|
||||
continue;
|
||||
}
|
||||
|
||||
const chromaRatio = index < 8 ? (index + 1) * 0.05 : 1;
|
||||
const chromaRatio = index < 8 ? index * 0.05 : 1;
|
||||
|
||||
const shade = {
|
||||
L: targetL, // Blend lightness
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# gitbook-v2
|
||||
|
||||
## 0.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5b2bf82: Use stable site URL data for route rewrite in the middleware
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"next": "^15.2.3",
|
||||
@@ -12,7 +12,8 @@
|
||||
"server-only": "^0.0.1",
|
||||
"warn-once": "^0.1.1",
|
||||
"rison": "^0.1.1",
|
||||
"jwt-decode": "^4.0.0"
|
||||
"jwt-decode": "^4.0.0",
|
||||
"p-memoize": "^7.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gitbook": "*",
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ export default async function SiteDynamicLayout({
|
||||
const forcedTheme = await getThemeFromMiddleware();
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout customization={context.customization}>
|
||||
<CustomizationRootLayout forcedTheme={forcedTheme} customization={context.customization}>
|
||||
<SiteLayout
|
||||
context={context}
|
||||
forcedTheme={forcedTheme}
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { serveLLMsTxt } from '@/routes/llms';
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
) {
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
return serveLLMsTxt(context);
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { serveRobotsTxt } from '@/routes/robots';
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
) {
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
return serveRobotsTxt(context);
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { servePagesSitemap } from '@/routes/sitemap';
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
) {
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
return servePagesSitemap(context);
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { serveRootSitemap } from '@/routes/sitemap';
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
) {
|
||||
const { context } = await getDynamicSiteContext(await params);
|
||||
return serveRootSitemap(context);
|
||||
}
|
||||
+1
-1
@@ -10,5 +10,5 @@ export async function GET(
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
) {
|
||||
const { context } = await getStaticSiteContext(await params);
|
||||
return serveLLMsTxt(context);
|
||||
return serveLLMsTxt(context, { withMarkdownPages: true });
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { servePageMarkdown } from '@/routes/markdownPage';
|
||||
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: { params: Promise<RouteParams> }) {
|
||||
const { context } = await getStaticSiteContext(await params);
|
||||
const pathname = getPagePathFromParams(await params);
|
||||
return servePageMarkdown(context, pathname);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
|
||||
import type { PublishedSiteContent, SiteAPIToken } from '@gitbook/api';
|
||||
import { fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
|
||||
import type { SiteAPIToken } from '@gitbook/api';
|
||||
import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { forbidden } from 'next/navigation';
|
||||
import rison from 'rison';
|
||||
@@ -98,7 +98,7 @@ function getModeFromParams(mode: string): RouteParamMode {
|
||||
/**
|
||||
* Get the decoded site data from the params.
|
||||
*/
|
||||
function getSiteURLDataFromParams(params: RouteLayoutParams): PublishedSiteContent {
|
||||
function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData {
|
||||
const decoded = decodeURIComponent(params.siteData);
|
||||
return rison.decode(decoded);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,42 @@ import type {
|
||||
SiteStructure,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { type GitBookDataFetcher, createDataFetcher, throwIfDataError } from '@v2/lib/data';
|
||||
import {
|
||||
type GitBookDataFetcher,
|
||||
createDataFetcher,
|
||||
getDataOrNull,
|
||||
throwIfDataError,
|
||||
} from '@v2/lib/data';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { assert } from 'ts-essentials';
|
||||
import { GITBOOK_URL } from './env';
|
||||
import { type ImageResizer, createImageResizer } from './images';
|
||||
import { type GitBookLinker, createLinker } from './links';
|
||||
|
||||
/**
|
||||
* Data about the site URL. Provided by the middleware.
|
||||
* These data are stable between pages in the same site space.
|
||||
*/
|
||||
export type SiteURLData = Pick<
|
||||
PublishedSiteContent,
|
||||
| 'organization'
|
||||
| 'apiToken'
|
||||
| 'site'
|
||||
| 'siteSpace'
|
||||
| 'space'
|
||||
| 'revision'
|
||||
| 'changeRequest'
|
||||
| 'shareKey'
|
||||
| 'siteSection'
|
||||
| 'siteBasePath'
|
||||
| 'basePath'
|
||||
> & {
|
||||
/**
|
||||
* Identifier used for image resizing.
|
||||
*/
|
||||
imagesContextId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic context when rendering content.
|
||||
*/
|
||||
@@ -105,7 +135,7 @@ export type GitBookPageContext = (GitBookSpaceContext | GitBookSiteContext) & {
|
||||
*/
|
||||
export function getBaseContext(input: {
|
||||
siteURL: URL | string;
|
||||
siteURLData: PublishedSiteContent;
|
||||
siteURLData: SiteURLData;
|
||||
urlMode: 'url' | 'url-host';
|
||||
}) {
|
||||
const { urlMode, siteURLData } = input;
|
||||
@@ -139,7 +169,7 @@ export function getBaseContext(input: {
|
||||
}
|
||||
|
||||
const imageResizer = createImageResizer({
|
||||
host: siteURL.host,
|
||||
imagesContextId: siteURLData.imagesContextId,
|
||||
// To ensure image resizing work for proxied sites,
|
||||
// we serve images from the root of the site.
|
||||
linker: linker,
|
||||
@@ -157,7 +187,7 @@ export function getBaseContext(input: {
|
||||
*/
|
||||
export async function fetchSiteContextByURLLookup(
|
||||
baseContext: GitBookBaseContext,
|
||||
data: PublishedSiteContent
|
||||
data: SiteURLData
|
||||
): Promise<GitBookSiteContext> {
|
||||
return await fetchSiteContextByIds(baseContext, {
|
||||
organization: data.organization,
|
||||
@@ -277,7 +307,7 @@ export async function fetchSpaceContextByIds(
|
||||
})
|
||||
),
|
||||
ids.changeRequest
|
||||
? throwIfDataError(
|
||||
? getDataOrNull(
|
||||
dataFetcher.getChangeRequest({
|
||||
spaceId: ids.space,
|
||||
changeRequestId: ids.changeRequest,
|
||||
@@ -286,17 +316,30 @@ export async function fetchSpaceContextByIds(
|
||||
: null,
|
||||
]);
|
||||
|
||||
const revisionId = changeRequest?.revision ?? ids.revision ?? space.revision;
|
||||
if (ids.changeRequest && !changeRequest) {
|
||||
// When trying to render a change request with an invalid / non-existing ID,
|
||||
// we should return a 404.
|
||||
notFound();
|
||||
}
|
||||
|
||||
const pages = await throwIfDataError(
|
||||
const revisionId = ids.revision ?? changeRequest?.revision ?? space.revision;
|
||||
|
||||
const pages = await getDataOrNull(
|
||||
dataFetcher.getRevisionPages({
|
||||
spaceId: ids.space,
|
||||
revisionId,
|
||||
// We only care about the Git metadata when the Git sync is enabled,
|
||||
// otherwise we can optimize performance by not fetching it
|
||||
metadata: !!space.gitSync,
|
||||
})
|
||||
}),
|
||||
|
||||
// When trying to render a revision with an invalid / non-existing ID,
|
||||
// we should handle gracefully the 404 and throw notFound.
|
||||
ids.revision ? [404] : undefined
|
||||
);
|
||||
if (!pages) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
...baseContext,
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
|
||||
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
|
||||
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
|
||||
import { wrapDataFetcherError } from './errors';
|
||||
import { DataFetcherError, wrapDataFetcherError } from './errors';
|
||||
import { memoize } from './memoize';
|
||||
import type { GitBookDataFetcher } from './types';
|
||||
|
||||
interface DataFetcherInput {
|
||||
@@ -94,6 +95,15 @@ export function createDataFetcher(
|
||||
})
|
||||
);
|
||||
},
|
||||
getRevisionPageMarkdown(params) {
|
||||
return trace('getRevisionPageMarkdown', () =>
|
||||
getRevisionPageMarkdown(input, {
|
||||
spaceId: params.spaceId,
|
||||
revisionId: params.revisionId,
|
||||
pageId: params.pageId,
|
||||
})
|
||||
);
|
||||
},
|
||||
getReusableContent(params) {
|
||||
return trace('getReusableContent', () =>
|
||||
getReusableContent(input, {
|
||||
@@ -165,29 +175,31 @@ export function createDataFetcher(
|
||||
})
|
||||
);
|
||||
},
|
||||
//
|
||||
// API that are not tied to the token
|
||||
// where the data is the same for all users
|
||||
//
|
||||
|
||||
getUserById(userId) {
|
||||
return trace('getUserById', () => getUserById({ apiToken: null }, { userId }));
|
||||
return trace('getUserById', () => getUserById(input, { userId }));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getUserById(input: DataFetcherInput, params: { userId: string }) {
|
||||
const getUserById = memoize(async function getUserById(
|
||||
input: DataFetcherInput,
|
||||
params: { userId: string }
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('days');
|
||||
return trace('getUserById.uncached', () => {
|
||||
cacheLife('days');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.users.getUserById(params.userId);
|
||||
return res.data;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.users.getUserById(params.userId);
|
||||
return res.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getSpace(
|
||||
const getSpace = memoize(async function getSpace(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
@@ -196,24 +208,26 @@ async function getSpace(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('days');
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
space: params.spaceId,
|
||||
})
|
||||
);
|
||||
return trace('getSpace.uncached', () => {
|
||||
cacheLife('days');
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
space: params.spaceId,
|
||||
})
|
||||
);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getSpaceById(params.spaceId, {
|
||||
shareKey: params.shareKey,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getSpaceById(params.spaceId, {
|
||||
shareKey: params.shareKey,
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getChangeRequest(
|
||||
const getChangeRequest = memoize(async function getChangeRequest(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
@@ -222,23 +236,28 @@ async function getChangeRequest(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('minutes');
|
||||
return trace('getChangeRequest.uncached', () => {
|
||||
cacheLife('minutes');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getChangeRequestById(params.spaceId, params.changeRequestId);
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'change-request',
|
||||
space: params.spaceId,
|
||||
changeRequest: res.data.id,
|
||||
})
|
||||
);
|
||||
return res.data;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getChangeRequestById(
|
||||
params.spaceId,
|
||||
params.changeRequestId
|
||||
);
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'change-request',
|
||||
space: params.spaceId,
|
||||
changeRequest: res.data.id,
|
||||
})
|
||||
);
|
||||
return res.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getRevision(
|
||||
const getRevision = memoize(async function getRevision(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
@@ -248,18 +267,20 @@ async function getRevision(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('max');
|
||||
return trace('getRevision.uncached', () => {
|
||||
cacheLife('max');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getRevisionById(params.spaceId, params.revisionId, {
|
||||
metadata: params.metadata,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getRevisionById(params.spaceId, params.revisionId, {
|
||||
metadata: params.metadata,
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getRevisionPages(
|
||||
const getRevisionPages = memoize(async function getRevisionPages(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
@@ -269,18 +290,24 @@ async function getRevisionPages(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('max');
|
||||
return trace('getRevisionPages.uncached', () => {
|
||||
cacheLife('max');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.listPagesInRevisionById(params.spaceId, params.revisionId, {
|
||||
metadata: params.metadata,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.listPagesInRevisionById(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
{
|
||||
metadata: params.metadata,
|
||||
}
|
||||
);
|
||||
return res.data.pages;
|
||||
});
|
||||
return res.data.pages;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getRevisionFile(
|
||||
const getRevisionFile = memoize(async function getRevisionFile(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
@@ -290,21 +317,56 @@ async function getRevisionFile(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('max');
|
||||
return trace('getRevisionFile.uncached', () => {
|
||||
cacheLife('max');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getFileInRevisionById(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
params.fileId,
|
||||
{}
|
||||
);
|
||||
return res.data;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getFileInRevisionById(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
params.fileId,
|
||||
{}
|
||||
);
|
||||
return res.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getRevisionPageByPath(
|
||||
const getRevisionPageMarkdown = memoize(async function getRevisionPageMarkdown(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
pageId: string;
|
||||
}
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
return trace('getRevisionPageMarkdown.uncached', () => {
|
||||
cacheLife('max');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getPageInRevisionById(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
params.pageId,
|
||||
{
|
||||
format: 'markdown',
|
||||
}
|
||||
);
|
||||
|
||||
if (!('markdown' in res.data)) {
|
||||
throw new DataFetcherError('Page is not a document', 404);
|
||||
}
|
||||
|
||||
return res.data.markdown;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const getRevisionPageByPath = memoize(async function getRevisionPageByPath(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
@@ -314,23 +376,25 @@ async function getRevisionPageByPath(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('max');
|
||||
return trace('getRevisionPageByPath.uncached', () => {
|
||||
cacheLife('max');
|
||||
|
||||
const encodedPath = encodeURIComponent(params.path);
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getPageInRevisionByPath(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
encodedPath,
|
||||
{}
|
||||
);
|
||||
const encodedPath = encodeURIComponent(params.path);
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getPageInRevisionByPath(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
encodedPath,
|
||||
{}
|
||||
);
|
||||
|
||||
return res.data;
|
||||
return res.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getDocument(
|
||||
const getDocument = memoize(async function getDocument(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
@@ -339,16 +403,18 @@ async function getDocument(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('max');
|
||||
return trace('getDocument.uncached', () => {
|
||||
cacheLife('max');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getDocumentById(params.spaceId, params.documentId, {});
|
||||
return res.data;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getDocumentById(params.spaceId, params.documentId, {});
|
||||
return res.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getComputedDocument(
|
||||
const getComputedDocument = memoize(async function getComputedDocument(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
@@ -359,29 +425,31 @@ async function getComputedDocument(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('days');
|
||||
return trace('getComputedDocument.uncached', () => {
|
||||
cacheLife('days');
|
||||
|
||||
cacheTag(
|
||||
...getComputedContentSourceCacheTags(
|
||||
{
|
||||
spaceId: params.spaceId,
|
||||
organizationId: params.organizationId,
|
||||
},
|
||||
params.source
|
||||
)
|
||||
);
|
||||
cacheTag(
|
||||
...getComputedContentSourceCacheTags(
|
||||
{
|
||||
spaceId: params.spaceId,
|
||||
organizationId: params.organizationId,
|
||||
},
|
||||
params.source
|
||||
)
|
||||
);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getComputedDocument(params.spaceId, {
|
||||
source: params.source,
|
||||
seed: params.seed,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getComputedDocument(params.spaceId, {
|
||||
source: params.source,
|
||||
seed: params.seed,
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getReusableContent(
|
||||
const getReusableContent = memoize(async function getReusableContent(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
spaceId: string;
|
||||
@@ -391,48 +459,54 @@ async function getReusableContent(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('max');
|
||||
return trace('getReusableContent.uncached', () => {
|
||||
cacheLife('max');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getReusableContentInRevisionById(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
params.reusableContentId
|
||||
);
|
||||
return res.data;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getReusableContentInRevisionById(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
params.reusableContentId
|
||||
);
|
||||
return res.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getLatestOpenAPISpecVersionContent(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
organizationId: string;
|
||||
slug: string;
|
||||
const getLatestOpenAPISpecVersionContent = memoize(
|
||||
async function getLatestOpenAPISpecVersionContent(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
organizationId: string;
|
||||
slug: string;
|
||||
}
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
return trace('getLatestOpenAPISpecVersionContent.uncached', () => {
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'openapi',
|
||||
organization: params.organizationId,
|
||||
openAPISpec: params.slug,
|
||||
})
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.orgs.getLatestOpenApiSpecVersionContent(
|
||||
params.organizationId,
|
||||
params.slug
|
||||
);
|
||||
return res.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
) {
|
||||
'use cache';
|
||||
);
|
||||
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'openapi',
|
||||
organization: params.organizationId,
|
||||
openAPISpec: params.slug,
|
||||
})
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.orgs.getLatestOpenApiSpecVersionContent(
|
||||
params.organizationId,
|
||||
params.slug
|
||||
);
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
|
||||
async function getPublishedContentSite(
|
||||
const getPublishedContentSite = memoize(async function getPublishedContentSite(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
organizationId: string;
|
||||
@@ -442,24 +516,32 @@ async function getPublishedContentSite(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('days');
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: params.siteId,
|
||||
})
|
||||
);
|
||||
return trace('getPublishedContentSite.uncached', () => {
|
||||
cacheLife('days');
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: params.siteId,
|
||||
})
|
||||
);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.orgs.getPublishedContentSite(params.organizationId, params.siteId, {
|
||||
shareKey: params.siteShareKey,
|
||||
return trace('getPublishedContentSite', () => {
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.orgs.getPublishedContentSite(
|
||||
params.organizationId,
|
||||
params.siteId,
|
||||
{
|
||||
shareKey: params.siteShareKey,
|
||||
}
|
||||
);
|
||||
return res.data;
|
||||
});
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getSiteRedirectBySource(
|
||||
const getSiteRedirectBySource = memoize(async function getSiteRedirectBySource(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
organizationId: string;
|
||||
@@ -470,26 +552,32 @@ async function getSiteRedirectBySource(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: params.siteId,
|
||||
})
|
||||
);
|
||||
cacheLife('days');
|
||||
return trace('getSiteRedirectBySource.uncached', () => {
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: params.siteId,
|
||||
})
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.orgs.getSiteRedirectBySource(params.organizationId, params.siteId, {
|
||||
shareKey: params.siteShareKey,
|
||||
source: params.source,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.orgs.getSiteRedirectBySource(
|
||||
params.organizationId,
|
||||
params.siteId,
|
||||
{
|
||||
shareKey: params.siteShareKey,
|
||||
source: params.source,
|
||||
}
|
||||
);
|
||||
|
||||
return res.data;
|
||||
});
|
||||
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getEmbedByUrl(
|
||||
const getEmbedByUrl = memoize(async function getEmbedByUrl(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
url: string;
|
||||
@@ -498,36 +586,40 @@ async function getEmbedByUrl(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheLife('weeks');
|
||||
return trace('getEmbedByUrl.uncached', () => {
|
||||
cacheLife('weeks');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url });
|
||||
return res.data;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url });
|
||||
return res.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function searchSiteContent(
|
||||
const searchSiteContent = memoize(async function searchSiteContent(
|
||||
input: DataFetcherInput,
|
||||
params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
const { organizationId, siteId, query, scope } = params;
|
||||
return trace('searchSiteContent.uncached', () => {
|
||||
const { organizationId, siteId, query, scope } = params;
|
||||
|
||||
cacheLife('days');
|
||||
cacheLife('days');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.orgs.searchSiteContent(organizationId, siteId, {
|
||||
query,
|
||||
...scope,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.orgs.searchSiteContent(organizationId, siteId, {
|
||||
query,
|
||||
...scope,
|
||||
});
|
||||
return res.data.items;
|
||||
});
|
||||
return res.data.items;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function renderIntegrationUi(
|
||||
const renderIntegrationUi = memoize(async function renderIntegrationUi(
|
||||
input: DataFetcherInput,
|
||||
params: {
|
||||
integrationName: string;
|
||||
@@ -536,18 +628,20 @@ async function renderIntegrationUi(
|
||||
) {
|
||||
'use cache';
|
||||
|
||||
cacheTag(getCacheTag({ tag: 'integration', integration: params.integrationName }));
|
||||
cacheLife('days');
|
||||
return trace('renderIntegrationUi.uncached', () => {
|
||||
cacheTag(getCacheTag({ tag: 'integration', integration: params.integrationName }));
|
||||
cacheLife('days');
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.integrations.renderIntegrationUiWithPost(
|
||||
params.integrationName,
|
||||
params.request
|
||||
);
|
||||
return res.data;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = await apiClient(input);
|
||||
const res = await api.integrations.renderIntegrationUiWithPost(
|
||||
params.integrationName,
|
||||
params.request
|
||||
);
|
||||
return res.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let loggedServiceBinding = false;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { race, tryCatch } from '@/lib/async';
|
||||
import { joinPath } from '@/lib/paths';
|
||||
import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
|
||||
import { trace } from '@/lib/tracing';
|
||||
import type { PublishedSiteContentLookup } from '@gitbook/api';
|
||||
import { apiClient } from './api';
|
||||
@@ -36,6 +36,13 @@ export async function getPublishedContentByURL(input: {
|
||||
url: alternative.url,
|
||||
visitorAuthToken: input.visitorAuthToken ?? undefined,
|
||||
redirectOnError: input.redirectOnError,
|
||||
|
||||
// As this endpoint is cached by our API, we version the request
|
||||
// to void getting stale data with missing properties.
|
||||
// this could be improved by ensuring our API cache layer is versioned
|
||||
// or invalidated when needed
|
||||
// @ts-expect-error - cacheVersion is not a real query param
|
||||
cacheVersion: 'v2',
|
||||
},
|
||||
{
|
||||
signal,
|
||||
@@ -100,6 +107,7 @@ export async function getPublishedContentByURL(input: {
|
||||
|
||||
const siteResult: PublishedSiteContentLookup = {
|
||||
...data,
|
||||
canonicalUrl: joinPathWithBaseURL(data.canonicalUrl, alternative.extraPath),
|
||||
basePath: joinPath(data.basePath, lookup.basePath ?? ''),
|
||||
pathname: joinPath(data.pathname, alternative.extraPath),
|
||||
...(changeRequest ? { changeRequest } : {}),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it, mock } from 'bun:test';
|
||||
import { memoize } from './memoize';
|
||||
|
||||
describe('memoize', () => {
|
||||
it('should memoize the function', async () => {
|
||||
const fn = mock(async () => Math.random());
|
||||
const memoized = memoize(fn);
|
||||
expect(await memoized()).toBe(await memoized());
|
||||
});
|
||||
|
||||
it('should memoize the function with different arguments', async () => {
|
||||
const fn = mock(async (a: number, b: number) => a + b);
|
||||
const memoized = memoize(fn);
|
||||
expect(await memoized(1, 2)).toBe(await memoized(1, 2));
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
expect(await memoized(1, 2)).not.toBe(await memoized(2, 3));
|
||||
expect(fn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should memoize a function complex object', async () => {
|
||||
const fn = mock(async (a: { foo: string; bar: number }) => a.foo + a.bar);
|
||||
const memoized = memoize(fn);
|
||||
expect(await memoized({ foo: 'foo', bar: 1 })).toBe(await memoized({ foo: 'foo', bar: 1 }));
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
expect(await memoized({ foo: 'foo', bar: 1 })).not.toBe(
|
||||
await memoized({ foo: 'foo', bar: 2 })
|
||||
);
|
||||
expect(fn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should wrap concurrent async calls', async () => {
|
||||
const fn = mock(async () => Math.random());
|
||||
const memoized = memoize(fn);
|
||||
const promise1 = memoized();
|
||||
const promise2 = memoized();
|
||||
expect(await promise1).toBe(await promise2);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import pMemoize from 'p-memoize';
|
||||
|
||||
/**
|
||||
* We wrap 'use cache' calls in a p-memoize function to avoid
|
||||
* executing the function multiple times when doing concurrent calls.
|
||||
*
|
||||
* Hopefully one day this can be done directly by 'use cache'.
|
||||
*/
|
||||
export function memoize<F extends (...args: any[]) => any>(f: F): F {
|
||||
return pMemoize(f, {
|
||||
cacheKey: (args) => {
|
||||
return JSON.stringify(deepSortValue(args));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function deepSortValue(value: unknown): unknown {
|
||||
if (
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
value === null ||
|
||||
value === undefined
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(deepSortValue);
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.entries(value)
|
||||
.map(([key, subValue]) => {
|
||||
return [key, deepSortValue(subValue)] as const;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a[0].localeCompare(b[0]);
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -97,6 +97,15 @@ export interface GitBookDataFetcher {
|
||||
path: string;
|
||||
}): Promise<DataFetcherResponse<api.RevisionPageDocument | api.RevisionPageGroup>>;
|
||||
|
||||
/**
|
||||
* Get the markdown content of a page by its path.
|
||||
*/
|
||||
getRevisionPageMarkdown(params: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
pageId: string;
|
||||
}): Promise<DataFetcherResponse<string>>;
|
||||
|
||||
/**
|
||||
* Get a document by its space ID and document ID.
|
||||
*/
|
||||
@@ -170,4 +179,9 @@ export interface GitBookDataFetcher {
|
||||
integrationName: string;
|
||||
request: api.RenderIntegrationUI;
|
||||
}): Promise<DataFetcherResponse<api.ContentKitRenderOutput>>;
|
||||
|
||||
getAction(params: {
|
||||
url: string
|
||||
claims: any
|
||||
}): Promise<DataFetcherResponse<{ text: string; url: string; icon?: string }>>;
|
||||
}
|
||||
|
||||
@@ -33,13 +33,13 @@ export interface CloudflareImageOptions {
|
||||
* Create an image resizer for a rendering context.
|
||||
*/
|
||||
export function createImageResizer({
|
||||
host,
|
||||
imagesContextId,
|
||||
linker,
|
||||
}: {
|
||||
/** The linker to use to create URLs. */
|
||||
linker: GitBookLinker;
|
||||
/** The host name of the current site. */
|
||||
host: string;
|
||||
/** The site identifier to use for verifying the image signature. */
|
||||
imagesContextId: string;
|
||||
}): ImageResizer {
|
||||
if (!GITBOOK_IMAGE_RESIZE_URL || !GITBOOK_IMAGE_RESIZE_SIGNING_KEY) {
|
||||
return createNoopImageResizer();
|
||||
@@ -58,7 +58,7 @@ export function createImageResizer({
|
||||
|
||||
return async (options) => {
|
||||
cachedSignature ??= await generateImageSignature({
|
||||
host,
|
||||
imagesContextId,
|
||||
url: urlInput,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Get the site identifier to use for image resizing for an incoming request.
|
||||
* This identifier can be obtained before resolving the request URL.
|
||||
*/
|
||||
export function getImageResizingContextId(url: URL): string {
|
||||
if (url.host === 'proxy.gitbook.site' || url.host === 'proxy.gitbook-staging.site') {
|
||||
// For proxy requests, we extract the site ID from the pathname
|
||||
// e.g. https://proxy.gitbook.site/site/siteId/...
|
||||
const pathname = url.pathname.slice(1).split('/');
|
||||
return pathname.slice(0, 2).join('/');
|
||||
}
|
||||
|
||||
return url.host;
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export * from './types';
|
||||
export * from './createImageResizer';
|
||||
export * from './signatures';
|
||||
export * from './utils';
|
||||
export * from './getImageResizingContextId';
|
||||
|
||||
@@ -18,7 +18,7 @@ export const CURRENT_SIGNATURE_VERSION: SignatureVersion = '2';
|
||||
|
||||
type SignFnInput = {
|
||||
url: string;
|
||||
host: string;
|
||||
imagesContextId: string;
|
||||
};
|
||||
|
||||
type SignFn = (input: SignFnInput) => MaybePromise<string>;
|
||||
@@ -60,7 +60,7 @@ const generateSignatureV2: SignFn = async (input) => {
|
||||
assert(GITBOOK_IMAGE_RESIZE_SIGNING_KEY, 'GITBOOK_IMAGE_RESIZE_SIGNING_KEY is not set');
|
||||
const all = [
|
||||
input.url,
|
||||
input.host, // The hostname is used to avoid serving images from other sites on the same domain
|
||||
input.imagesContextId, // The hostname is used to avoid serving images from other sites on the same domain
|
||||
GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CustomizationThemeMode, type PublishedSiteContent } from '@gitbook/api';
|
||||
import { CustomizationThemeMode } from '@gitbook/api';
|
||||
import { headers } from 'next/headers';
|
||||
import type { SiteURLData } from './context';
|
||||
|
||||
export enum MiddlewareHeaders {
|
||||
/**
|
||||
@@ -57,7 +58,7 @@ export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> {
|
||||
* Get the site URL data from the middleware headers.
|
||||
* This function should only be called in a server action or a dynamic route.
|
||||
*/
|
||||
export async function getSiteURLDataFromMiddleware(): Promise<PublishedSiteContent> {
|
||||
export async function getSiteURLDataFromMiddleware(): Promise<SiteURLData> {
|
||||
const headersList = await headers();
|
||||
const siteURLData = headersList.get(MiddlewareHeaders.SiteURLData);
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
throwIfDataError,
|
||||
} from '@v2/lib/data';
|
||||
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
|
||||
import { getImageResizingContextId } from '@v2/lib/images';
|
||||
import { MiddlewareHeaders } from '@v2/lib/middleware';
|
||||
import type { SiteURLData } from './lib/context';
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
@@ -63,7 +65,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { url: siteURL, mode } = match;
|
||||
const { url: siteRequestURL, mode } = match;
|
||||
const imagesContextId = getImageResizingContextId(siteRequestURL);
|
||||
|
||||
/**
|
||||
* Serve image resizing requests (all requests containing `/~gitbook/image`).
|
||||
@@ -73,9 +76,9 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
* In GitBook v1: image resizing was done at the root of the hostname (docs.company.com/~gitbook/image)
|
||||
* In GitBook v2: image resizing is done at the content level (docs.company.com/section/variant/~gitbook/image)
|
||||
*/
|
||||
if (siteURL.pathname.endsWith('/~gitbook/image')) {
|
||||
if (siteRequestURL.pathname.endsWith('/~gitbook/image')) {
|
||||
return await serveResizedImage(request, {
|
||||
host: siteURL.host,
|
||||
imagesContextId: imagesContextId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,13 +88,13 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
// @ts-ignore - request typing
|
||||
const visitorToken = getVisitorToken({
|
||||
cookies: request.cookies.getAll(),
|
||||
url: siteURL,
|
||||
url: siteRequestURL,
|
||||
});
|
||||
|
||||
const withAPIToken = async (apiToken: string | null) => {
|
||||
const siteURLData = await throwIfDataError(
|
||||
getPublishedContentByURL({
|
||||
url: siteURL.toString(),
|
||||
url: siteRequestURL.toString(),
|
||||
visitorAuthToken: visitorToken?.token ?? null,
|
||||
// When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
|
||||
// redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
|
||||
@@ -136,11 +139,15 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
|
||||
cookies.push(...getResponseCookiesForVisitorAuth(siteURLData.siteBasePath, visitorToken));
|
||||
|
||||
// We use the host/origin from the canonical URL to ensure the links are
|
||||
// correctly generated when the site is proxied. e.g. https://proxy.gitbook.com/site/siteId/...
|
||||
const siteCanonicalURL = new URL(siteURLData.canonicalUrl);
|
||||
|
||||
//
|
||||
// Make sure the URL is clean of any va token after a successful lookup
|
||||
// The token is stored in a cookie that is set on the redirect response
|
||||
//
|
||||
const incomingURL = mode === 'url' ? requestURL : siteURL;
|
||||
const incomingURL = mode === 'url' ? requestURL : siteCanonicalURL;
|
||||
const requestURLWithoutToken = normalizeVisitorAuthURL(incomingURL);
|
||||
if (
|
||||
requestURLWithoutToken !== incomingURL &&
|
||||
@@ -163,16 +170,19 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
const requestHeaders = new Headers(request.headers);
|
||||
requestHeaders.set(MiddlewareHeaders.RouteType, routeType);
|
||||
requestHeaders.set(MiddlewareHeaders.URLMode, mode);
|
||||
requestHeaders.set(MiddlewareHeaders.SiteURL, `${siteURL.origin}${siteURLData.basePath}`);
|
||||
requestHeaders.set(
|
||||
MiddlewareHeaders.SiteURL,
|
||||
`${siteCanonicalURL.origin}${siteURLData.basePath}`
|
||||
);
|
||||
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(siteURLData));
|
||||
|
||||
// Preview of customization/theme
|
||||
const customization = siteURL.searchParams.get('customization');
|
||||
const customization = siteRequestURL.searchParams.get('customization');
|
||||
if (customization && validateSerializedCustomization(customization)) {
|
||||
routeType = 'dynamic';
|
||||
requestHeaders.set(MiddlewareHeaders.Customization, customization);
|
||||
}
|
||||
const theme = siteURL.searchParams.get('theme');
|
||||
const theme = siteRequestURL.searchParams.get('theme');
|
||||
if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
|
||||
routeType = 'dynamic';
|
||||
requestHeaders.set(MiddlewareHeaders.Theme, theme);
|
||||
@@ -188,18 +198,44 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
requestHeaders.set('x-forwarded-host', request.nextUrl.host);
|
||||
requestHeaders.set('origin', request.nextUrl.origin);
|
||||
|
||||
const siteURLWithoutProtocol = `${siteURL.host}${siteURLData.basePath}`;
|
||||
const siteURLWithoutProtocol = `${siteCanonicalURL.host}${siteURLData.basePath}`;
|
||||
const { pathname, routeType: routeTypeFromPathname } = encodePathInSiteContent(
|
||||
siteURLData.pathname
|
||||
);
|
||||
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,
|
||||
mode,
|
||||
encodeURIComponent(siteURLWithoutProtocol),
|
||||
encodeURIComponent(rison.encode(siteURLData)),
|
||||
encodeURIComponent(
|
||||
rison.encode(
|
||||
// rison can't encode undefined values
|
||||
Object.fromEntries(
|
||||
Object.entries(stableSiteURLData).filter(
|
||||
([_, v]) => typeof v !== 'undefined'
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
pathname,
|
||||
].join('/');
|
||||
|
||||
@@ -226,10 +262,10 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
};
|
||||
|
||||
// For https://preview/<siteURL> requests,
|
||||
if (siteURL.hostname === 'preview') {
|
||||
if (siteRequestURL.hostname === 'preview') {
|
||||
return serveWithQueryAPIToken(
|
||||
// We scope the API token to the site ID.
|
||||
`${siteURL.hostname}/${requestURL.pathname.slice(1).split('/')[0]}`,
|
||||
`${siteRequestURL.hostname}/${requestURL.pathname.slice(1).split('/')[0]}`,
|
||||
request,
|
||||
withAPIToken
|
||||
);
|
||||
@@ -382,13 +418,26 @@ function encodePathInSiteContent(rawPathname: string): {
|
||||
return { pathname };
|
||||
}
|
||||
|
||||
// If the pathname is a markdown file, we rewrite it to ~gitbook/markdown/:pathname
|
||||
if (pathname.match(/\.md$/)) {
|
||||
const pagePathWithoutMD = pathname.slice(0, -3);
|
||||
return {
|
||||
pathname: `~gitbook/markdown/${encodeURIComponent(pagePathWithoutMD)}`,
|
||||
// The markdown content is always static and doesn't depend on the dynamic parameter (customization, theme, etc)
|
||||
routeType: 'static',
|
||||
};
|
||||
}
|
||||
|
||||
switch (pathname) {
|
||||
case '~gitbook/icon':
|
||||
return { pathname };
|
||||
case 'llms.txt':
|
||||
case 'sitemap.xml':
|
||||
case 'sitemap-pages.xml':
|
||||
case 'robots.txt':
|
||||
return { pathname };
|
||||
// LLMs.txt, sitemap, sitemap-pages and robots.txt are always static
|
||||
// as they only depend on the site structure / pages.
|
||||
return { pathname, routeType: 'static' };
|
||||
case '~gitbook/pdf':
|
||||
// PDF routes are always dynamic as they depend on the search params.
|
||||
return { pathname, routeType: 'dynamic' };
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
# gitbook
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fb90eb0: Limit tinted background on bold theme to sites with filled sidebar
|
||||
- 7d0b422: Handle grouped OpenAPISchemas
|
||||
- Updated dependencies [7d0b422]
|
||||
- Updated dependencies [fb90eb0]
|
||||
- @gitbook/react-openapi@1.1.8
|
||||
- @gitbook/colors@0.3.1
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 77fd393: Track event when visitor is opening a search result.
|
||||
- d70d566: Support site announcement banner
|
||||
- 77fd393: Track event when clicking announcement banner link.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e84a46a: Fix OpenAPI tabs indicator overflow
|
||||
- bc90adb: Fix favicon not being displayed in Google because `robots.txt` was preventing the indexation of the image route
|
||||
- 434af90: Fix image resizing when using the proxy feature in a site.
|
||||
- c756761: Add breadcrumbs to search results
|
||||
- 40e8e69: Disallow crawling by web-robots of search/ask URLs
|
||||
- 77fd393: Fix clicking search results when the site is embedded in an iframe.
|
||||
- 1505ddb: Fix multiple request examples selector not showing
|
||||
- 61db166: Add OpenAPI write-only indicator
|
||||
- 6f71da8: Fix padding in schemas
|
||||
- fa91eb7: Fix PDF generation when user has dark mode configured.
|
||||
- 5b1e01c: Support for x-stability property
|
||||
- 57ca4e0: Fix a crash when a page contains a block of an integration that is no longer installed
|
||||
- d236bf0: Fix flash when loading sites with dark mode as default theme
|
||||
- cd99ed5: Fix spec properties rendering and missing keys
|
||||
- 813b2af: Support for x-enumDescriptions and x-gitbook-enum
|
||||
- e9fa50d: Trim the search query to avoid showing a loading state when typing
|
||||
- Updated dependencies [bd35348]
|
||||
- Updated dependencies [ae78fc5]
|
||||
- Updated dependencies [7bb37c7]
|
||||
- Updated dependencies [373183a]
|
||||
- Updated dependencies [1505ddb]
|
||||
- Updated dependencies [61db166]
|
||||
- Updated dependencies [5b1e01c]
|
||||
- Updated dependencies [cd99ed5]
|
||||
- Updated dependencies [813b2af]
|
||||
- Updated dependencies [a25fded]
|
||||
- @gitbook/react-openapi@1.1.7
|
||||
- @gitbook/openapi-parser@2.1.2
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
headerLinks,
|
||||
runTestCases,
|
||||
waitForCookiesDialog,
|
||||
waitForNotFound,
|
||||
} from './util';
|
||||
|
||||
const testCases: TestsCase[] = [
|
||||
@@ -308,6 +309,18 @@ const testCases: TestsCase[] = [
|
||||
url: '~/revisions/S55pwsEr5UVoroaOiWnP/blocks/headings',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Invalid revision',
|
||||
url: '~/revisions/idnotfound/blocks/headings',
|
||||
run: waitForNotFound,
|
||||
screenshot: false,
|
||||
},
|
||||
{
|
||||
name: 'Invalid change request',
|
||||
url: '~/changes/idnotfound/blocks/headings',
|
||||
run: waitForNotFound,
|
||||
screenshot: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -390,6 +403,22 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Markdown page',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Text page',
|
||||
url: 'text-page.md',
|
||||
screenshot: false,
|
||||
run: async (_page, response) => {
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(response?.headers()['content-type']).toContain('text/markdown');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Content tests',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type CustomizationThemedColor,
|
||||
type SiteCustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
import { type BrowserContext, type Page, expect, test } from '@playwright/test';
|
||||
import { type BrowserContext, type Page, type Response, expect, test } from '@playwright/test';
|
||||
import deepMerge from 'deepmerge';
|
||||
import rison from 'rison';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
@@ -33,7 +33,7 @@ export interface Test {
|
||||
/**
|
||||
* Test to run
|
||||
*/
|
||||
run?: (page: Page) => Promise<unknown>;
|
||||
run?: (page: Page, response: Response | null) => Promise<unknown>;
|
||||
/**
|
||||
* Whether the test should be fullscreened during testing.
|
||||
*/
|
||||
@@ -138,6 +138,11 @@ export async function waitForCookiesDialog(page: Page) {
|
||||
await expect(dialog).toBeVisible();
|
||||
}
|
||||
|
||||
export async function waitForNotFound(_page: Page, response: Response | null) {
|
||||
expect(response).not.toBeNull();
|
||||
expect(response?.status()).toBe(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform test cases into Playwright tests and run it.
|
||||
*/
|
||||
@@ -183,9 +188,9 @@ export function runTestCases(testCases: TestsCase[]) {
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(url);
|
||||
const response = await page.goto(url);
|
||||
if (testEntry.run) {
|
||||
await testEntry.run(page);
|
||||
await testEntry.run(page, response);
|
||||
}
|
||||
const screenshotOptions = testEntry.screenshot;
|
||||
if (screenshotOptions !== false) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "env-cmd --silent -f ../../.env.local next dev",
|
||||
@@ -45,7 +45,7 @@
|
||||
"katex": "^0.16.9",
|
||||
"mathjax": "^3.2.2",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"memoizee": "^0.4.15",
|
||||
"memoizee": "^0.4.17",
|
||||
"next": "14.2.25",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^2.2.3",
|
||||
|
||||
@@ -25,10 +25,21 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const result = await revalidateTags(json.tags);
|
||||
try {
|
||||
const result = await revalidateTags(json.tags);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
stats: result.stats,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to revalidate tags',
|
||||
message: err instanceof Error ? err.message : 'Internal Server Error',
|
||||
stack: err instanceof Error ? err.stack : '',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
stats: result.stats,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import type { GitBookSiteContext } from '@v2/lib/context';
|
||||
import { AnnouncementBanner } from './AnnouncementBanner';
|
||||
|
||||
/**
|
||||
* Server-side component to resolve content refs and pass down to client-side component
|
||||
*/
|
||||
export async function Announcement(props: {
|
||||
context: GitBookSiteContext;
|
||||
}) {
|
||||
const { context } = props;
|
||||
const { customization } = context;
|
||||
|
||||
if (
|
||||
!customization.announcement ||
|
||||
!customization.announcement.enabled ||
|
||||
!customization.announcement.message
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedContentRef = customization.announcement?.link
|
||||
? await resolveContentRef(customization.announcement?.link?.to, context)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<AnnouncementBanner
|
||||
announcement={customization.announcement}
|
||||
contentRef={resolvedContentRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import * as storage from '@/lib/local-storage';
|
||||
import type { ResolvedContentRef } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { type CustomizationAnnouncement, SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import { CONTAINER_STYLE } from '../layout';
|
||||
import { Link, linkStyles } from '../primitives';
|
||||
import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants';
|
||||
|
||||
/**
|
||||
* Client-side component to enable closing the banner
|
||||
*/
|
||||
export function AnnouncementBanner(props: {
|
||||
announcement: CustomizationAnnouncement;
|
||||
contentRef: ResolvedContentRef | null;
|
||||
}) {
|
||||
const { announcement, contentRef } = props;
|
||||
|
||||
const hasLink = announcement.link && contentRef?.href;
|
||||
const closeable = announcement.style !== 'danger';
|
||||
|
||||
const Tag = hasLink ? Link : 'div';
|
||||
const style = BANNER_STYLES[announcement.style];
|
||||
|
||||
return (
|
||||
<div className="announcement-banner theme-bold:bg-header-background pt-4 pb-2">
|
||||
<div className="scroll-nojump">
|
||||
<div className={tcls('relative', CONTAINER_STYLE)}>
|
||||
<Tag
|
||||
href={contentRef?.href ?? ''}
|
||||
className={tcls(
|
||||
'flex w-full items-start justify-center overflow-hidden rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
|
||||
style.container,
|
||||
closeable && 'pr-12',
|
||||
hasLink && style.hover
|
||||
)}
|
||||
insights={
|
||||
announcement.link
|
||||
? {
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: announcement.link.to,
|
||||
position: SiteInsightsLinkPosition.Announcement,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
icon={style.icon as IconName}
|
||||
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
|
||||
/>
|
||||
<div>
|
||||
{announcement.message}
|
||||
{hasLink ? (
|
||||
<div className={tcls(linkStyles, style.link, 'ml-1 inline')}>
|
||||
{contentRef?.icon ? (
|
||||
<span className="mr-1 ml-2 *:inline">
|
||||
{contentRef?.icon}
|
||||
</span>
|
||||
) : null}
|
||||
{announcement.link?.title && (
|
||||
<span className="mr-1">{announcement.link?.title}</span>
|
||||
)}
|
||||
<Icon
|
||||
icon={
|
||||
announcement.link?.to.kind === 'url'
|
||||
? 'arrow-up-right'
|
||||
: 'chevron-right'
|
||||
}
|
||||
className={tcls('mb-0.5 inline size-3')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Tag>
|
||||
{closeable ? (
|
||||
<button
|
||||
className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
|
||||
type="button"
|
||||
onClick={dismissAnnouncement}
|
||||
>
|
||||
<Icon icon="close" className="size-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the announcement banner and store the dismissal state in local storage.
|
||||
* @see AnnouncementScript
|
||||
*/
|
||||
function dismissAnnouncement() {
|
||||
storage.setItem(ANNOUNCEMENT_STORAGE_KEY, {
|
||||
visible: false,
|
||||
at: Date.now(),
|
||||
});
|
||||
|
||||
document.documentElement.classList.add(ANNOUNCEMENT_CSS_CLASS);
|
||||
}
|
||||
|
||||
const BANNER_STYLES = {
|
||||
info: {
|
||||
container: 'bg-info ring-info-subtle',
|
||||
hover: 'hover:bg-info-hover active:bg-info-active',
|
||||
icon: 'circle-info',
|
||||
iconColor: 'text-info-subtle',
|
||||
close: 'hover:bg-tint-base hover:ring-info-subtle',
|
||||
link: '',
|
||||
},
|
||||
warning: {
|
||||
container: 'bg-warning decoration-warning/6 ring-warning-subtle',
|
||||
hover: 'hover:bg-warning-hover',
|
||||
icon: 'circle-exclamation',
|
||||
iconColor: 'text-warning-subtle',
|
||||
close: 'hover:bg-tint-base hover:ring-warning-subtle',
|
||||
link: 'links-default:text-warning links-default:hover:text-warning-strong links-default:decoration-warning/6 links-accent:decoration-warning',
|
||||
},
|
||||
danger: {
|
||||
container: 'bg-danger decoration-danger/6 ring-danger-subtle',
|
||||
hover: 'hover:bg-danger-hover',
|
||||
icon: 'triangle-exclamation',
|
||||
iconColor: 'text-danger-subtle',
|
||||
close: 'hover:bg-tint-base hover:ring-danger-subtle',
|
||||
link: 'links-default:text-danger links-default:hover:text-danger-strong links-default:decoration-danger/6 links-accent:decoration-danger',
|
||||
},
|
||||
success: {
|
||||
container: 'bg-success decoration-success/6 ring-success-subtle',
|
||||
hover: 'hover:bg-success-hover',
|
||||
icon: 'circle-check',
|
||||
iconColor: 'text-success-subtle',
|
||||
close: 'hover:bg-tint-base hover:ring-success-subtle',
|
||||
link: 'links-default:text-success links-default:hover:text-success-strong links-default:decoration-success/6 links-accent:decoration-success',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ANNOUNCEMENT_CSS_CLASS,
|
||||
ANNOUNCEMENT_DAYS_TILL_RESET,
|
||||
ANNOUNCEMENT_STORAGE_KEY,
|
||||
} from './constants';
|
||||
import { checkStorageForDismissedScript } from './script';
|
||||
|
||||
/**
|
||||
* Inject a script to read the local storage state for the announcement banner and apply the appropriate CSS class to the <html> element as early as possible.
|
||||
* Bypasses react state to prevent flickering.
|
||||
*/
|
||||
export function AnnouncementDismissedScript() {
|
||||
const scriptArgs = JSON.stringify([
|
||||
ANNOUNCEMENT_STORAGE_KEY,
|
||||
ANNOUNCEMENT_DAYS_TILL_RESET,
|
||||
ANNOUNCEMENT_CSS_CLASS,
|
||||
]).slice(1, -1);
|
||||
|
||||
// makes sure the banner dismissed state is kept when navigating between sites
|
||||
React.useEffect(() => {
|
||||
checkStorageForDismissedScript(
|
||||
ANNOUNCEMENT_STORAGE_KEY,
|
||||
ANNOUNCEMENT_DAYS_TILL_RESET,
|
||||
ANNOUNCEMENT_CSS_CLASS
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<script
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(${checkStorageForDismissedScript.toString()})(${scriptArgs})`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* The local storage key for the announcement banner.
|
||||
*/
|
||||
export const ANNOUNCEMENT_STORAGE_KEY = '@gitbook/announcement';
|
||||
/**
|
||||
* The CSS class to hide the announcement banner. Applies to the <html> element.
|
||||
*/
|
||||
export const ANNOUNCEMENT_CSS_CLASS = 'announcement-hidden';
|
||||
/**
|
||||
* The number of days until the announcement banner resets.
|
||||
*/
|
||||
export const ANNOUNCEMENT_DAYS_TILL_RESET = 7;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Announcement';
|
||||
export * from './AnnouncementDismissedScript';
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Read the local storage state for the announcement banner and apply the appropriate CSS class to the <html> element.
|
||||
*
|
||||
* NOTE: this script is stringified and run in the browser, so it must be self-contained and have syntax supported in all browsers.
|
||||
*/
|
||||
export function checkStorageForDismissedScript(
|
||||
storageKey: string,
|
||||
daysTillReset: number,
|
||||
cssClass: string
|
||||
) {
|
||||
let showBanner = true;
|
||||
|
||||
try {
|
||||
const announcementStateStr = window.localStorage.getItem(storageKey);
|
||||
const announcementState = announcementStateStr
|
||||
? JSON.parse(announcementStateStr)
|
||||
: undefined;
|
||||
|
||||
if (announcementState && !announcementState.visible) {
|
||||
const dismissedAt = announcementState.at;
|
||||
const nowTime = new Date().getTime();
|
||||
|
||||
// Check if enough days have passed since dismissal
|
||||
const daysSinceDismissal = Math.floor((nowTime - dismissedAt) / (1000 * 60 * 60 * 24));
|
||||
if (daysSinceDismissal < daysTillReset) {
|
||||
showBanner = false;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (!showBanner) {
|
||||
document.documentElement.classList.add(cssClass);
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
|
||||
const { nodes, blockStyle, isOffscreen: defaultIsOffscreen = false, ...contextProps } = props;
|
||||
|
||||
let isOffscreen = defaultIsOffscreen;
|
||||
return nodes.map((node) => {
|
||||
return nodes.map((node, index) => {
|
||||
isOffscreen =
|
||||
isOffscreen ||
|
||||
isBlockOffscreen({
|
||||
@@ -65,7 +65,7 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
|
||||
|
||||
return (
|
||||
<Block
|
||||
key={node.key}
|
||||
key={node.key || `${node.type}-${index}`}
|
||||
block={node}
|
||||
style={[
|
||||
'mx-auto w-full decoration-primary/6',
|
||||
|
||||
@@ -104,7 +104,7 @@ const HINT_STYLES: {
|
||||
'[&_.can-override-text]:text-neutral-strong',
|
||||
],
|
||||
container:
|
||||
'bg-info border-info theme-muted-tint:bg-info-solid/2 theme-bold-tint:bg-info-solid/2',
|
||||
'bg-info border-info theme-muted-tint:bg-info-solid/2 [html.sidebar-filled.theme-bold.tint_&]:bg-info-solid/2',
|
||||
containerWithHeader: 'border-info-solid bg-info-subtle',
|
||||
},
|
||||
warning: {
|
||||
|
||||
@@ -13,6 +13,7 @@ import assertNever from 'assert-never';
|
||||
import { Annotation } from './Annotation/Annotation';
|
||||
import type { DocumentContextProps } from './DocumentView';
|
||||
import { Emoji } from './Emoji';
|
||||
import { InlineAction } from './InlineAction';
|
||||
import { InlineImage } from './InlineImage';
|
||||
import { InlineLink } from './InlineLink';
|
||||
import { InlineMath } from './Math';
|
||||
@@ -61,6 +62,8 @@ export function Inline<
|
||||
return <Mention {...contextProps} inline={inline} />;
|
||||
case 'inline-image':
|
||||
return <InlineImage {...contextProps} inline={inline} />;
|
||||
case 'action':
|
||||
return <InlineAction {...contextProps} inline={inline} />;
|
||||
default:
|
||||
assertNever(inline);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getVisitorAuthClaims } from '@/lib/adaptive';
|
||||
import { getDataOrNull } from '@v2/lib/data';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { TrackEventButton } from './TrackEventButton';
|
||||
|
||||
export async function InlineAction(props: InlineProps<any>) {
|
||||
const { inline, context } = props;
|
||||
|
||||
if (!context.contentContext) {
|
||||
throw new Error('inline action requires a contentContext');
|
||||
}
|
||||
|
||||
const action = inline.data.action;
|
||||
|
||||
if (action.type === 'url') {
|
||||
return <TrackEventButton action={inline.data.action} resolved={{ url: action.url, text: action.text }} />
|
||||
}
|
||||
|
||||
// API action
|
||||
|
||||
const { dataFetcher } = context.contentContext;
|
||||
|
||||
const siteData = await getSiteURLDataFromMiddleware();
|
||||
const claims = getVisitorAuthClaims(siteData)
|
||||
const resolved = await getDataOrNull(dataFetcher.getAction({ url: inline.data.action.url, claims }));
|
||||
|
||||
if (!resolved) {
|
||||
throw new Error('inline action not found');
|
||||
}
|
||||
|
||||
return <TrackEventButton action={inline.data.action} resolved={resolved} />
|
||||
}
|
||||
@@ -24,14 +24,16 @@ export function Inlines<T extends DocumentInline | DocumentText>(
|
||||
) {
|
||||
const { nodes, document, ancestorInlines, ...contextProps } = props;
|
||||
|
||||
return nodes.map((node) => {
|
||||
return nodes.map((node, index) => {
|
||||
const key = node.key || `key-${index}`;
|
||||
|
||||
if (node.object === 'text') {
|
||||
return <Text key={node.key} text={node} />;
|
||||
return <Text key={key} text={node} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Inline
|
||||
key={node.key}
|
||||
key={key}
|
||||
inline={node}
|
||||
document={document}
|
||||
ancestorInlines={ancestorInlines}
|
||||
|
||||
@@ -34,7 +34,11 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
dataFetcher.renderIntegrationUi({
|
||||
integrationName: block.data.integration,
|
||||
request: initialInput,
|
||||
})
|
||||
}),
|
||||
|
||||
// The API can respond with a 400 error if the integration is not installed
|
||||
// and 404 if the integration is not found.
|
||||
[404, 400]
|
||||
);
|
||||
if (!initialOutput || initialOutput.type === 'complete') {
|
||||
return null;
|
||||
|
||||
@@ -77,7 +77,12 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
|
||||
ancestorBlocks={props.ancestorBlocks}
|
||||
isEstimatedOffscreen={props.isEstimatedOffscreen}
|
||||
context={props.context}
|
||||
style={headingProps.deprecated ? 'line-through' : undefined}
|
||||
style={tcls([
|
||||
headingProps.deprecated ? 'line-through' : undefined,
|
||||
headingProps.deprecated || !!headingProps.stability
|
||||
? '[&>div]:mt-0'
|
||||
: undefined,
|
||||
])}
|
||||
block={{
|
||||
object: 'block',
|
||||
key: `${block.key}-heading`,
|
||||
|
||||
@@ -50,6 +50,7 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
|
||||
return (
|
||||
<BaseOpenAPISchemas
|
||||
data={data}
|
||||
grouped={block.data.grouped}
|
||||
context={{
|
||||
specUrl,
|
||||
icons: {
|
||||
|
||||
@@ -20,10 +20,31 @@
|
||||
@apply flex flex-col items-start justify-start gap-3;
|
||||
}
|
||||
|
||||
.openapi-deprecated {
|
||||
.openapi-summary-tags {
|
||||
@apply flex flex-row gap-2 mt-[0.75em];
|
||||
}
|
||||
|
||||
.openapi-deprecated,
|
||||
.openapi-stability {
|
||||
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
|
||||
}
|
||||
|
||||
.openapi-stability-stable {
|
||||
@apply text-green-600 dark:text-green-300 bg-green-50 dark:bg-green-900/6 ring-green-500/5;
|
||||
}
|
||||
|
||||
.openapi-stability-alpha {
|
||||
@apply text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-900/6 ring-amber-500/5;
|
||||
}
|
||||
|
||||
.openapi-stability-beta {
|
||||
@apply text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/6 ring-blue-500/5;
|
||||
}
|
||||
|
||||
.openapi-stability-experimental {
|
||||
@apply text-violet-700 dark:text-violet-300 bg-violet-50 dark:bg-violet-900/6 ring-violet-500/5;
|
||||
}
|
||||
|
||||
.openapi-deprecated-sunset-date {
|
||||
@apply font-semibold font-mono truncate;
|
||||
}
|
||||
@@ -131,7 +152,7 @@
|
||||
|
||||
/* Schema Structure */
|
||||
.openapi-schema-root {
|
||||
@apply mt-2;
|
||||
/* unstyled */
|
||||
}
|
||||
|
||||
.openapi-schema-properties {
|
||||
@@ -209,6 +230,10 @@
|
||||
@apply text-primary-subtle/9 text-[0.813rem];
|
||||
}
|
||||
|
||||
.openapi-schema-writeonly {
|
||||
@apply text-success dark:text-success-subtle/9 text-[0.813rem];
|
||||
}
|
||||
|
||||
.openapi-schema-type {
|
||||
@apply text-tint select-text text-[0.813rem] font-mono [word-spacing:-0.25rem];
|
||||
}
|
||||
@@ -240,11 +265,11 @@
|
||||
|
||||
/* Schema Enum */
|
||||
.openapi-schema-enum {
|
||||
@apply flex flex-row text-sm leading-relaxed flex-wrap text-tint;
|
||||
@apply text-sm leading-relaxed max-w-full text-tint;
|
||||
}
|
||||
|
||||
.openapi-schema-enum-value {
|
||||
@apply text-sm;
|
||||
@apply text-sm mr-1.5;
|
||||
}
|
||||
|
||||
.openapi-schema-enum-value:first-child {
|
||||
@@ -467,13 +492,9 @@
|
||||
|
||||
/* Common Elements */
|
||||
.openapi-select {
|
||||
@apply max-w-60 rounded font-mono text-xs leading-6 px-1 py-0.5 border border-tint-subtle bg-tint;
|
||||
@apply max-w-60 rounded font-mono text-xs leading-6 px-1 py-0.5 truncate border border-tint-subtle bg-tint;
|
||||
}
|
||||
|
||||
.openapi-select {
|
||||
min-width: fit-content;
|
||||
max-width: fit-content;
|
||||
}
|
||||
.openapi-select:focus {
|
||||
width: auto;
|
||||
}
|
||||
@@ -521,7 +542,7 @@
|
||||
|
||||
/* Tabs */
|
||||
.openapi-tabs-list {
|
||||
@apply flex flex-row gap-1.5 py-1.5 px-2.5 border-b border-tint-subtle w-full overflow-x-scroll;
|
||||
@apply flex flex-row gap-1.5 py-1.5 px-2.5 w-full overflow-x-scroll;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
@@ -531,11 +552,12 @@
|
||||
}
|
||||
|
||||
.openapi-tabs-tab[aria-selected="true"] {
|
||||
@apply text-primary after:absolute after:-bottom-2 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all;
|
||||
@apply text-primary after:absolute after:-bottom-[calc(0.375rem_+_1px)] after:z-20 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all;
|
||||
}
|
||||
|
||||
.openapi-tabs-panel {
|
||||
@apply flex-1 text-sm focus-visible:outline-none;
|
||||
@apply flex-1 text-sm relative focus-visible:outline-none;
|
||||
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
|
||||
}
|
||||
|
||||
.openapi-tabs-footer {
|
||||
@@ -654,7 +676,8 @@
|
||||
@apply border border-tint-subtle rounded-lg;
|
||||
}
|
||||
|
||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema {
|
||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema,
|
||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-root {
|
||||
@apply p-2.5;
|
||||
}
|
||||
|
||||
@@ -691,3 +714,7 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.openapi-copy-button {
|
||||
@apply hover:brightness-95;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
|
||||
const tabs: TabsItem[] = [];
|
||||
const tabsBody: React.ReactNode[] = [];
|
||||
|
||||
block.nodes.forEach((tab) => {
|
||||
block.nodes.forEach((tab, index) => {
|
||||
tabs.push({
|
||||
id: tab.meta?.id ?? tab.key!,
|
||||
title: tab.data.title ?? '',
|
||||
@@ -20,6 +20,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
|
||||
|
||||
tabsBody.push(
|
||||
<Blocks
|
||||
key={tab.key ?? index}
|
||||
nodes={tab.nodes}
|
||||
document={document}
|
||||
ancestorBlocks={[...ancestorBlocks, block, tab]}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import { LoadingButton } from '../primitives';
|
||||
|
||||
export function TrackEventButton(props: {
|
||||
action: any;
|
||||
resolved: { url: string; text: string };
|
||||
}) {
|
||||
const { action, resolved } = props;
|
||||
const trackEvent = useTrackEvent();
|
||||
|
||||
return (
|
||||
<LoadingButton
|
||||
onClick={async () => {
|
||||
trackEvent({ type: 'action_click', action: action });
|
||||
window.open(resolved.url, '_blank', 'noopener noreferrer');
|
||||
}}
|
||||
href={resolved.url}
|
||||
variant="primary"
|
||||
size="medium"
|
||||
className={tcls(
|
||||
'theme-bold:bg-header-link theme-bold:text-header-background theme-bold:shadow-none theme-bold:hover:bg-header-link theme-bold:hover:text-header-background theme-bold:hover:shadow-none'
|
||||
)}
|
||||
// insights={{
|
||||
// type: 'link_click',
|
||||
// link: {
|
||||
// target: linkTarget,
|
||||
// position: SiteInsightsLinkPosition.Header,
|
||||
// },
|
||||
// }}
|
||||
>
|
||||
{resolved.text}
|
||||
</LoadingButton>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
|
||||
|
||||
'bg-tint-base/9',
|
||||
'theme-muted:bg-tint-subtle/9',
|
||||
'theme-bold-tint:bg-tint-subtle/9',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle/9',
|
||||
'theme-gradient:bg-gradient-primary',
|
||||
'theme-gradient-tint:bg-gradient-tint',
|
||||
'contrast-more:bg-tint-base',
|
||||
|
||||
@@ -63,7 +63,7 @@ export function SpacesDropdown(props: {
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className={tcls('truncate', 'grow')}>{siteSpace.title}</span>
|
||||
<span className={tcls('line-clamp-1', 'grow')}>{siteSpace.title}</span>
|
||||
<DropdownChevron />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -93,6 +93,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
const flushEventsSync = useEventCallback(() => {
|
||||
const session = getSession();
|
||||
const visitorId = visitorIdRef.current;
|
||||
console.log('flushEventsSync');
|
||||
if (!visitorId) {
|
||||
throw new Error('Visitor ID should be set before flushing events');
|
||||
}
|
||||
@@ -126,6 +127,8 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
};
|
||||
}
|
||||
|
||||
console.log('allEvents', allEvents.length)
|
||||
|
||||
if (allEvents.length > 0) {
|
||||
if (enabled) {
|
||||
sendEvents({
|
||||
@@ -143,6 +146,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
const visitorId = visitorIdRef.current ?? (await getVisitorId(appURL));
|
||||
visitorIdRef.current = visitorId;
|
||||
|
||||
console.log('flushBatchedEvents');
|
||||
flushEventsSync();
|
||||
}, 1500);
|
||||
|
||||
@@ -167,6 +171,8 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
context,
|
||||
};
|
||||
|
||||
console.log('trackEvent', event, eventsRef.current[pathname].pageContext);
|
||||
|
||||
if (eventsRef.current[pathname].pageContext !== undefined) {
|
||||
// If the pageId is set, we know that the page_view event has been tracked
|
||||
// and we can flush the events
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
type CustomizationSettings,
|
||||
type Revision,
|
||||
type RevisionPageDocument,
|
||||
type RevisionPageGroup,
|
||||
@@ -24,7 +23,7 @@ import { tString } from '@/intl/translate';
|
||||
import { getPagePDFContainerId } from '@/lib/links';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { defaultCustomizationForSpace } from '@/lib/utils';
|
||||
import { defaultCustomization } from '@/lib/utils';
|
||||
import { type PDFSearchParams, getPDFSearchParams } from './urls';
|
||||
|
||||
import { PageControlButtons } from './PageControlButtons';
|
||||
@@ -57,7 +56,7 @@ export async function PDFPage(props: {
|
||||
const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams));
|
||||
|
||||
const customization =
|
||||
'customization' in baseContext ? baseContext.customization : defaultCustomizationForSpace();
|
||||
'customization' in baseContext ? baseContext.customization : defaultCustomization();
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
// Compute the pages to render
|
||||
@@ -180,7 +179,7 @@ export async function PDFPage(props: {
|
||||
|
||||
async function PDFSpaceIntro(props: {
|
||||
space: Space;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: SiteCustomizationSettings;
|
||||
}) {
|
||||
const { space, customization } = props;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||
import { defaultCustomizationForSpace } from '@/lib/utils';
|
||||
import { defaultCustomization } from '@/lib/utils';
|
||||
import { CustomizationThemeMode } from '@gitbook/api';
|
||||
import type { GitBookSiteContext, GitBookSpaceContext } from '@v2/lib/context';
|
||||
|
||||
/**
|
||||
@@ -14,8 +15,9 @@ export async function PDFRootLayout(props: {
|
||||
return (
|
||||
<CustomizationRootLayout
|
||||
customization={
|
||||
'customization' in context ? context.customization : defaultCustomizationForSpace()
|
||||
'customization' in context ? context.customization : defaultCustomization()
|
||||
}
|
||||
forcedTheme={CustomizationThemeMode.Light}
|
||||
>
|
||||
{children}
|
||||
</CustomizationRootLayout>
|
||||
|
||||
@@ -228,7 +228,7 @@ export function PageAside(props: {
|
||||
) : null}
|
||||
<div
|
||||
className={tcls(
|
||||
'sticky bottom-0 z-10 mt-auto flex flex-col bg-tint-base theme-bold-tint:bg-tint-subtle theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle pb-4 page-api-block:xl:max-2xl:hidden page-api-block:xl:max-2xl:pb-0 page-api-block:xl:max-2xl:group-hover/aside:flex',
|
||||
'sticky bottom-0 z-10 mt-auto flex flex-col bg-tint-base theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle pb-4 page-api-block:xl:max-2xl:hidden page-api-block:xl:max-2xl:pb-0 page-api-block:xl:max-2xl:group-hover/aside:flex [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'page-api-block:xl:max-2xl:bg-transparent'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -111,7 +111,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
||||
|
||||
'hover:bg-primary-hover',
|
||||
'theme-muted:hover:bg-primary-active',
|
||||
'theme-bold-tint:hover:bg-primary-active',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:hover:bg-primary-active',
|
||||
'theme-gradient:hover:bg-primary-active',
|
||||
|
||||
'tint:font-semibold',
|
||||
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
CustomizationCorners,
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationIconsStyle,
|
||||
type CustomizationSettings,
|
||||
CustomizationSidebarBackgroundStyle,
|
||||
CustomizationSidebarListStyle,
|
||||
CustomizationThemeMode,
|
||||
type CustomizationThemedColor,
|
||||
type CustomizationTint,
|
||||
type SiteCustomizationSettings,
|
||||
@@ -34,16 +34,18 @@ import { ClientContexts } from './ClientContexts';
|
||||
import '@gitbook/icons/style.css';
|
||||
import './globals.css';
|
||||
import { GITBOOK_FONTS_URL, GITBOOK_ICONS_TOKEN, GITBOOK_ICONS_URL } from '@v2/lib/env';
|
||||
import { AnnouncementDismissedScript } from '../Announcement';
|
||||
|
||||
/**
|
||||
* Layout shared between the content and the PDF renderer.
|
||||
* It takes care of setting the theme and the language.
|
||||
*/
|
||||
export async function CustomizationRootLayout(props: {
|
||||
customization: SiteCustomizationSettings | CustomizationSettings;
|
||||
forcedTheme?: CustomizationThemeMode | null;
|
||||
customization: SiteCustomizationSettings;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { customization, children } = props;
|
||||
const { customization, forcedTheme, children } = props;
|
||||
|
||||
const language = getSpaceLanguage(customization);
|
||||
const tintColor = getTintColor(customization);
|
||||
@@ -85,7 +87,12 @@ export async function CustomizationRootLayout(props: {
|
||||
'links' in customization.styling && `links-${customization.styling.links}`,
|
||||
fontNotoColorEmoji.variable,
|
||||
ibmPlexMono.variable,
|
||||
fontData.type === 'default' ? fontData.variable : 'font-custom'
|
||||
fontData.type === 'default' ? fontData.variable : 'font-custom',
|
||||
|
||||
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
|
||||
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
|
||||
? 'dark'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
<head>
|
||||
@@ -96,6 +103,11 @@ export async function CustomizationRootLayout(props: {
|
||||
{/* Inject custom font @font-face rules */}
|
||||
{fontData.type === 'custom' ? <style>{fontData.fontFaceRules}</style> : null}
|
||||
|
||||
{/* Inject a script to detect if the announcmeent banner has been dismissed */}
|
||||
{'announcement' in customization && customization.announcement?.enabled ? (
|
||||
<AnnouncementDismissedScript />
|
||||
) : null}
|
||||
|
||||
<style
|
||||
nonce={
|
||||
//Since I can't get the nonce to work for inline styles, we need to allow unsafe-inline
|
||||
@@ -141,9 +153,9 @@ export async function CustomizationRootLayout(props: {
|
||||
</head>
|
||||
<body
|
||||
className={tcls(
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'bg-tint-base',
|
||||
'theme-muted:bg-tint-subtle',
|
||||
'theme-bold-tint:bg-tint-subtle',
|
||||
|
||||
'theme-gradient:bg-gradient-primary',
|
||||
'theme-gradient-tint:bg-gradient-tint'
|
||||
@@ -175,7 +187,7 @@ export async function CustomizationRootLayout(props: {
|
||||
* If the tint color is not set or it is a space customization, it will return the default color.
|
||||
*/
|
||||
function getTintColor(
|
||||
customization: CustomizationSettings | SiteCustomizationSettings
|
||||
customization: SiteCustomizationSettings
|
||||
): CustomizationTint['color'] | undefined {
|
||||
if ('tint' in customization.styling && customization.styling.tint) {
|
||||
return {
|
||||
@@ -222,7 +234,7 @@ function getTintMixColor(
|
||||
* If it is a space customization, it will return the default styles.
|
||||
*/
|
||||
function getSidebarStyles(
|
||||
customization: CustomizationSettings | SiteCustomizationSettings
|
||||
customization: SiteCustomizationSettings
|
||||
): SiteCustomizationSettings['styling']['sidebar'] {
|
||||
if ('sidebar' in customization.styling) {
|
||||
return {
|
||||
@@ -242,7 +254,7 @@ function getSidebarStyles(
|
||||
* If it is a space customization, it will return the default styles.
|
||||
*/
|
||||
function getSemanticColors(
|
||||
customization: CustomizationSettings | SiteCustomizationSettings
|
||||
customization: SiteCustomizationSettings
|
||||
): Pick<
|
||||
SiteCustomizationSettings['styling'],
|
||||
'infoColor' | 'successColor' | 'warningColor' | 'dangerColor'
|
||||
|
||||
@@ -160,3 +160,7 @@ html {
|
||||
html.dark {
|
||||
color-scheme: dark light;
|
||||
}
|
||||
|
||||
html.announcement-hidden .announcement-banner {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
@@ -189,6 +189,9 @@ function SearchModalBody(
|
||||
setSearchState((state) => (state ? { ...state, ask: true } : null));
|
||||
};
|
||||
|
||||
// We trim the query to avoid invalidating the search when the user is typing between words.
|
||||
const normalizedQuery = state.query.trim();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
transition={{
|
||||
@@ -288,12 +291,14 @@ function SearchModalBody(
|
||||
<SearchResults
|
||||
ref={resultsRef}
|
||||
global={isMultiVariants && state.global}
|
||||
query={state.query}
|
||||
query={normalizedQuery}
|
||||
withAsk={withAsk}
|
||||
onSwitchToAsk={onSwitchToAsk}
|
||||
/>
|
||||
) : null}
|
||||
{state.query && state.ask && withAsk ? <SearchAskAnswer query={state.query} /> : null}
|
||||
{normalizedQuery && state.ask && withAsk ? (
|
||||
<SearchAskAnswer query={normalizedQuery} />
|
||||
) : null}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import React from 'react';
|
||||
|
||||
import { Link } from '../primitives';
|
||||
import { HighlightQuery } from './HighlightQuery';
|
||||
@@ -17,6 +16,14 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
|
||||
) {
|
||||
const { query, item, active } = props;
|
||||
|
||||
const breadcrumbs =
|
||||
item.breadcrumbs?.map((crumb) => (
|
||||
<span key={crumb.label} className="flex items-center gap-1">
|
||||
{crumb.icon ? <Icon className="size-3" icon={crumb.icon as IconName} /> : null}
|
||||
{crumb.label}
|
||||
</span>
|
||||
)) ?? [];
|
||||
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
@@ -38,6 +45,14 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
|
||||
? ['is-active', 'bg-primary', 'text-contrast-primary', 'hover:bg-primary-hover']
|
||||
: null
|
||||
)}
|
||||
insights={{
|
||||
type: 'search_open_result',
|
||||
query,
|
||||
result: {
|
||||
pageId: item.pageId,
|
||||
spaceId: item.spaceId,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="size-4">
|
||||
<Icon
|
||||
@@ -46,18 +61,44 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
|
||||
/>
|
||||
</div>
|
||||
<div className={tcls('flex', 'flex-col', 'w-full')}>
|
||||
{item.spaceTitle ? (
|
||||
{breadcrumbs.length > 0 ? (
|
||||
<div
|
||||
className={tcls(
|
||||
'text-xs',
|
||||
'opacity-6',
|
||||
'contrast-more:opacity-11',
|
||||
'font-normal',
|
||||
'uppercase',
|
||||
'tracking-wider',
|
||||
'mb-1'
|
||||
'mb-1',
|
||||
'flex',
|
||||
'flex-wrap',
|
||||
'gap-x-2',
|
||||
'gap-y-1',
|
||||
'items-center'
|
||||
)}
|
||||
>
|
||||
{item.spaceTitle}
|
||||
{(breadcrumbs.length > 3
|
||||
? [
|
||||
...breadcrumbs.slice(0, 2),
|
||||
<Icon key="ellipsis" icon="ellipsis-h" className="size-3" />,
|
||||
...breadcrumbs.slice(-1),
|
||||
]
|
||||
: breadcrumbs
|
||||
).map((crumb, index) => (
|
||||
<>
|
||||
{index !== 0 ? (
|
||||
<Icon
|
||||
key={`${crumb.key}-icon`}
|
||||
icon="chevron-right"
|
||||
className="size-3"
|
||||
/>
|
||||
) : null}
|
||||
<span key={crumb.key} className="line-clamp-1">
|
||||
{crumb}
|
||||
</span>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<HighlightQuery query={query} text={item.title} />
|
||||
|
||||
@@ -77,7 +77,8 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
setResultsState({ results: [], fetching: true });
|
||||
// Silently fetch the recommended questions, instead of showing a spinner
|
||||
setResultsState({ results: [], fetching: false });
|
||||
|
||||
// We currently have a bug where the same question can be returned multiple times.
|
||||
// This is a workaround to avoid that.
|
||||
|
||||
@@ -40,6 +40,14 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
|
||||
'hover:bg-primary-hover',
|
||||
]
|
||||
)}
|
||||
insights={{
|
||||
type: 'search_open_result',
|
||||
query,
|
||||
result: {
|
||||
pageId: item.pageId,
|
||||
spaceId: item.spaceId,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
'use server';
|
||||
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { findSiteSpaceById, getSiteStructureSections } from '@/lib/sites';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import type {
|
||||
RevisionPage,
|
||||
SearchAIAnswer,
|
||||
SearchAIRecommendedQuestionStream,
|
||||
SearchPageResult,
|
||||
SiteSpace,
|
||||
SearchSpaceResult,
|
||||
SiteSection,
|
||||
SiteSectionGroup,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import type { GitBookBaseContext, GitBookSiteContext } from '@v2/lib/context';
|
||||
@@ -13,13 +19,9 @@ import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/li
|
||||
import { createStreamableValue } from 'ai/rsc';
|
||||
import type * as React from 'react';
|
||||
|
||||
import { getAbsoluteHref } from '@/lib/links';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { findSiteSpaceById } from '@/lib/sites';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
|
||||
import { joinPathWithBaseURL } from '@/lib/paths';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
@@ -30,18 +32,24 @@ export interface ComputedSectionResult {
|
||||
type: 'section';
|
||||
id: string;
|
||||
title: string;
|
||||
href: string;
|
||||
body: string;
|
||||
href: string;
|
||||
|
||||
pageId: string;
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
export interface ComputedPageResult {
|
||||
type: 'page';
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
href: string;
|
||||
|
||||
/** When part of a multi-spaces search, the title of the space */
|
||||
spaceTitle?: string;
|
||||
pageId: string;
|
||||
spaceId: string;
|
||||
|
||||
breadcrumbs?: Array<{ icon?: IconName; label: string }>;
|
||||
}
|
||||
|
||||
export interface AskAnswerSource {
|
||||
@@ -252,11 +260,27 @@ async function searchSiteContent(
|
||||
return (
|
||||
await Promise.all(
|
||||
searchResults.map(async (spaceItem) => {
|
||||
const sections = getSiteStructureSections(structure).flatMap((item) =>
|
||||
item.object === 'site-section-group' ? [item, ...item.sections] : item
|
||||
);
|
||||
const siteSpace = findSiteSpaceById(structure, spaceItem.id);
|
||||
const siteSection = sections.find(
|
||||
(section) => section.id === siteSpace?.section
|
||||
) as SiteSection;
|
||||
const siteSectionGroup = siteSection?.sectionGroup
|
||||
? sections.find((sectionGroup) => sectionGroup.id === siteSection.sectionGroup)
|
||||
: null;
|
||||
|
||||
return Promise.all(
|
||||
spaceItem.pages.map((item) =>
|
||||
transformSitePageResult(item, siteSpace ?? undefined)
|
||||
spaceItem.pages.map((pageItem) =>
|
||||
transformSitePageResult(context, {
|
||||
pageItem,
|
||||
spaceItem,
|
||||
space: siteSpace?.space,
|
||||
spaceURL: siteSpace?.urls.published,
|
||||
siteSection: siteSection ?? undefined,
|
||||
siteSectionGroup: (siteSectionGroup as SiteSectionGroup) ?? undefined,
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
@@ -297,7 +321,7 @@ async function transformAnswer(
|
||||
const spaceURL = siteSpace?.urls.published;
|
||||
|
||||
const href = spaceURL
|
||||
? await getURLWithSections(page.page.path, spaceURL)
|
||||
? joinPathWithBaseURL(spaceURL, page.page.path)
|
||||
: context.linker.toPathForPage({
|
||||
pages,
|
||||
page: page.page,
|
||||
@@ -330,55 +354,59 @@ async function transformAnswer(
|
||||
};
|
||||
}
|
||||
|
||||
async function transformSectionsAndPage(args: {
|
||||
item: SearchPageResult;
|
||||
space?: Space;
|
||||
spaceURL?: string;
|
||||
}): Promise<[ComputedPageResult, ComputedSectionResult[]]> {
|
||||
const { item, space, spaceURL } = args;
|
||||
|
||||
const sections = await Promise.all(
|
||||
item.sections?.map<Promise<ComputedSectionResult>>(async (section) => ({
|
||||
type: 'section',
|
||||
id: `${item.id}/${section.id}`,
|
||||
title: section.title,
|
||||
href: await getURLWithSections(section.path, spaceURL),
|
||||
body: section.body,
|
||||
})) ?? []
|
||||
);
|
||||
async function transformSitePageResult(
|
||||
context: GitBookBaseContext,
|
||||
args: {
|
||||
pageItem: SearchPageResult;
|
||||
spaceItem: SearchSpaceResult;
|
||||
space?: Space;
|
||||
spaceURL?: string;
|
||||
siteSection?: SiteSection;
|
||||
siteSectionGroup?: SiteSectionGroup;
|
||||
}
|
||||
): Promise<OrderedComputedResult[]> {
|
||||
const { pageItem, spaceItem, space, spaceURL, siteSection, siteSectionGroup } = args;
|
||||
const { linker } = context;
|
||||
|
||||
const page: ComputedPageResult = {
|
||||
type: 'page',
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
href: await getURLWithSections(item.path, spaceURL),
|
||||
spaceTitle: space?.title,
|
||||
id: `${spaceItem.id}/${pageItem.id}`,
|
||||
title: pageItem.title,
|
||||
href: spaceURL
|
||||
? linker.toLinkForContent(joinPathWithBaseURL(spaceURL, pageItem.path))
|
||||
: linker.toPathInSpace(pageItem.path),
|
||||
pageId: pageItem.id,
|
||||
spaceId: spaceItem.id,
|
||||
breadcrumbs: [
|
||||
siteSectionGroup && {
|
||||
icon: siteSectionGroup?.icon as IconName,
|
||||
label: siteSectionGroup.title,
|
||||
},
|
||||
siteSection && {
|
||||
icon: siteSection?.icon as IconName,
|
||||
label: siteSection.title,
|
||||
},
|
||||
(!siteSection || siteSection?.siteSpaces.length > 1) && space
|
||||
? {
|
||||
label: space?.title,
|
||||
}
|
||||
: undefined,
|
||||
].filter((item) => item !== undefined),
|
||||
};
|
||||
|
||||
return [page, sections];
|
||||
}
|
||||
const pageSections = await Promise.all(
|
||||
pageItem.sections?.map<Promise<ComputedSectionResult>>(async (section) => ({
|
||||
type: 'section',
|
||||
id: `${page.id}/${section.id}`,
|
||||
title: section.title,
|
||||
href: spaceURL
|
||||
? linker.toLinkForContent(joinPathWithBaseURL(spaceURL, section.path))
|
||||
: linker.toPathInSpace(pageItem.path),
|
||||
body: section.body,
|
||||
pageId: pageItem.id,
|
||||
spaceId: spaceItem.id,
|
||||
})) ?? []
|
||||
);
|
||||
|
||||
async function transformSitePageResult(item: SearchPageResult, siteSpace?: SiteSpace) {
|
||||
const [page, sections] = await transformSectionsAndPage({
|
||||
item,
|
||||
space: siteSpace?.space,
|
||||
spaceURL: siteSpace?.urls.published,
|
||||
});
|
||||
|
||||
return [page, ...sections];
|
||||
}
|
||||
|
||||
// Resolve a relative path to an absolute URL
|
||||
// if the search result is relative to another space, we use the space URL
|
||||
async function getURLWithSections(path: string, spaceURL?: string) {
|
||||
if (spaceURL) {
|
||||
if (!spaceURL.endsWith('/')) {
|
||||
spaceURL += '/';
|
||||
}
|
||||
if (path.startsWith('/')) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
return spaceURL + path;
|
||||
}
|
||||
return getAbsoluteHref(path);
|
||||
return [page, ...pageSections];
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className
|
||||
<nav
|
||||
aria-label="Sections"
|
||||
className={tcls(
|
||||
'-mx-5 before:contents[] relative border-tint-subtle border-b from-transparent sidebar-filled:to-tint-subtle theme-bold-tint:to-tint-subtle theme-muted:to-tint-subtle to-tint-base text-sm text-tint before:pointer-events-none before:absolute before:right-2 before:bottom-0 before:left-0 before:h-12 before:bg-gradient-to-b [html.sidebar-filled.theme-bold.tint_&]:to-tint-base [html.sidebar-filled.theme-muted_&]:to-tint-base',
|
||||
'-mx-5 before:contents[] relative border-tint-subtle border-b from-transparent sidebar-filled:to-tint-subtle theme-muted:to-tint-subtle to-tint-base text-sm text-tint before:pointer-events-none before:absolute before:right-2 before:bottom-0 before:left-0 before:h-12 before:bg-gradient-to-b [html.sidebar-filled.theme-bold.tint_&]:to-tint-base [html.sidebar-filled.theme-bold.tint_&]:to-tint-subtle [html.sidebar-filled.theme-muted_&]:to-tint-base',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||
import { GITBOOK_API_PUBLIC_URL, GITBOOK_APP_URL } from '@v2/lib/env';
|
||||
import { Announcement } from '../Announcement';
|
||||
import { SpacesDropdown } from '../Header/SpacesDropdown';
|
||||
import { InsightsProvider } from '../Insights';
|
||||
import { SiteSectionList, encodeClientSiteSections } from '../SiteSections';
|
||||
@@ -62,6 +63,7 @@ export function SpaceLayout(props: {
|
||||
spaceId={context.space.id}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
>
|
||||
<Announcement context={context} />
|
||||
<Header withTopHeader={withTopHeader} context={context} />
|
||||
<div className="scroll-nojump">
|
||||
<div
|
||||
|
||||
@@ -40,7 +40,7 @@ export function PageGroupItem(props: {
|
||||
'bg-tint-base',
|
||||
'sidebar-filled:bg-tint-subtle',
|
||||
'theme-muted:bg-tint-subtle',
|
||||
'theme-bold-tint:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
'[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
|
||||
|
||||
@@ -75,7 +75,7 @@ export function TableOfContents(props: {
|
||||
|
||||
'sidebar-filled:bg-tint-subtle',
|
||||
'theme-muted:bg-tint-subtle',
|
||||
'theme-bold-tint:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
'page-no-toc:!bg-transparent',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type {
|
||||
CustomizationSettings,
|
||||
SiteCustomizationSettings,
|
||||
SiteInsightsTrademarkPlacement,
|
||||
Space,
|
||||
@@ -16,7 +15,7 @@ import { Link } from '../primitives';
|
||||
*/
|
||||
export function Trademark(props: {
|
||||
space: Space;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: SiteCustomizationSettings;
|
||||
placement: SiteInsightsTrademarkPlacement;
|
||||
}) {
|
||||
return (
|
||||
@@ -53,7 +52,7 @@ export function Trademark(props: {
|
||||
'before:to-tint-base',
|
||||
'sidebar-filled:before:to-tint-subtle',
|
||||
'theme-muted:before:to-tint-subtle',
|
||||
'theme-bold-tint:before:to-tint-subtle',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:before:to-tint-subtle',
|
||||
'[html.sidebar-filled.theme-muted_&]:before:to-tint-base',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:before:to-tint-base',
|
||||
'page-no-toc:before:!to-transparent'
|
||||
@@ -69,7 +68,7 @@ export function Trademark(props: {
|
||||
*/
|
||||
export function TrademarkLink(props: {
|
||||
space: Space;
|
||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||
customization: SiteCustomizationSettings;
|
||||
placement: SiteInsightsTrademarkPlacement;
|
||||
}) {
|
||||
const { space, customization, placement } = props;
|
||||
|
||||
@@ -79,7 +79,7 @@ function ThemeButton(props: {
|
||||
active && [
|
||||
'bg-primary',
|
||||
'theme-muted:bg-primary-hover',
|
||||
'theme-bold-tint:bg-primary-hover',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-primary-hover',
|
||||
'hover:bg-primary',
|
||||
'text-primary-strong',
|
||||
'contrast-more:text-primary-strong',
|
||||
|
||||
@@ -12,11 +12,12 @@ export function useScrollActiveId(
|
||||
) {
|
||||
const { rootMargin, threshold = 0.5 } = options;
|
||||
|
||||
const [activeId, setActiveId] = React.useState<string | null>(null);
|
||||
const [activeId, setActiveId] = React.useState<string>(ids[0]);
|
||||
const sectionsIntersectingMap = React.useRef<Map<string, boolean>>(new Map());
|
||||
|
||||
React.useEffect(() => {
|
||||
setActiveId(null);
|
||||
const defaultActiveId = ids[0];
|
||||
setActiveId((activeId) => (ids.indexOf(activeId) !== -1 ? activeId : defaultActiveId));
|
||||
|
||||
if (typeof IntersectionObserver === 'undefined') {
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { Link, type LinkInsightsProps } from './Link';
|
||||
|
||||
type ButtonProps = {
|
||||
export type ButtonProps = {
|
||||
href?: string;
|
||||
variant?: 'primary' | 'secondary';
|
||||
size?: 'default' | 'medium' | 'small';
|
||||
|
||||
@@ -16,7 +16,8 @@ export type LinkInsightsProps = {
|
||||
insights?:
|
||||
| TrackEventInput<'ad_click'>
|
||||
| TrackEventInput<'link_click'>
|
||||
| TrackEventInput<'trademark_click'>;
|
||||
| TrackEventInput<'trademark_click'>
|
||||
| TrackEventInput<'search_open_result'>;
|
||||
};
|
||||
|
||||
export type LinkProps = Omit<BaseLinkProps, 'href'> &
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { useTransition } from 'react';
|
||||
import { Button, type ButtonProps } from './Button';
|
||||
|
||||
export function LoadingButton({ href, onClick, className, ...rest }: ButtonProps & { onClick: () => Promise<void> }) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
startTransition(onClick);
|
||||
return false;
|
||||
}
|
||||
return <Button className={tcls(className, isPending ? 'animate-pulse' : '')} {...rest} onClick={handleClick} />
|
||||
}
|
||||
@@ -2,6 +2,26 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { Link, type LinkProps } from '../primitives/Link';
|
||||
|
||||
export const linkStyles = [
|
||||
'underline',
|
||||
'decoration-[max(0.07em,1px)]', // Set the underline to be proportional to the font size, with a minimum. The default is too thin.
|
||||
'underline-offset-2',
|
||||
'links-accent:underline-offset-4',
|
||||
|
||||
'links-default:decoration-primary/6',
|
||||
'links-default:text-primary-subtle',
|
||||
'links-default:hover:text-primary-strong',
|
||||
'links-default:contrast-more:text-primary',
|
||||
'links-default:contrast-more:hover:text-primary-strong',
|
||||
|
||||
'links-accent:decoration-primary-subtle',
|
||||
'links-accent:hover:decoration-[3px]',
|
||||
'links-accent:hover:[text-decoration-skip-ink:none]',
|
||||
|
||||
'transition-all',
|
||||
'duration-100',
|
||||
];
|
||||
|
||||
/**
|
||||
* Styled version of Link component.
|
||||
*/
|
||||
@@ -9,29 +29,7 @@ export function StyledLink(props: Omit<LinkProps, 'style'> & { style?: ClassValu
|
||||
const { style, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
{...rest}
|
||||
className={tcls(
|
||||
'underline',
|
||||
'decoration-[max(0.07em,1px)]', // Set the underline to be proportional to the font size, with a minimum. The default is too thin.
|
||||
'underline-offset-2',
|
||||
'links-accent:underline-offset-4',
|
||||
|
||||
'links-default:decoration-primary/6',
|
||||
'links-default:text-primary-subtle',
|
||||
'links-default:hover:text-primary-strong',
|
||||
'links-default:contrast-more:text-primary',
|
||||
'links-default:contrast-more:hover:text-primary-strong',
|
||||
|
||||
'links-accent:decoration-primary-subtle',
|
||||
'links-accent:hover:decoration-[3px]',
|
||||
'links-accent:hover:[text-decoration-skip-ink:none]',
|
||||
|
||||
'transition-all',
|
||||
'duration-100',
|
||||
style
|
||||
)}
|
||||
>
|
||||
<Link {...rest} className={tcls(linkStyles, style)}>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './StyledLink';
|
||||
export * from './DateRelative';
|
||||
export * from './Emoji';
|
||||
export * from './LoadingPane';
|
||||
export * from './LoadingButton';
|
||||
@@ -6,6 +6,7 @@ import { generateFontFacesCSS, getFontSourcesToPreload } from './custom';
|
||||
const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
|
||||
basic: {
|
||||
id: 'open-sans',
|
||||
custom: true,
|
||||
fontFamily: 'Open Sans',
|
||||
fontFaces: [
|
||||
{
|
||||
@@ -31,6 +32,7 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
|
||||
|
||||
multiWeight: {
|
||||
id: 'roboto',
|
||||
custom: true,
|
||||
fontFamily: 'Roboto',
|
||||
fontFaces: [
|
||||
{
|
||||
@@ -83,6 +85,7 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
|
||||
|
||||
multiSource: {
|
||||
id: 'lato',
|
||||
custom: true,
|
||||
fontFamily: 'Lato',
|
||||
fontFaces: [
|
||||
{
|
||||
@@ -100,6 +103,7 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
|
||||
|
||||
missingFormat: {
|
||||
id: 'source-sans',
|
||||
custom: true,
|
||||
fontFamily: 'Source Sans Pro',
|
||||
fontFaces: [
|
||||
{
|
||||
@@ -117,12 +121,14 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
|
||||
|
||||
empty: {
|
||||
id: 'empty-font',
|
||||
custom: true,
|
||||
fontFamily: 'Empty Font',
|
||||
fontFaces: [],
|
||||
},
|
||||
|
||||
specialChars: {
|
||||
id: 'special-font',
|
||||
custom: true,
|
||||
fontFamily: 'Special Font & Co.',
|
||||
fontFaces: [
|
||||
{
|
||||
@@ -134,6 +140,7 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
|
||||
|
||||
complex: {
|
||||
id: 'complex-font',
|
||||
custom: true,
|
||||
fontFamily: 'Complex Font',
|
||||
fontFaces: [
|
||||
{
|
||||
@@ -155,6 +162,7 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
|
||||
|
||||
variousURLs: {
|
||||
id: 'various-urls',
|
||||
custom: true,
|
||||
fontFamily: 'Various URLs Font',
|
||||
fontFaces: [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CustomizationSettings, SiteCustomizationSettings } from '@gitbook/api';
|
||||
import type { SiteCustomizationSettings } from '@gitbook/api';
|
||||
|
||||
import { type TranslationLanguage, languages } from './translations';
|
||||
|
||||
@@ -7,9 +7,7 @@ export * from './translate';
|
||||
/**
|
||||
* Create the translation context for a space to use in the server components.
|
||||
*/
|
||||
export function getSpaceLanguage(
|
||||
customization: CustomizationSettings | SiteCustomizationSettings
|
||||
): TranslationLanguage {
|
||||
export function getSpaceLanguage(customization: SiteCustomizationSettings): TranslationLanguage {
|
||||
const fallback = languages.en;
|
||||
|
||||
const { locale } = customization.internationalization;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PublishedSiteContent, SiteAPIToken } from '@gitbook/api';
|
||||
import type { SiteAPIToken } from '@gitbook/api';
|
||||
import type { SiteURLData } from '@v2/lib/context';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
/**
|
||||
@@ -9,7 +10,7 @@ export type VisitorAuthClaims = Record<string, any>;
|
||||
/**
|
||||
* Get the visitor auth claims from the API response obtained from `getPublishedContentByUrl`.
|
||||
*/
|
||||
export function getVisitorAuthClaims(siteData: PublishedSiteContent): VisitorAuthClaims {
|
||||
export function getVisitorAuthClaims(siteData: SiteURLData): VisitorAuthClaims {
|
||||
const { apiToken } = siteData;
|
||||
|
||||
return getVisitorAuthClaimsFromToken(jwtDecode<SiteAPIToken>(apiToken));
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
OpenAPISchemasBlock,
|
||||
ResolveOpenAPIBlockArgs,
|
||||
} from '@/lib/openapi/types';
|
||||
import { memoize } from '@v2/lib/data/memoize';
|
||||
import { assert } from 'ts-essentials';
|
||||
import { resolveContentRef } from '../references';
|
||||
import { isV2 } from '../v2';
|
||||
@@ -65,7 +66,7 @@ const fetchFilesystemV1 = cache({
|
||||
},
|
||||
});
|
||||
|
||||
async function fetchFilesystemV2(url: string) {
|
||||
const fetchFilesystemV2 = memoize(async function fetchFilesystemV2(url: string) {
|
||||
'use cache';
|
||||
|
||||
// TODO: add cache lifetime once we can use next.js 15 code here
|
||||
@@ -73,7 +74,7 @@ async function fetchFilesystemV2(url: string) {
|
||||
const response = await fetchFilesystemUncached(url);
|
||||
|
||||
return response;
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchFilesystemUncached(
|
||||
url: string,
|
||||
|
||||
@@ -104,7 +104,7 @@ export function resolvePrevNextPages(
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a page to its canonical path.
|
||||
* Resolve a page to its canonical path. The first page of the site will return "".
|
||||
* The path will NOT start with "/".
|
||||
*/
|
||||
export function getPagePath(
|
||||
|
||||
@@ -5,6 +5,23 @@ export function joinPath(...parts: string[]): string {
|
||||
return parts.join('/').replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a path with a base URL.
|
||||
*/
|
||||
export function joinPathWithBaseURL(baseURL: string, path: string): string {
|
||||
if (!path) {
|
||||
return baseURL;
|
||||
}
|
||||
|
||||
if (!baseURL.endsWith('/')) {
|
||||
baseURL += '/';
|
||||
}
|
||||
if (path.startsWith('/')) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
return baseURL + path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the trailing slash from a path.
|
||||
*/
|
||||
|
||||
@@ -28,16 +28,21 @@ export async function trace<T>(
|
||||
};
|
||||
|
||||
const start = now();
|
||||
let failed = false;
|
||||
try {
|
||||
return await fn(span);
|
||||
} catch (error) {
|
||||
span.setAttribute('error', true);
|
||||
failed = true;
|
||||
throw error;
|
||||
} finally {
|
||||
if (process.env.SILENT !== 'true') {
|
||||
if (process.env.SILENT !== 'true' && process.env.NODE_ENV !== 'development') {
|
||||
const end = now();
|
||||
// biome-ignore lint/suspicious/noConsole: we want to log performance data
|
||||
console.log(`trace ${completeName} ${end - start}ms`, attributes);
|
||||
console.log(
|
||||
`trace ${completeName} ${failed ? 'failed' : 'succeeded'} in ${end - start}ms`,
|
||||
attributes
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { headers } from 'next/headers';
|
||||
export async function shouldTrackEvents(): Promise<boolean> {
|
||||
const headersList = await headers();
|
||||
|
||||
return true;
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' ||
|
||||
(process.env.GITBOOK_BLOCK_PAGE_VIEWS_TRACKING &&
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import * as api from '@gitbook/api';
|
||||
|
||||
/**
|
||||
* Return the customizations with the default values for a space.
|
||||
* Return the default customization settings for a site.
|
||||
*/
|
||||
export function defaultCustomizationForSpace(): api.CustomizationSettings {
|
||||
export function defaultCustomization(): api.SiteCustomizationSettings {
|
||||
return {
|
||||
internationalization: {
|
||||
inherit: false,
|
||||
locale: api.CustomizationLocale.En,
|
||||
},
|
||||
styling: {
|
||||
primaryColor: {
|
||||
dark: '#346DDB',
|
||||
light: '#346DDB',
|
||||
},
|
||||
theme: api.CustomizationTheme.Clean,
|
||||
primaryColor: { light: '#346DDB', dark: '#346DDB' },
|
||||
infoColor: { light: '#787878', dark: '#787878' },
|
||||
warningColor: { light: '#FE9A00', dark: '#FE9A00' },
|
||||
dangerColor: { light: '#FB2C36', dark: '#FB2C36' },
|
||||
successColor: { light: '#00C950', dark: '#00C950' },
|
||||
corners: api.CustomizationCorners.Rounded,
|
||||
font: api.CustomizationDefaultFont.Inter,
|
||||
background: api.CustomizationBackground.Plain,
|
||||
icons: api.CustomizationIconsStyle.Regular,
|
||||
links: api.CustomizationLinksStyle.Default,
|
||||
sidebar: {
|
||||
background: api.CustomizationSidebarBackgroundStyle.Default,
|
||||
list: api.CustomizationSidebarListStyle.Default,
|
||||
},
|
||||
search: api.CustomizationSearchStyle.Subtle,
|
||||
},
|
||||
internationalization: {
|
||||
locale: api.CustomizationLocale.En,
|
||||
},
|
||||
favicon: {},
|
||||
header: {
|
||||
@@ -28,28 +36,32 @@ export function defaultCustomizationForSpace(): api.CustomizationSettings {
|
||||
},
|
||||
themes: {
|
||||
default: api.CustomizationThemeMode.Light,
|
||||
toggeable: false,
|
||||
toggeable: true,
|
||||
},
|
||||
trademark: {
|
||||
pdf: {
|
||||
enabled: true,
|
||||
},
|
||||
feedback: {
|
||||
enabled: false,
|
||||
},
|
||||
pdf: {
|
||||
enabled: true,
|
||||
},
|
||||
aiSearch: {
|
||||
enabled: true,
|
||||
},
|
||||
advancedCustomization: {
|
||||
enabled: true,
|
||||
},
|
||||
git: {
|
||||
showEditLink: false,
|
||||
},
|
||||
pagination: {
|
||||
enabled: true,
|
||||
},
|
||||
privacyPolicy: {},
|
||||
socialPreview: {},
|
||||
git: {
|
||||
showEditLink: false,
|
||||
trademark: {
|
||||
enabled: true,
|
||||
},
|
||||
inherit: false,
|
||||
privacyPolicy: {
|
||||
url: 'https://www.gitbook.com/privacy',
|
||||
},
|
||||
socialPreview: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function getV1BaseContext(): Promise<GitBookBaseContext> {
|
||||
const dataFetcher = await getDataFetcherV1();
|
||||
|
||||
const imageResizer = createImageResizer({
|
||||
host,
|
||||
imagesContextId: host,
|
||||
// In V1, we always resize at the top level of the hostname, not relative to the content.
|
||||
linker: createLinker({
|
||||
host,
|
||||
@@ -153,6 +153,10 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
|
||||
});
|
||||
},
|
||||
|
||||
getRevisionPageMarkdown() {
|
||||
throw new Error('Not implemented in v1');
|
||||
},
|
||||
|
||||
getDocument(params) {
|
||||
return wrapDataFetcherError(async () => {
|
||||
const document = await getDocument(params.spaceId, params.documentId);
|
||||
@@ -266,6 +270,22 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
|
||||
return result;
|
||||
});
|
||||
},
|
||||
|
||||
getAction(params) {
|
||||
return wrapDataFetcherError(async () => {
|
||||
console.log('fetching', params.url);
|
||||
const url = new URL(params.url);
|
||||
Object.keys(params.claims).forEach((key) => {
|
||||
url.searchParams.append(key, params.claims[key]);
|
||||
});
|
||||
const result = await fetch(url.toString());
|
||||
console.log('fetched', result.statusText)
|
||||
if (!result) {
|
||||
throw new DataFetcherError('Action not found', 404);
|
||||
}
|
||||
return await result.json();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return dataFetcher;
|
||||
|
||||
@@ -564,6 +564,7 @@ async function lookupSiteOrSpaceInMultiIdMode(
|
||||
apiEndpoint,
|
||||
contextId,
|
||||
cookies,
|
||||
canonicalUrl: url.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ export async function serveResizedImage(
|
||||
request: Request,
|
||||
requestOptions: {
|
||||
/**
|
||||
* The host to use for the image.
|
||||
* The site identifier to use for verifying the image signature.
|
||||
*/
|
||||
host?: string;
|
||||
imagesContextId?: string;
|
||||
} = {}
|
||||
) {
|
||||
const requestURL = new URL(request.url);
|
||||
@@ -45,8 +45,8 @@ export async function serveResizedImage(
|
||||
}
|
||||
|
||||
// Verify the signature
|
||||
const host =
|
||||
requestOptions.host ??
|
||||
const imagesContextId =
|
||||
requestOptions.imagesContextId ??
|
||||
request.headers.get('x-gitbook-host') ?? // Only for v1, to be removed
|
||||
request.headers.get('x-forwarded-host') ??
|
||||
request.headers.get('host') ??
|
||||
@@ -54,13 +54,13 @@ export async function serveResizedImage(
|
||||
const verified = await verifyImageSignature(
|
||||
{
|
||||
url,
|
||||
host,
|
||||
imagesContextId,
|
||||
},
|
||||
{ signature, version: signatureVersion }
|
||||
);
|
||||
if (!verified) {
|
||||
return new Response(
|
||||
`Invalid signature "${signature ?? ''}" (version ${signatureVersion}) for "${url}" on "${host}"`,
|
||||
`Invalid signature "${signature ?? ''}" (version ${signatureVersion}) for "${url}" on identifier "${imagesContextId}"`,
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import { joinPath } from '@/lib/paths';
|
||||
import { getIndexablePages } from '@/lib/sitemap';
|
||||
import { getSiteStructureSections } from '@/lib/sites';
|
||||
import type { SiteSection, SiteSpace } from '@gitbook/api';
|
||||
import { type GitBookSiteContext, checkIsRootSiteContext } from '@v2/lib/context';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
import assertNever from 'assert-never';
|
||||
import type { ListItem, Paragraph, Root, RootContent } from 'mdast';
|
||||
import { toMarkdown } from 'mdast-util-to-markdown';
|
||||
|
||||
import { getPagePath } from '@/lib/pages';
|
||||
import { joinPath } from '@/lib/paths';
|
||||
import { getIndexablePages } from '@/lib/sitemap';
|
||||
import { getSiteStructureSections } from '@/lib/sites';
|
||||
import { type GitBookSiteContext, checkIsRootSiteContext } from '@v2/lib/context';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
|
||||
/**
|
||||
* Generate a llms.txt file for the site.
|
||||
*/
|
||||
export async function serveLLMsTxt(context: GitBookSiteContext) {
|
||||
export async function serveLLMsTxt(
|
||||
context: GitBookSiteContext,
|
||||
{
|
||||
withMarkdownPages = false,
|
||||
}: {
|
||||
/**
|
||||
* If true, a markdown extension will be added to the page path.
|
||||
*/
|
||||
withMarkdownPages?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const { site } = context;
|
||||
|
||||
if (!checkIsRootSiteContext(context)) {
|
||||
@@ -28,7 +36,7 @@ export async function serveLLMsTxt(context: GitBookSiteContext) {
|
||||
depth: 1,
|
||||
children: [{ type: 'text', value: site.title }],
|
||||
},
|
||||
...(await getNodesFromSiteStructure(context)),
|
||||
...(await getNodesFromSiteStructure(context, { withMarkdownPages })),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -47,15 +55,24 @@ export async function serveLLMsTxt(context: GitBookSiteContext) {
|
||||
/**
|
||||
* Get MDAST nodes from site structure.
|
||||
*/
|
||||
async function getNodesFromSiteStructure(context: GitBookSiteContext): Promise<RootContent[]> {
|
||||
async function getNodesFromSiteStructure(
|
||||
context: GitBookSiteContext,
|
||||
options: {
|
||||
withMarkdownPages: boolean;
|
||||
}
|
||||
): Promise<RootContent[]> {
|
||||
switch (context.structure.type) {
|
||||
case 'sections':
|
||||
return getNodesFromSections(
|
||||
context,
|
||||
getSiteStructureSections(context.structure, { ignoreGroups: true })
|
||||
getSiteStructureSections(context.structure, { ignoreGroups: true }),
|
||||
{ withMarkdownPages: options.withMarkdownPages }
|
||||
);
|
||||
case 'siteSpaces':
|
||||
return getNodesFromSiteSpaces(context, context.structure.structure, { heading: true });
|
||||
return getNodesFromSiteSpaces(context, context.structure.structure, {
|
||||
heading: true,
|
||||
withMarkdownPages: options.withMarkdownPages,
|
||||
});
|
||||
default:
|
||||
assertNever(context.structure);
|
||||
}
|
||||
@@ -66,12 +83,16 @@ async function getNodesFromSiteStructure(context: GitBookSiteContext): Promise<R
|
||||
*/
|
||||
async function getNodesFromSections(
|
||||
context: GitBookSiteContext,
|
||||
siteSections: SiteSection[]
|
||||
siteSections: SiteSection[],
|
||||
options: {
|
||||
withMarkdownPages: boolean;
|
||||
}
|
||||
): Promise<RootContent[]> {
|
||||
const all = await Promise.all(
|
||||
siteSections.map(async (siteSection): Promise<RootContent[]> => {
|
||||
const siteSpaceNodes = await getNodesFromSiteSpaces(context, siteSection.siteSpaces, {
|
||||
heading: false,
|
||||
withMarkdownPages: options.withMarkdownPages,
|
||||
});
|
||||
return [
|
||||
{
|
||||
@@ -97,6 +118,11 @@ async function getNodesFromSiteSpaces(
|
||||
* Includes a heading for each site space.
|
||||
*/
|
||||
heading?: boolean;
|
||||
|
||||
/**
|
||||
* If true, a markdown extension will be added to the page path.
|
||||
*/
|
||||
withMarkdownPages: boolean;
|
||||
}
|
||||
): Promise<RootContent[]> {
|
||||
const { dataFetcher, linker } = context;
|
||||
@@ -118,7 +144,10 @@ async function getNodesFromSiteSpaces(
|
||||
const listChildren = await Promise.all(
|
||||
pages.map(async ({ page }): Promise<ListItem> => {
|
||||
const pageURL = new URL(siteSpaceUrl);
|
||||
pageURL.pathname = joinPath(pageURL.pathname, getPagePath(rootPages, page));
|
||||
pageURL.pathname = joinPath(pageURL.pathname, page.path);
|
||||
if (options.withMarkdownPages) {
|
||||
pageURL.pathname = `${pageURL.pathname}.md`;
|
||||
}
|
||||
|
||||
const url = linker.toLinkForContent(pageURL.toString());
|
||||
const children: Paragraph['children'] = [
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { resolvePagePath } from '@/lib/pages';
|
||||
import { RevisionPageType } from '@gitbook/api';
|
||||
import type { GitBookSiteContext } from '@v2/lib/context';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
|
||||
/**
|
||||
* Generate a markdown version of a page.
|
||||
*/
|
||||
export async function servePageMarkdown(context: GitBookSiteContext, pagePath: string) {
|
||||
const pageLookup = resolvePagePath(context.pages, pagePath);
|
||||
if (!pageLookup) {
|
||||
return new Response(`Page "${pagePath}" not found`, { status: 404 });
|
||||
}
|
||||
|
||||
const { page } = pageLookup;
|
||||
|
||||
if (page.type !== RevisionPageType.Document) {
|
||||
return new Response(`Page "${pagePath}" is not a document`, { status: 404 });
|
||||
}
|
||||
|
||||
const markdown = await throwIfDataError(
|
||||
context.dataFetcher.getRevisionPageMarkdown({
|
||||
spaceId: context.space.id,
|
||||
revisionId: context.revisionId,
|
||||
pageId: page.id,
|
||||
})
|
||||
);
|
||||
|
||||
return new Response(markdown, {
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown; charset=utf-8',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -10,7 +10,8 @@ export async function serveRobotsTxt(context: GitBookSiteContext) {
|
||||
const isRoot = checkIsRootSiteContext(context);
|
||||
const lines = [
|
||||
'User-agent: *',
|
||||
'Disallow: /~gitbook/',
|
||||
// Disallow dynamic routes / search queries
|
||||
'Disallow: /*?',
|
||||
...((await isSiteIndexable(context))
|
||||
? [
|
||||
'Allow: /',
|
||||
|
||||
Vendored
+9
@@ -1,3 +1,12 @@
|
||||
declare module 'memoizee' {
|
||||
declare function memoizee<F extends (...args: any[]) => any>(
|
||||
f: F,
|
||||
options?: { normalizer?: (args: any[]) => string }
|
||||
): F;
|
||||
|
||||
export = memoizee;
|
||||
}
|
||||
|
||||
declare module 'memoizee/weak' {
|
||||
declare function memoizee<F extends (...args: any[]) => any>(f: F): F;
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @gitbook/openapi-parser
|
||||
|
||||
## 2.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7bb37c7: Move filterSelectedOpenAPISchemas to @gitbook/openapi-parser
|
||||
- 5b1e01c: Support for x-stability property
|
||||
- 813b2af: Support for x-enumDescriptions and x-gitbook-enum
|
||||
|
||||
## 2.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@scalar/openapi-parser": "^0.10.10",
|
||||
|
||||
@@ -6,4 +6,5 @@ export type * from '@scalar/openapi-types';
|
||||
export * from './error';
|
||||
export * from './helpers/shouldIgnoreEntity';
|
||||
export * from './traverse';
|
||||
export * from './schemas';
|
||||
export type * from './types';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import { shouldIgnoreEntity } from './helpers/shouldIgnoreEntity';
|
||||
import type { OpenAPISchema } from './types';
|
||||
|
||||
/**
|
||||
* Extract selected schemas from the OpenAPI document.
|
||||
*/
|
||||
export function filterSelectedOpenAPISchemas(
|
||||
schema: OpenAPIV3.Document | OpenAPIV3_1.Document,
|
||||
selectedSchemas: string[]
|
||||
): OpenAPISchema[] {
|
||||
const componentsSchemas = schema.components?.schemas ?? {};
|
||||
|
||||
// Preserve the order of the selected schemas
|
||||
return selectedSchemas
|
||||
.map((name) => {
|
||||
const schema = componentsSchemas[name];
|
||||
if (schema && !shouldIgnoreEntity(schema)) {
|
||||
return {
|
||||
name,
|
||||
schema,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((schema): schema is OpenAPISchema => !!schema);
|
||||
}
|
||||
@@ -47,8 +47,31 @@ export interface OpenAPICustomOperationProperties {
|
||||
* Description in Document format.
|
||||
*/
|
||||
'x-gitbook-description-document'?: object;
|
||||
|
||||
/**
|
||||
* Enums with name and description
|
||||
*/
|
||||
'x-enumDescriptions'?: object;
|
||||
|
||||
/**
|
||||
* Enums with name and description
|
||||
*/
|
||||
'x-gitbook-enum'?: {
|
||||
[key: string]: {
|
||||
description?: string;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Stability of the operation.
|
||||
* @enum 'experimental' | 'alpha' | 'beta' | 'stable'
|
||||
*/
|
||||
'x-stability'?: OpenAPIStability;
|
||||
}
|
||||
|
||||
export type OpenAPIStability = 'experimental' | 'alpha' | 'beta' | 'stable';
|
||||
|
||||
/**
|
||||
* Custom code samples that can be defined at the operation level.
|
||||
* It follows the spec defined by Redocly.
|
||||
@@ -79,3 +102,8 @@ export type FilesystemEntry<T extends AnyObject> = {
|
||||
filename: string;
|
||||
specification: T;
|
||||
};
|
||||
|
||||
export type OpenAPISchema = {
|
||||
name: string;
|
||||
schema: OpenAPIV3.SchemaObject;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# @gitbook/react-openapi
|
||||
|
||||
## 1.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7d0b422: Handle grouped OpenAPISchemas
|
||||
|
||||
## 1.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bd35348: Fix missing alternative schemas
|
||||
- ae78fc5: Fix XML in code sample
|
||||
- 7bb37c7: Move filterSelectedOpenAPISchemas to @gitbook/openapi-parser
|
||||
- 373183a: Safe parse OpenAPI JSON schema
|
||||
- 1505ddb: Fix multiple request examples selector not showing
|
||||
- 61db166: Add OpenAPI write-only indicator
|
||||
- 5b1e01c: Support for x-stability property
|
||||
- cd99ed5: Fix spec properties rendering and missing keys
|
||||
- 813b2af: Support for x-enumDescriptions and x-gitbook-enum
|
||||
- a25fded: Replace $ref with $reference in json-decycle
|
||||
- Updated dependencies [7bb37c7]
|
||||
- Updated dependencies [5b1e01c]
|
||||
- Updated dependencies [813b2af]
|
||||
- @gitbook/openapi-parser@2.1.2
|
||||
|
||||
## 1.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user