Skip to content
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
14 changes: 11 additions & 3 deletions docs/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 29 additions & 7 deletions packages/chronicle/src/components/ui/sidebar-links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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}`)
Comment thread
rsbh marked this conversation as resolved.
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') {
Expand Down
Loading