From db9fb1741d6b263a6d0fda8259d71ff1a69765ee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 00:24:21 +0000 Subject: [PATCH] Distinguish link types in inline link popover (RND-12165) Same-page anchor links now show the target section (humanized anchor) instead of the current page's title, and external links show their domain as the heading with the full URL as sub-text. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016W3fKsXSvRWHJFUe8v5YAH --- .changeset/inline-link-tooltip-distinguish.md | 5 ++ .../DocumentView/InlineLink/InlineLink.tsx | 46 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 .changeset/inline-link-tooltip-distinguish.md diff --git a/.changeset/inline-link-tooltip-distinguish.md b/.changeset/inline-link-tooltip-distinguish.md new file mode 100644 index 000000000..3d1e9a0a1 --- /dev/null +++ b/.changeset/inline-link-tooltip-distinguish.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Improve the inline link hover popover: same-page anchor links now read as an on-page jump to a section instead of showing the current page's title, and external links show their domain with the full URL as sub-text. diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx index 1623dcbb2..40a976a7a 100644 --- a/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx @@ -119,10 +119,11 @@ function InlineLinkTooltipWrapper(props: { }) { const { inline, language, resolved, children } = props; + const ref = inline.data.ref; let breadcrumbs = resolved.ancestors ?? []; const isMailto = resolved.href.startsWith('mailto:'); - const isExternal = inline.data.ref.kind === 'url'; - const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined; + const isExternal = ref.kind === 'url'; + const isSamePage = ref.kind === 'anchor' && ref.page === undefined; if (isMailto) { resolved.text = resolved.text.split('mailto:')[1] ?? resolved.text; @@ -137,6 +138,13 @@ function InlineLinkTooltipWrapper(props: { label: tString(language, 'link_tooltip_external_link'), }, ]; + // Present the destination domain as the heading, keeping the full URL as sub-text, + // instead of surfacing the raw URL as if it were a page title. + const hostname = getHostname(resolved.href); + if (hostname) { + resolved.subText = resolved.text; + resolved.text = hostname; + } } else if (isSamePage) { breadcrumbs = [ { @@ -144,6 +152,16 @@ function InlineLinkTooltipWrapper(props: { icon: , }, ]; + // A same-page anchor is a jump within the current page, not a link to it, + // so avoid showing the current page's title as the destination. Fall back to the + // humanized anchor as the section label (the true section title isn't resolved here, + // as resolveAnchorText is intentionally disabled for performance). + if (ref.kind === 'anchor') { + const sectionLabel = humanizeAnchor(ref.anchor); + if (sectionLabel) { + resolved.text = sectionLabel; + } + } resolved.subText = undefined; } @@ -161,3 +179,27 @@ function InlineLinkTooltipWrapper(props: { ); } + +/** + * Extract the display hostname of an external URL, without the leading `www.`. + * Returns null if the URL cannot be parsed. + */ +function getHostname(href: string): string | null { + try { + return new URL(href).hostname.replace(/^www\./, ''); + } catch { + return null; + } +} + +/** + * Turn an anchor id (e.g. `installation-guide`) into a readable section label. + * Returns null when the anchor is empty. + */ +function humanizeAnchor(anchor: string): string | null { + const words = anchor.trim().replace(/[-_]+/g, ' ').trim(); + if (!words) { + return null; + } + return words.charAt(0).toUpperCase() + words.slice(1); +}