diff --git a/.changeset/plenty-laws-buy.md b/.changeset/plenty-laws-buy.md new file mode 100644 index 000000000..498a9ecb2 --- /dev/null +++ b/.changeset/plenty-laws-buy.md @@ -0,0 +1,5 @@ +--- +"gitbook": minor +--- + +Add support for site customization option to change how external links open. diff --git a/bun.lock b/bun.lock index 6fe99724d..096de72ae 100644 --- a/bun.lock +++ b/bun.lock @@ -286,7 +286,7 @@ "react-dom": "^19.0.0", }, "catalog": { - "@gitbook/api": "^0.122.0", + "@gitbook/api": "^0.123.0", }, "packages": { "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="], @@ -651,7 +651,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.122.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-zZ40xlJsfh4aed4zkEAcLtuSNRbPmyG9HZ0zqVXqeUiqsm6gm2uhdY0VDL+zYxcDXDqbPcA1fiubfivdfe3LtA=="], + "@gitbook/api": ["@gitbook/api@0.123.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-BszoIk4H/wWb0jnGSJPSSXSAO8tV4B4/LHwl5ygfERHqk9rOGpM/U7+yOlmbX/tnm30vE+MQ3QQ5i8hN8wVk1w=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], diff --git a/package.json b/package.json index 9e82307ae..7423660b4 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "workspaces": { "packages": ["packages/*"], "catalog": { - "@gitbook/api": "^0.122.0" + "@gitbook/api": "^0.123.0" } }, "patchedDependencies": { diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index 937b776d7..fdbd2a342 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -16,6 +16,7 @@ import { CustomizationThemeMode, type CustomizationThemedColor, type SiteCustomizationSettings, + SiteExternalLinksTarget, } from '@gitbook/api'; import { type BrowserContext, type Page, type Response, expect, test } from '@playwright/test'; import deepMerge from 'deepmerge'; @@ -317,6 +318,9 @@ export function getCustomizationURL(partial: DeepPartial - {children} + + {children} + ); } diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx index f81d1c2bd..e23bfddf4 100644 --- a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx +++ b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx @@ -54,6 +54,7 @@ export async function SiteLayout(props: { forcedTheme ?? (customization.themes.toggeable ? undefined : customization.themes.default) } + externalLinksTarget={customization.externalLinks.target} > & classNames?: DesignTokenName[]; }; +/** + * Context to configure the default behavior of links. + */ +export const LinkSettingsContext = React.createContext<{ + externalLinksTarget: SiteExternalLinksTarget; +}>({ + externalLinksTarget: SiteExternalLinksTarget.Self, +}); + /** * Low-level Link component that handles navigation to external urls. * It does not contain any styling. @@ -39,6 +49,7 @@ export const Link = React.forwardRef(function Link( ref: React.Ref ) { const { href, prefetch, children, insights, classNames, className, ...domProps } = props; + const { externalLinksTarget } = React.useContext(LinkSettingsContext); const trackEvent = useTrackEvent(); const forwardedClassNames = useClassnames(classNames || []); @@ -53,9 +64,14 @@ export const Link = React.forwardRef(function Link( }); } - // When the page is embedded in an iframe, for security reasons other urls cannot be opened. - // In this case, we open the link in a new tab. - if (window.self !== window.top && isExternalLink(href, window.location.origin)) { + if ( + isExternalLink(href, window.location.origin) && + // When the page is embedded in an iframe, for security reasons other urls cannot be opened. + // In this case, we open the link in a new tab. + (window.self !== window.top || + // If the site is configured to open links in a new tab + externalLinksTarget === SiteExternalLinksTarget.Blank) + ) { event.preventDefault(); window.open(href, '_blank'); } @@ -73,6 +89,9 @@ export const Link = React.forwardRef(function Link( {...domProps} href={href} onClick={onClick} + {...(externalLinksTarget === SiteExternalLinksTarget.Blank + ? { target: '_blank', rel: 'noopener noreferrer' } + : {})} > {children} diff --git a/packages/gitbook/src/lib/utils.ts b/packages/gitbook/src/lib/utils.ts index d7a98610d..1f139874e 100644 --- a/packages/gitbook/src/lib/utils.ts +++ b/packages/gitbook/src/lib/utils.ts @@ -51,6 +51,9 @@ export function defaultCustomization(): api.SiteCustomizationSettings { aiSearch: { enabled: true, }, + externalLinks: { + target: api.SiteExternalLinksTarget.Self, + }, advancedCustomization: { enabled: true, },