Skip to content

Support for site customization option to change how external links open #3372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plenty-laws-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gitbook": minor
---

Add support for site customization option to change how external links open.
4 changes: 2 additions & 2 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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=="],
Expand Down Expand Up @@ -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"],

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "^0.122.0"
"@gitbook/api": "^0.123.0"
}
},
"patchedDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions packages/gitbook/e2e/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -317,6 +318,9 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
aiSearch: {
enabled: true,
},
externalLinks: {
target: SiteExternalLinksTarget.Self,
},
advancedCustomization: {
enabled: true,
},
Expand Down
10 changes: 7 additions & 3 deletions packages/gitbook/src/components/SiteLayout/ClientContexts.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
'use client';

import type { CustomizationThemeMode } from '@gitbook/api';
import type { CustomizationThemeMode, SiteExternalLinksTarget } from '@gitbook/api';
import { ThemeProvider } from 'next-themes';
import type React from 'react';
import { LinkSettingsContext } from '../primitives';

export function ClientContexts(props: {
nonce?: string;
forcedTheme: CustomizationThemeMode | undefined;
externalLinksTarget: SiteExternalLinksTarget;
children: React.ReactNode;
}) {
const { children, forcedTheme } = props;
const { children, forcedTheme, externalLinksTarget } = props;

/**
* A bug in ThemeProvider is causing the nonce to be included incorrectly
Expand All @@ -22,7 +24,9 @@ export function ClientContexts(props: {

return (
<ThemeProvider nonce={nonce} attribute="class" enableSystem forcedTheme={forcedTheme}>
{children}
<LinkSettingsContext.Provider value={{ externalLinksTarget }}>
{children}
</LinkSettingsContext.Provider>
</ThemeProvider>
);
}
1 change: 1 addition & 0 deletions packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export async function SiteLayout(props: {
forcedTheme ??
(customization.themes.toggeable ? undefined : customization.themes.default)
}
externalLinksTarget={customization.externalLinks.target}
>
<SpaceLayout
context={context}
Expand Down
25 changes: 22 additions & 3 deletions packages/gitbook/src/components/primitives/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import NextLink, { type LinkProps as NextLinkProps } from 'next/link';
import React from 'react';

import { tcls } from '@/lib/tailwind';
import { SiteExternalLinksTarget } from '@gitbook/api';
import { type TrackEventInput, useTrackEvent } from '../Insights';
import { type DesignTokenName, useClassnames } from './StyleProvider';

Expand All @@ -30,6 +31,15 @@ export type LinkProps = Omit<BaseLinkProps, 'href'> &
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.
Expand All @@ -39,6 +49,7 @@ export const Link = React.forwardRef(function Link(
ref: React.Ref<HTMLAnchorElement>
) {
const { href, prefetch, children, insights, classNames, className, ...domProps } = props;
const { externalLinksTarget } = React.useContext(LinkSettingsContext);
const trackEvent = useTrackEvent();
const forwardedClassNames = useClassnames(classNames || []);

Expand All @@ -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');
}
Expand All @@ -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}
</a>
Expand Down
3 changes: 3 additions & 0 deletions packages/gitbook/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export function defaultCustomization(): api.SiteCustomizationSettings {
aiSearch: {
enabled: true,
},
externalLinks: {
target: api.SiteExternalLinksTarget.Self,
},
advancedCustomization: {
enabled: true,
},
Expand Down