diff --git a/frontend/src/AnalyticsView.vue b/frontend/src/AnalyticsView.vue
index c8ac619..3ae5772 100644
--- a/frontend/src/AnalyticsView.vue
+++ b/frontend/src/AnalyticsView.vue
@@ -482,10 +482,19 @@ const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], c
}
/* The referer badge outgrows the 8rem trail-link cap (it carries the UTM
- summary too); keep the inline-flex layout from the component. */
-.visit-table .trail a.referer-badge {
+ summary too); keep the inline-flex layout from the component. The
+ generic trail-link rule above would otherwise clip the badge (overflow:
+ hidden, hiding the absolutely positioned favicon) and cap its inner
+ link — the link is display: contents, so its parts lay out as badge
+ flex items. */
+.visit-table .trail .referer-badge {
display: inline-flex;
max-width: 100%;
+ overflow: visible;
+}
+
+.visit-table .trail .referer-badge a.badge-link {
+ display: contents;
}
/* Same flag chip as the visitor cells (VisitorCell.vue); the flags here
diff --git a/frontend/src/RefererBadge.vue b/frontend/src/RefererBadge.vue
index 4221f0d..e3af123 100644
--- a/frontend/src/RefererBadge.vue
+++ b/frontend/src/RefererBadge.vue
@@ -1,11 +1,15 @@
-
-
- {{ badge.label }}
+
+
+
+ {{ badge.label }}
+
+
+
+ {{ badge.label }}
+
{{ badge.utm }}
-
+
diff --git a/frontend/src/analytics/format.js b/frontend/src/analytics/format.js
index 2a7050a..b762332 100644
--- a/frontend/src/analytics/format.js
+++ b/frontend/src/analytics/format.js
@@ -180,22 +180,45 @@ function stepOf(path, titles) {
/**
* Badge data combining a visit's/crawler's external referer origin with the
* visit's UTM tags: the origin as the badge link/label (the favicon is
- * looked up by origin in the component), the known UTM values as a short
- * inline summary, and a one-fact-per-line tooltip — the full origin URL on
- * the first line, then every ``utm_*=value`` pair. Null when there is no
- * external referer and no UTM tag (a plain direct visit).
+ * looked up by origin in the component), a compact UTM summary (the few
+ * most informative values) as ``utm`` with the full ``utm_*=value`` list
+ * as ``utmCopy`` for click-to-copy, and a one-fact-per-line tooltip — the
+ * full origin URL on the first line, then every ``utm_*=value`` pair.
+ * Null when there is no external referer and no UTM tag (a plain direct
+ * visit).
*/
function refererBadgeOf(referer, titles, utmTags = {}) {
const step = stepOf(referer, titles)
const external = step?.external ? step : null
- const known = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
- const utm = known.map((k) => utmTags[k]).filter(Boolean).join(' · ')
+ // Compact UTM summary, in display order source, campaign, content, term:
+ // source only when no referer is known (it just repeats where the visitor
+ // came from), content only as a stand-in when there is no term. The
+ // remaining tags (medium and any nonstandard utm_*) fill in only when
+ // fewer than three of these more useful items exist — and never when a
+ // term is present (the term alone says enough). The tooltip keeps
+ // every tag, one pair per line.
+ const useful = []
+ if (utmTags.utm_source && !referer) useful.push(utmTags.utm_source)
+ if (utmTags.utm_campaign) useful.push(utmTags.utm_campaign)
+ if (utmTags.utm_content && !utmTags.utm_term) useful.push(utmTags.utm_content)
+ if (utmTags.utm_term) useful.push(utmTags.utm_term)
+ const rest = useful.length < 3 && !utmTags.utm_term
+ ? Object.keys(utmTags)
+ .filter((k) => !['utm_source', 'utm_campaign', 'utm_content', 'utm_term'].includes(k))
+ .map((k) => utmTags[k])
+ .filter(Boolean)
+ : []
+ const utm = [...useful, ...rest].join(' · ')
if (!external && !utm) return null
+ const utmCopy = Object.entries(utmTags)
+ .map(([k, value]) => `${k}=${value}`)
+ .join('\n')
return {
href: external?.origin || '',
label: external?.slug || '',
origin: external?.origin || '',
utm,
+ utmCopy,
title: [
...(external ? [external.origin] : []),
...Object.entries(utmTags).map(([k, value]) => `${k}=${value}`),
diff --git a/frontend/src/assets/pagerite.css b/frontend/src/assets/pagerite.css
index 4b4f4e1..1f06f46 100644
--- a/frontend/src/assets/pagerite.css
+++ b/frontend/src/assets/pagerite.css
@@ -26,14 +26,15 @@
/* Selection fill for page text and the CodeMirror editors; themes
override when the accent tint clashes with accent-colored text. */
--selection-bg: color-mix(var(--accent) 30%, transparent);
- /* Referer-badge chip in the analytics viewer: a fixed neutral palette,
+ /* Referer-badge chip in the analytics viewer: a translucent neutral wash,
deliberately NOT themed — the chip sits behind transparent favicons, so
black-on-transparent and white-on-transparent glyphs must both stay
- legible on every theme (dark greys kill black glyphs, pure white kills
- white ones); its text likewise stays dark on any theme. */
- --badge-bg: #c9d1d9;
- --badge-text: #1f2328;
- --badge-muted: #59636e;
+ legible on every theme (a slight whitening keeps black glyphs readable
+ on dark bars without a glaring solid-white chip). Being translucent, it
+ takes the page's tone, so its text follows the theme's colors. */
+ --badge-bg: #aaaaaa44;
+ --badge-text: var(--text);
+ --badge-muted: var(--muted);
/* Code highlighting palette, consumed by pygments.css: complete light and
dark sets (background included), resolved by light-dark() from the
used color-scheme. A theme picks a set simply by declaring