diff --git a/docs/content/docs/configuration.mdx b/docs/content/docs/configuration.mdx index 05d5c1f..dc2c08c 100644 --- a/docs/content/docs/configuration.mdx +++ b/docs/content/docs/configuration.mdx @@ -300,13 +300,21 @@ links: | `label` | `string` | Menu item text | | `href` | `string` | URL. Absolute URLs open in a new tab; relative paths navigate in-app | -Every link is tagged with a `ref` query param carrying the full URL of the page it was clicked from, so the destination can attribute the visit. External links open with `noopener` (not `noreferrer`), so the destination also receives a `Referer` header — but under the browser default referrer policy that header carries only the origin, so `ref` is what identifies the specific page. +External links are tagged with UTM query params so the destination can attribute the visit, using the standard names every analytics tool recognises: + +| Param | Value | +|-------|-------| +| `utm_source` | Hostname of the docs site the click came from | +| `utm_medium` | `site.title`, slugified | +| `utm_content` | Path of the page the link was clicked from | ``` -https://support.example.com?ref=https%3A%2F%2Fdocs.example.com%2Fdocs%2Fguide +https://support.example.com?utm_source=docs.example.com&utm_medium=my-documentation&utm_content=%2Fdocs%2Fguide ``` -Any query string already on `href` is preserved. Non-web schemes (`mailto:`, `slack:`) are left untouched. +External links open with `noopener` (not `noreferrer`), so the destination also receives a `Referer` header — but under the browser default referrer policy that header carries only the origin, so `utm_content` is what identifies the specific page. + +Any query string already on `href` is preserved, and a `utm_*` param you set yourself is never overwritten. Relative paths are not tagged — they stay on the same origin, so the params would only register as a self-referral. Non-web schemes (`mailto:`, `slack:`) are left untouched. ### search diff --git a/packages/chronicle/src/components/ui/sidebar-links.tsx b/packages/chronicle/src/components/ui/sidebar-links.tsx index ef1cd52..b1182e8 100644 --- a/packages/chronicle/src/components/ui/sidebar-links.tsx +++ b/packages/chronicle/src/components/ui/sidebar-links.tsx @@ -8,6 +8,12 @@ import styles from './sidebar-links.module.css' const isExternal = (href: string) => /^[a-z][a-z0-9+.-]*:/i.test(href) +const slugify = (value: string) => + value + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + interface SidebarLinksProps { /** * `menu` — a `?` icon button opening a dropdown, for the desktop sidebar footer. @@ -23,19 +29,35 @@ export function SidebarLinks({ variant = 'menu' }: SidebarLinksProps) { if (!links.length) return null - // Tag the destination with the page the user clicked from. Schemes other than - // http(s) — mailto:, slack: — carry an opaque path, so they're left alone. + // Tag outbound links so the destination can attribute the visit. Relative + // paths are left untouched — they're same-origin, and tagging them would read + // as a self-referral. Schemes other than http(s) — mailto:, slack: — carry an + // opaque path, so they're left alone too. const open = (href: string) => { const url = new URL(href, window.location.origin) const isWeb = url.protocol === 'http:' || url.protocol === 'https:' - if (isWeb) url.searchParams.set('ref', window.location.href) - if (isExternal(href)) { - // `noopener` only — the destination should see Chronicle as the referrer. - window.open(isWeb ? url.toString() : href, '_blank', 'noopener') - } else { + if (!isExternal(href)) { navigate(`${url.pathname}${url.search}${url.hash}`) + return } + + if (isWeb) { + const params = { + utm_source: window.location.hostname, + utm_medium: slugify(config.site.title), + utm_content: window.location.pathname, + } + // Anything already set on `href` wins. + for (const [key, value] of Object.entries(params)) { + if (value && !url.searchParams.has(key)) { + url.searchParams.set(key, value) + } + } + } + + // `noopener` only — the destination should see Chronicle as the referrer. + window.open(isWeb ? url.toString() : href, '_blank', 'noopener') } if (variant === 'list') {