Compare commits

...

20 Commits

Author SHA1 Message Date
Johan Preynat c773f8b5e7 Bump next@14.2.4 2024-06-24 12:07:31 +02:00
Johan Preynat f5df40e17d Use a singleton to store the memory cache in an internal WeakMap for the current request (#2356) 2024-06-24 11:13:03 +02:00
Johan Preynat e13c76efed Set preconnect links to main domains on render (#2354) 2024-06-21 17:02:02 +02:00
Johan Preynat 4a11d8d9eb Share memory cache between middleware and request handler (#2347) 2024-06-21 11:13:06 +02:00
spastorelli 009ed8cc6e Call trackViewInSiteById API endpoint when tracking view for sites (#2350) 2024-06-21 10:01:35 +02:00
vibhanshub 106cbbd987 Add support for examples in OpenAPI block (#2314) 2024-06-20 16:02:58 +05:30
Johan Preynat 56e5c3c3f8 Parse cache tags returned from the API set them in response (#2349) 2024-06-20 10:09:02 +02:00
Taran Vohra 1dc3234003 Fix page metadata title (#2348) 2024-06-18 20:52:12 +05:30
Johan Preynat 22dc0d4ff5 Fix passing revalidateBefore option when calling cacheResponse to ensure these go through background revalidation (#2342) 2024-06-17 17:16:12 +02:00
Taran Vohra 3a4caf062c Add getContentTitle to properly render the title for sites & legacy published content (#2343) 2024-06-17 20:28:21 +05:30
Taran Vohra 8ad0657159 Use site's share key as context while fetching site spaces (#2341) 2024-06-14 20:59:05 +05:30
Taran Vohra d8a4ecd0c6 Pass site url as context to listSiteSpaces API (#2339) 2024-06-14 10:40:18 +05:30
David Burghoff 65c7968438 Add German Locale support (#2317)
Co-authored-by: David Burghoff <david.burghoff@swot.de>
Co-authored-by: taranvohra <taranvohra@outlook.com>
2024-06-13 15:04:49 +02:00
Rodrigo Castro 3914f11150 Add Brazilian Portuguese translation (#2285)
Co-authored-by: taranvohra <taranvohra@outlook.com>
2024-06-13 14:54:47 +02:00
Addison d363908297 Add contributors to readme, bump API (#2333) 2024-06-12 15:24:18 +02:00
Greg Bergé 414556ac7a Remove cache client-side (#2337) 2024-06-10 11:17:48 +02:00
Steven H c768e9a90a Add another space to ignore list whilst we debug issues. (#2334) 2024-05-28 19:02:07 +01:00
Taran Vohra 553f1f228f Bump @gitbook/api to 0.48.0 (#2328) 2024-05-24 20:10:25 +05:30
Taran Vohra 30986528a7 bump @gitbook/api to 0.47 (#2327) 2024-05-24 19:07:54 +05:30
Steven H c70c2ddad8 Bump next-on-pages version to fix SSRF attack. (#2319) 2024-05-16 17:05:58 +01:00
28 changed files with 381 additions and 67 deletions
+6
View File
@@ -119,6 +119,12 @@ GitBook wouldn't be possible without these projects:
- [Tailwind CSS](https://tailwindcss.com/)
- [Framer Motion](https://www.npmjs.com/package/framer-motion)
## Contributors
<a href="https://github.com/gitbookIO/gitbook/graphs/contributors">
<img src="https://contrib.rocks/image?repo=gitbookIO/gitbook" />
</a>
## Legacy GitBook (Deprecated)
Our previous version of GitBook and it's CLI tool are now deprecated. You can still view the old repository and it's commits on this [branch](https://github.com/GitbookIO/gitbook/tree/legacy).
BIN
View File
Binary file not shown.
+4 -4
View File
@@ -11,7 +11,7 @@
"format": "prettier ./ --ignore-unknown --write",
"format:check": "prettier ./ --ignore-unknown --list-different",
"typecheck": "tsc --noEmit",
"unit": "bun test {src,packages}/**/*.test.ts",
"unit": "bun test {src,packages}",
"e2e": "playwright test",
"postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2"
},
@@ -20,7 +20,7 @@
],
"dependencies": {
"@geist-ui/icons": "^1.0.2",
"@gitbook/api": "^0.46.0",
"@gitbook/api": "^0.51.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@sentry/nextjs": "^7.94.1",
@@ -38,7 +38,7 @@
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"memoizee": "^0.4.15",
"next": "^14.1.3",
"next": "^14.2.4",
"next-themes": "^0.2.1",
"nuqs": "^1.15.4",
"object-hash": "^3.0.0",
@@ -64,7 +64,7 @@
},
"devDependencies": {
"@argos-ci/playwright": "^2.0.0",
"@cloudflare/next-on-pages": "^1.9.0",
"@cloudflare/next-on-pages": "^1.11.3",
"@cloudflare/workers-types": "^4.20231218.0",
"@playwright/test": "^1.42.1",
"@types/js-cookie": "^3.0.6",
+1 -1
View File
@@ -3,7 +3,7 @@
"exports": "./src/index.ts",
"dependencies": {
"classnames": "^2.5.1",
"@gitbook/api": "^0.46.0",
"@gitbook/api": "^0.51.0",
"assert-never": "^1.2.1"
},
"peerDependencies": {
@@ -47,6 +47,9 @@ export function OpenAPISchemaProperty(
? null
: getSchemaAlternatives(schema, new Set(circularRefs.keys()));
const shouldDisplayExample = (schema: OpenAPIV3.SchemaObject): boolean => {
return (typeof schema.example === 'string' || typeof schema.example === 'number' || typeof schema.example === 'boolean')
}
return (
<InteractiveSection
id={id}
@@ -92,6 +95,9 @@ export function OpenAPISchemaProperty(
className="openapi-schema-description"
/>
) : null}
{shouldDisplayExample(schema) ? (
<span className="openapi-schema-example">Example: <code>{JSON.stringify(schema.example)}</code></span>
) : null}
</div>
}
>
@@ -48,6 +48,7 @@ export function OpenAPISpec(props: { rawData: any; context: OpenAPIClientContext
// Description of the parameter is defined at the parameter level
// we use display it if the schema doesn't override it
description: parameter.description,
example: parameter.example,
...(noReference(parameter.schema) ?? {}),
},
required: parameter.required,
@@ -9,6 +9,7 @@ import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
import { getPagePath } from '@/lib/pages';
import { ContentRefContext } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
import { PageClientLayout } from './PageClientLayout';
import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } from '../../fetch';
@@ -22,8 +23,15 @@ export default async function Page(props: { params: PagePathParams }) {
const { params } = props;
const rawPathname = getPathnameParam(params);
const { contentTarget, space, customization, pages, page, document } =
await fetchPageData(params);
const {
content: contentPointer,
contentTarget,
space,
customization,
pages,
page,
document,
} = await fetchPageData(params);
const linksContext: PageHrefContext = {};
if (!page) {
@@ -62,6 +70,7 @@ export default async function Page(props: { params: PagePathParams }) {
<div className={tcls('flex', 'flex-row')}>
<PageBody
space={space}
contentPointer={contentPointer}
contentTarget={contentTarget}
customization={customization}
context={contentRefContext}
@@ -110,7 +119,7 @@ export async function generateMetadata({ params }: { params: PagePathParams }):
}
return {
title: [page.title, customization.title ?? space.title, parent?.title]
title: [page.title, getContentTitle(space, customization, parent)]
.filter(Boolean)
.join(' | '),
description: page.description ?? '',
+9 -1
View File
@@ -8,10 +8,13 @@ import { AdminToolbar } from '@/components/AdminToolbar';
import { CookiesToast } from '@/components/Cookies';
import { LoadIntegrations } from '@/components/Integrations';
import { SpaceLayout } from '@/components/SpaceLayout';
import { api } from '@/lib/api';
import { assetsDomain } from '@/lib/assets';
import { buildVersion } from '@/lib/build';
import { getContentSecurityPolicyNonce } from '@/lib/csp';
import { absoluteHref, baseUrl } from '@/lib/links';
import { shouldIndexSpace } from '@/lib/seo';
import { getContentTitle } from '@/lib/utils';
import { ClientContexts } from './ClientContexts';
import { RocketLoaderDetector } from './RocketLoaderDetector';
@@ -38,6 +41,11 @@ export default async function ContentLayout(props: { children: React.ReactNode }
scripts,
} = await fetchSpaceData();
ReactDOM.preconnect(api().endpoint);
if (assetsDomain) {
ReactDOM.preconnect(assetsDomain);
}
scripts.forEach(({ script }) => {
ReactDOM.preload(script, {
as: 'script',
@@ -104,7 +112,7 @@ export async function generateMetadata(): Promise<Metadata> {
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
return {
title: `${parent ? parent.title : customization.title ?? space.title}`,
title: getContentTitle(space, customization, parent),
generator: `GitBook (${buildVersion()})`,
metadataBase: new URL(baseUrl()),
icons: {
@@ -14,6 +14,7 @@ import {
} from '@/lib/api';
import { getEmojiForCode } from '@/lib/emojis';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
export const runtime = 'edge';
@@ -54,7 +55,7 @@ export async function GET(req: NextRequest) {
: space.visibility === ContentVisibility.InCollection && space.parent
? await getCollection(space.parent)
: null;
const contentTitle = parent?.title ?? customization.title ?? space.title;
const contentTitle = getContentTitle(space, customization, parent);
return new ImageResponse(
(
@@ -3,6 +3,8 @@ import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import React from 'react';
import { getContentTitle } from '@/lib/utils';
import { PageIdParams, fetchPageData } from '../../../../fetch';
export const runtime = 'edge';
@@ -31,7 +33,7 @@ export async function GET(req: NextRequest, { params }: { params: PageIdParams }
}}
>
<h2 tw="text-7xl font-bold tracking-tight text-left">
{parent?.title ?? customization.title ?? space.title}
{getContentTitle(space, customization, parent)}
</h2>
<div tw="flex flex-1">
<p tw="text-4xl">{page ? page.title : 'Not found'}</p>
+22 -4
View File
@@ -40,6 +40,7 @@ export function getContentPointer(): ContentPointer | SiteContentPointer {
if (siteId) {
const organizationId = headerSet.get('x-gitbook-content-organization');
const siteSpaceId = headerSet.get('x-gitbook-content-site-space');
const siteShareKey = headerSet.get('x-gitbook-content-site-share-key');
if (!organizationId) {
throw new Error('Missing site content headers');
}
@@ -48,6 +49,7 @@ export function getContentPointer(): ContentPointer | SiteContentPointer {
siteId,
spaceId,
siteSpaceId: siteSpaceId ?? undefined,
siteShareKey: siteShareKey ?? undefined,
organizationId,
revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined,
changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined,
@@ -71,7 +73,14 @@ export async function fetchSpaceData() {
const [{ space, contentTarget, pages, customization, scripts }, parentSite] = await Promise.all(
'siteId' in content
? [getCurrentSiteData(content), fetchParentSite(content.organizationId, content.siteId)]
? [
getCurrentSiteData(content),
fetchParentSite({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey: content.siteShareKey,
}),
]
: [getSpaceData(content)],
);
@@ -102,7 +111,11 @@ export async function fetchPageData(params: PagePathParams | PageIdParams) {
const page = await resolvePage(contentTarget, pages, params);
const [parent, document] = await Promise.all([
'siteId' in content
? fetchParentSite(content.organizationId, content.siteId)
? fetchParentSite({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey: content.siteShareKey,
})
: fetchParentCollection(space),
page?.page.documentId ? getDocument(space.id, page.page.documentId) : null,
]);
@@ -173,10 +186,15 @@ async function fetchParentCollection(space: Space) {
return { parent: collection, spaces };
}
async function fetchParentSite(organizationId: string, siteId: string) {
async function fetchParentSite(args: {
organizationId: string;
siteId: string;
siteShareKey: string | undefined;
}) {
const { organizationId, siteId, siteShareKey } = args;
const [site, siteSpaces] = await Promise.all([
getSite(organizationId, siteId),
getSiteSpaces(organizationId, siteId),
getSiteSpaces({ organizationId, siteId, siteShareKey }),
]);
const spaces: Record<string, Space> = {};
@@ -7,6 +7,7 @@ const PLAIN_HIGHLIGHTING_SPACES: string[] = [
'V9geAO9ITPi8WOYK5o0r',
'puRmcwVxGFtHph8IjXaf',
'e3jwbMOrr4RhKtZ9C0XL',
'ryjzVNizLfd6pYogN3dm',
];
/**
+3
View File
@@ -1,5 +1,6 @@
import * as gitbookAPI from '@gitbook/api';
import Script from 'next/script';
import ReactDOM from 'react-dom';
import { Card } from '@/components/primitives';
import { api } from '@/lib/api';
@@ -11,6 +12,8 @@ import { IntegrationBlock } from './Integration';
export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
const { block, context, ...otherProps } = props;
ReactDOM.preconnect('https://cdn.iframe.ly');
const { data: embed } = await (context.content
? api().spaces.getEmbedByUrlInSpace(context.content.spaceId, { url: block.data.url })
@@ -172,6 +172,10 @@
@apply prose-sm;
}
.openapi-schema-example {
@apply prose-sm mt-2 text-dark/10 dark:text-light/10;
}
/** Authentication */
.openapi-securities {
+2 -1
View File
@@ -11,6 +11,7 @@ import { HeaderMobileMenu } from '@/components/Header/HeaderMobileMenu';
import { Image } from '@/components/utils';
import { absoluteHref } from '@/lib/links';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
import { Link } from '../primitives';
@@ -145,7 +146,7 @@ function LogoFallback(props: HeaderLogoProps) {
: 'text-header-link',
)}
>
{parent ? parent.title : customization.title ?? space.title}
{getContentTitle(space, customization, parent)}
</h1>
</>
);
+22 -4
View File
@@ -9,7 +9,7 @@ import React from 'react';
import { getSpaceLanguage } from '@/intl/server';
import { t } from '@/intl/translate';
import { ContentTarget, api } from '@/lib/api';
import { ContentPointer, ContentTarget, SiteContentPointer, api } from '@/lib/api';
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
import { ContentRefContext, resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
@@ -25,6 +25,7 @@ import { DateRelative } from '../primitives';
export function PageBody(props: {
space: Space;
contentPointer: ContentPointer | SiteContentPointer;
contentTarget: ContentTarget;
customization: CustomizationSettings | SiteCustomizationSettings;
page: RevisionPageDocument;
@@ -32,13 +33,25 @@ export function PageBody(props: {
context: ContentRefContext;
withPageFeedback: boolean;
}) {
const { space, contentTarget, customization, context, page, document, withPageFeedback } =
props;
const {
space,
contentPointer,
contentTarget,
customization,
context,
page,
document,
withPageFeedback,
} = props;
const asFullWidth = document ? hasFullWidthBlock(document) : false;
const language = getSpaceLanguage(customization);
const updatedAt = page.updatedAt ?? page.createdAt;
const shouldHighlightCode = createHighlightingContext();
const sitePointer =
'siteId' in contentPointer
? { organizationId: contentPointer.organizationId, siteId: contentPointer.siteId }
: undefined;
return (
<>
@@ -129,7 +142,12 @@ export function PageBody(props: {
</div>
</main>
<React.Suspense fallback={null}>
<TrackPageView spaceId={space.id} pageId={page.id} apiHost={api().endpoint} />
<TrackPageView
sitePointer={sitePointer}
spaceId={space.id}
pageId={page.id}
apiHost={api().endpoint}
/>
</React.Suspense>
</>
);
+60 -16
View File
@@ -1,28 +1,66 @@
'use client';
import type { RequestSpaceTrackPageView } from '@gitbook/api';
import type { RequestSiteTrackPageView, RequestSpaceTrackPageView } from '@gitbook/api';
import cookies from 'js-cookie';
import * as React from 'react';
import { getVisitorId } from '@/lib/analytics';
import { SiteContentPointer } from '@/lib/api';
/**
* Track the page view for the current page to integrations.
*/
export function TrackPageView(props: {
apiHost: string;
sitePointer?: Pick<SiteContentPointer, 'siteId' | 'organizationId'>;
spaceId: string;
pageId: string | undefined;
}) {
const { apiHost, spaceId, pageId } = props;
const { apiHost, sitePointer, spaceId, pageId } = props;
React.useEffect(() => {
trackPageView(apiHost, spaceId, pageId);
}, [apiHost, spaceId, pageId]);
trackPageView({ apiHost, sitePointer, spaceId, pageId });
}, [apiHost, spaceId, pageId, sitePointer]);
return null;
}
async function sendSpaceTrackPageViewRequest(args: {
apiHost: string;
spaceId: string;
body: RequestSpaceTrackPageView;
}) {
const { apiHost, spaceId, body } = args;
const url = new URL(apiHost);
url.pathname = `/v1/spaces/${spaceId}/insights/track_view`;
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
}
async function sendSiteTrackPageViewRequest(args: {
apiHost: string;
sitePointer: Pick<SiteContentPointer, 'siteId' | 'organizationId'>;
body: RequestSiteTrackPageView;
}) {
const { apiHost, sitePointer, body } = args;
const url = new URL(apiHost);
url.pathname = `/v1/orgs/${sitePointer.organizationId}/sites/${sitePointer.siteId}/insights/track_view`;
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
}
let latestPageId: string | undefined | null = null;
/**
@@ -30,7 +68,13 @@ let latestPageId: string | undefined | null = null;
* We don't use the API client to avoid shipping 80kb of JS to the client.
* And instead use a simple fetch.
*/
async function trackPageView(apiHost: string, spaceId: string, pageId: string | undefined) {
async function trackPageView(args: {
apiHost: string;
sitePointer?: Pick<SiteContentPointer, 'siteId' | 'organizationId'>;
spaceId: string;
pageId: string | undefined;
}) {
const { apiHost, sitePointer, pageId, spaceId } = args;
if (pageId === latestPageId) {
// The hook can be called multiple times, we only want to track once.
return;
@@ -39,7 +83,7 @@ async function trackPageView(apiHost: string, spaceId: string, pageId: string |
latestPageId = pageId;
const visitorId = await getVisitorId();
const body: RequestSpaceTrackPageView = {
const sharedTrackedProps = {
url: window.location.href,
pageId,
visitor: {
@@ -51,17 +95,17 @@ async function trackPageView(apiHost: string, spaceId: string, pageId: string |
referrer: document.referrer,
};
const url = new URL(apiHost);
url.pathname = `/v1/spaces/${spaceId}/insights/track_view`;
try {
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
sitePointer
? await sendSiteTrackPageViewRequest({
apiHost,
sitePointer,
body: {
...sharedTrackedProps,
spaceId,
},
})
: await sendSpaceTrackPageViewRequest({ apiHost, spaceId, body: sharedTrackedProps });
} catch (error) {
console.error('Failed to track page view', error);
}
+5 -1
View File
@@ -76,7 +76,11 @@ export async function searchParentContent(
api.searchParentContent(parent.id, query),
parent.object === 'collection' ? api.getCollectionSpaces(parent.id) : null,
parent.object === 'site' && 'organizationId' in pointer
? api.getSiteSpaces(pointer.organizationId, parent.id)
? api.getSiteSpaces({
organizationId: pointer.organizationId,
siteId: parent.id,
siteShareKey: pointer.siteShareKey,
})
: null,
]);
+52
View File
@@ -0,0 +1,52 @@
export const de = {
locale: 'de',
powered_by_gitbook: 'Bereitgestellt von GitBook',
switch_to_dark_theme: 'Zum dunklen Modus wechseln',
switch_to_light_theme: 'Zum hellen Modus wechseln',
switch_to_system_theme: 'Zum Systemmodus wechseln',
search: 'Suche',
search_or_ask: 'Fragen oder Suchen',
search_input_placeholder: 'Inhalt durchsuchen',
search_ask_input_placeholder: 'Inhalt durchsuchen oder eine Frage stellen',
search_no_results: 'Keine Ergebnisse für "${1}".',
search_scope_space: 'Nur in ${1}',
search_scope_all: 'Alle Inhalte',
search_ask: 'Fragen "${1}"',
search_ask_sources: 'Quellen',
search_ask_no_answer: 'Es konnte keine Antwort auf Ihre Frage gefunden werden, versuchen Sie es mit einer anderen Frage.',
search_ask_error: 'Etwas ist schief gelaufen. Bitte versuchen Sie es später noch einmal.',
on_this_page: 'Auf dieser Seite',
next_page: 'Nächste',
previous_page: 'Vorherige',
page_last_modified: 'Zuletzt aktualisiert ${1}',
was_this_helpful: 'War das hilfreich?',
was_this_helpful_positive: 'Ja, das war es!',
was_this_helpful_neutral: 'Nicht sicher',
was_this_helpful_negative: 'Nein',
was_this_helpful_thank_you: 'Danke!',
annotation_button_label: 'Kommentar öffnen',
code_copied: 'Kopiert!',
code_copy: 'Kopieren',
table_of_contents_button_label: 'Inhaltsverzeichnis öffnen',
cookies_title: 'Cookies',
cookies_prompt:
'Diese Website verwendet Cookies, um ihre Dienste bereitzustellen und den Datenverkehr zu analysieren. Durch die Nutzung dieser Website akzeptieren Sie die ${1}.',
cookies_prompt_privacy: 'Datenschutzrichtlinie',
cookies_accept: 'Akzeptieren',
cookies_reject: 'Ablehnen',
cookies_close: 'Schließen',
edit_on_git: 'Bearbeiten auf ${1}',
notfound_title: 'Seite nicht gefunden',
notfound: 'Die gesuchte Seite existiert nicht.',
unexpected_error_title: 'Ein Fehler ist aufgetreten',
unexpected_error: 'Entschuldigung, ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.',
unexpected_error_retry: 'Erneut versuchen',
pdf_download: 'Als PDF exportieren',
pdf_goback: 'Zurück zum Inhalt',
pdf_print: 'Drucken oder als PDF speichern',
pdf_page_of: '${1} von ${2}',
pdf_mode_only_page: 'Nur diese Seite',
pdf_mode_all: 'Alle Seiten',
pdf_limit_reached: 'Das PDF konnte für ${1} Seiten nicht generiert werden, Generierung wurde bei ${2} gestoppt.',
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.',
};
+4
View File
@@ -1,9 +1,11 @@
import { CustomizationLocale } from '@gitbook/api';
import { de } from './de';
import { en } from './en';
import { es } from './es';
import { fr } from './fr';
import { ja } from './ja';
import { pt_br } from './pt-br';
import { TranslationLanguage } from './types';
import { zh } from './zh';
@@ -12,9 +14,11 @@ export * from './types';
export const languages: {
[locale in CustomizationLocale]: TranslationLanguage;
} = {
de,
en,
fr,
es,
zh,
ja,
'pt-br': pt_br,
};
+55
View File
@@ -0,0 +1,55 @@
export const pt_br = {
locale: 'pt-br',
powered_by_gitbook: 'Powered by GitBook',
switch_to_dark_theme: 'Mudar para modo escuro',
switch_to_light_theme: 'Mudar para modo claro',
switch_to_system_theme: 'Mudar para configuração do sistema',
search: 'Busca',
search_or_ask: 'Perguntar ou buscar',
search_input_placeholder: 'Buscar conteúdo',
search_ask_input_placeholder: 'Buscar conteúdo ou fazer uma pergunta',
search_no_results: 'Sem resultados para "${1}".',
search_scope_space: 'Somente em ${1}',
search_scope_all: 'Todo o conteúdo',
search_ask: 'Perguntar "${1}"',
search_ask_sources: 'Fontes',
search_ask_no_answer:
'Nenhuma resposta foi encontrada para sua pergunta, tente outra pergunta.',
search_ask_error: 'Algo deu errado. Por favor tente novamente mais tarde.',
on_this_page: 'Nesta página',
next_page: 'Próximo',
previous_page: 'Anterior',
page_last_modified: 'Atualizado ${1}',
was_this_helpful: 'Isto foi útil?',
was_this_helpful_positive: 'Sim, foi!',
was_this_helpful_neutral: 'Não sei',
was_this_helpful_negative: 'Não',
was_this_helpful_thank_you: 'Obrigado!',
annotation_button_label: 'Abrir anotação',
code_copied: 'Copiado!',
code_copy: 'Copiar',
table_of_contents_button_label: 'Abrir o índice',
cookies_title: 'Cookies',
cookies_prompt:
'Este site usa cookies para entregar suas funcionalidades e analizar o tráfego. Ao navegar neste site, você aceita a ${1}.',
cookies_prompt_privacy: 'política de privacidade',
cookies_accept: 'Aceitar',
cookies_reject: 'Rejeitar',
cookies_close: 'Fechar',
edit_on_git: 'Editar no ${1}',
notfound_title: 'Página não encontrada',
notfound: 'A página que você está procurando não existe.',
unexpected_error_title: 'Aconteceu um erro',
unexpected_error:
'Desculpe, aconteceu um erro inesperado. Por favor tente novamente mais tarde.',
unexpected_error_retry: ' Tentar novamente',
pdf_download: 'Exportar como PDF',
pdf_goback: 'Voltar ao conteúdo',
pdf_print: 'Imprimir ou salvar como PDF',
pdf_page_of: '${1} de ${2}',
pdf_mode_only_page: 'Somente esta página',
pdf_mode_all: 'Todas as páginas',
pdf_limit_reached:
'Não foi possível gerar o PDF para ${1} páginas, generation stopped at ${2}.',
pdf_limit_reached_continue: 'Extender com mais ${1} páginas.',
};
+37 -10
View File
@@ -47,6 +47,11 @@ export interface SiteContentPointer extends ContentPointer {
* ID of the siteSpace can be undefined when rendering in multi-id mode (for site previews)
*/
siteSpaceId: string | undefined;
/**
* Share key of the site that was used in lookup. Only set for `multi` and `multi-path` modes
* where an URL with the share-link key is involved in the lookup/resolution.
*/
siteShareKey: string | undefined;
}
/**
@@ -610,6 +615,7 @@ const getSiteSpaceCustomizationFromAPI = cache(
organizationId,
siteId,
siteSpaceId,
{},
{
signal: options.signal,
...noCacheFetchOptions,
@@ -633,10 +639,15 @@ const getSiteSpaceCustomizationFromAPI = cache(
const getSiteCustomizationFromAPI = cache(
'api.getSiteCustomizationById',
async (organizationId: string, siteId: string, options: CacheFunctionOptions) => {
const response = await api().orgs.getSiteCustomizationById(organizationId, siteId, {
signal: options.signal,
...noCacheFetchOptions,
});
const response = await api().orgs.getSiteCustomizationById(
organizationId,
siteId,
{},
{
signal: options.signal,
...noCacheFetchOptions,
},
);
return cacheResponse(response, {
revalidateBefore: 60 * 60,
tags: [
@@ -730,18 +741,34 @@ export const getSite = cache(
*/
export const getSiteSpaces = cache(
'api.getSiteSpaces',
async (organizationId: string, siteId: string, options: CacheFunctionOptions) => {
async (
args: {
organizationId: string;
siteId: string;
/** Site share key that can be used as context to resolve site space published urls */
siteShareKey: string | undefined;
},
options: CacheFunctionOptions,
) => {
const response = await getAll((params) =>
api().orgs.listSiteSpaces(organizationId, siteId, params, {
...noCacheFetchOptions,
signal: options.signal,
}),
api().orgs.listSiteSpaces(
args.organizationId,
args.siteId,
{
...params,
...(args.siteShareKey ? { shareKey: args.siteShareKey } : {}),
},
{
...noCacheFetchOptions,
signal: options.signal,
},
),
);
return cacheResponse(response, {
revalidateBefore: 60 * 60,
data: response.data.items.map((siteSpace) => siteSpace),
tags: [getAPICacheTag({ tag: 'site', site: siteId })],
tags: [getAPICacheTag({ tag: 'site', site: args.siteId })],
});
},
);
+19 -1
View File
@@ -7,21 +7,24 @@ describe('cache', () => {
let fn: CacheFunction<[string], string>;
let testId = 0;
let getTtl: () => number;
beforeEach(() => {
impl.mockClear();
testId += 1;
getTtl = () => 1000;
fn = cache(`cache-${testId}`, async (arg: string, options: CacheFunctionOptions) => {
await new Promise((resolve) => setTimeout(resolve, 20));
return {
data: impl(arg),
ttl: getTtl(),
};
});
});
it('should only execute once for same argument', async () => {
it('should only execute once for same argument if there is a ttl', async () => {
const result = await Promise.all([fn('a'), fn('a')]);
expect(result).toEqual(['test-a', 'test-a']);
@@ -34,6 +37,21 @@ describe('cache', () => {
expect(impl).toHaveBeenCalledTimes(1);
});
it('should execute multiple times for same argument if ttl is 0 or undefined', async () => {
getTtl = () => 0;
const result = await Promise.all([fn('a'), fn('a')]);
expect(result).toEqual(['test-a', 'test-a']);
expect(impl).toHaveBeenCalled();
expect(impl).toHaveBeenCalledTimes(1);
expect(await fn('a')).toEqual('test-a');
expect(impl).toHaveBeenCalledTimes(2);
});
it('should execute for different arguments', async () => {
const result = await Promise.all([fn('a'), fn('b')]);
+1 -1
View File
@@ -202,7 +202,7 @@ export function cache<Args extends any[], Result>(
if (savedEntry.meta.revalidatesAt && savedEntry.meta.revalidatesAt < Date.now()) {
// Revalidate in the background
waitUntil(revalidate(key, undefined, ...args));
await waitUntil(revalidate(key, undefined, ...args));
}
return savedEntry.data;
+5 -1
View File
@@ -26,9 +26,12 @@ export function parseCacheResponse(response: Response): {
const cacheControlHeader = response.headers.get('cache-control');
const cacheControl = cacheControlHeader ? parseCacheControl(cacheControlHeader) : null;
const cacheTagHeader = response.headers.get('x-gitbook-cache-tag');
const tags = !cacheTagHeader ? [] : cacheTagHeader.split(',');
const entry = {
ttl: 60 * 60 * 24,
tags: [],
tags,
};
if (cacheControl && cacheControl['max-age']) {
@@ -50,6 +53,7 @@ export function cacheResponse<Result, DefaultData = Result>(
return {
ttl: defaultEntry.ttl ?? parsed.ttl,
tags: [...(defaultEntry.tags ?? []), ...parsed.tags],
revalidateBefore: defaultEntry.revalidateBefore,
// @ts-ignore
data: defaultEntry.data ?? response.data,
};
+9 -15
View File
@@ -1,11 +1,12 @@
import { CacheBackend, CacheEntry } from './types';
import { CacheBackend } from './types';
import { NON_IMMUTABLE_LOCAL_CACHE_MAX_AGE_SECONDS, isCacheEntryImmutable } from './utils';
import { singleton } from '../async';
export const memoryCache: CacheBackend = {
name: 'memory',
replication: 'local',
async get(key) {
const memoryCache = getMemoryCache();
const memoryCache = await getMemoryCache();
const memoryEntry = memoryCache.get(key);
if (!memoryEntry) {
@@ -21,7 +22,7 @@ export const memoryCache: CacheBackend = {
return null;
},
async set(key, entry) {
const memoryCache = getMemoryCache();
const memoryCache = await getMemoryCache();
// When the entry is immutable, we can cache it for the entire duration.
// Else we cache it for a very short time.
const expiresAt =
@@ -41,11 +42,11 @@ export const memoryCache: CacheBackend = {
memoryCache.set(key, { ...entry, meta });
},
async del(keys) {
const memoryCache = getMemoryCache();
const memoryCache = await getMemoryCache();
keys.forEach((key) => memoryCache.delete(key));
},
async revalidateTags(tags) {
const memoryCache = getMemoryCache();
const memoryCache = await getMemoryCache();
const keys: string[] = [];
memoryCache.forEach((entry, key) => {
@@ -65,14 +66,7 @@ export const memoryCache: CacheBackend = {
/**
* With next-on-pages, the code seems to be isolated between the middleware and the handler.
* To share the cache between the two, we use a global variable.
* By using a singleton, we ensure that the cache is only created once and stored in the
* current request context.
*/
function getMemoryCache(): Map<string, CacheEntry> {
// @ts-ignore
if (!globalThis.gitbookMemoryCache) {
// @ts-ignore
globalThis.gitbookMemoryCache = new Map();
}
// @ts-ignore
return globalThis.gitbookMemoryCache;
}
const getMemoryCache = singleton(async () => new Map());
+25
View File
@@ -0,0 +1,25 @@
import {
Collection,
CustomizationSettings,
Site,
SiteCustomizationSettings,
Space,
} from '@gitbook/api';
/**
* Get the title to display for a content.
*/
export function getContentTitle(
space: Space,
customization: CustomizationSettings | SiteCustomizationSettings,
parent: Site | Collection | null,
) {
// When we are rendering a site, always give priority to the customization title first
// and then fallback to the site title
if (parent?.object === 'site') {
return customization.title ?? parent.title ?? space.title;
}
// Otherwise the legacy behavior is not changed to avoid regressions
return parent ? parent.title : customization.title ?? space.title;
}
+11 -2
View File
@@ -208,7 +208,11 @@ export async function middleware(request: NextRequest) {
if (resolved.siteSpace) {
headers.set('x-gitbook-content-site-space', resolved.siteSpace);
}
if (resolved.shareKey) {
headers.set('x-gitbook-content-site-share-key', resolved.shareKey);
}
}
if (resolved.revision) {
headers.set('x-gitbook-content-revision', resolved.revision);
}
@@ -261,7 +265,7 @@ export async function middleware(request: NextRequest) {
);
} else {
if (resolved.cacheMaxAge) {
const cacheControl = `public, max-age=60, s-maxage=${resolved.cacheMaxAge}, stale-while-revalidate=60, stale-if-error=0`;
const cacheControl = `public, max-age=0, s-maxage=${resolved.cacheMaxAge}, stale-if-error=0`;
if (
process.env.GITBOOK_OUTPUT_CACHE === 'true' &&
@@ -660,7 +664,12 @@ async function lookupSpaceByAPI(
cacheMaxAge: data.cacheMaxAge,
cacheTags: data.cacheTags,
...('site' in data
? { site: data.site, siteSpace: data.siteSpace, organization: data.organization }
? {
site: data.site,
siteSpace: data.siteSpace,
organization: data.organization,
shareKey: data.shareKey,
}
: {}),
} as PublishedContentWithCache;
});