analytics viewer: referer + UTM as one neutral-grey badge
Favicon flush to the badge's left edge, then the referer host and UTM summary in one link, with a single newline-formatted tooltip (origin, then each utm_* pair). Fixed --badge-bg/--badge-text/--badge-muted palette (defined in pagerite.css, deliberately unthemed) so transparent favicons stay legible in both light and dark themes; own-article trail links remain plain, lifting referer info visually apart from them. Replaces the outlined utm-tag pill.
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
|||||||
formatVisitRows,
|
formatVisitRows,
|
||||||
} from './analytics/format.js'
|
} from './analytics/format.js'
|
||||||
import TrailLink from './TrailLink.vue'
|
import TrailLink from './TrailLink.vue'
|
||||||
|
import RefererBadge from './RefererBadge.vue'
|
||||||
import VisitorCell from './VisitorCell.vue'
|
import VisitorCell from './VisitorCell.vue'
|
||||||
import TransitionGraph from './TransitionGraph.vue'
|
import TransitionGraph from './TransitionGraph.vue'
|
||||||
import VisitorCharts from './VisitorCharts.vue'
|
import VisitorCharts from './VisitorCharts.vue'
|
||||||
@@ -215,8 +216,7 @@ const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], c
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(v, i) in visitRows" :key="i">
|
<tr v-for="(v, i) in visitRows" :key="i">
|
||||||
<td class="trail">
|
<td class="trail">
|
||||||
<TrailLink v-if="v.refererStep" :step="v.refererStep" :favicons="favicons" @close="$emit('close')" />
|
<RefererBadge v-if="v.refererBadge" :badge="v.refererBadge" :favicons="favicons" />
|
||||||
<span v-if="v.utm && v.utm !== '—'" class="utm-tag small muted" :title="v.utmTitle">{{ v.utm }}</span>
|
|
||||||
<span v-if="v.rowFlag" class="flag" v-html="v.rowFlag" :title="v.rowFlagTitle"></span>
|
<span v-if="v.rowFlag" class="flag" v-html="v.rowFlag" :title="v.rowFlagTitle"></span>
|
||||||
<TrailLink v-for="(s, si) in v.trail" :key="si" :step="s" :favicons="favicons" :flags="s.langFlags" @close="$emit('close')" />
|
<TrailLink v-for="(s, si) in v.trail" :key="si" :step="s" :favicons="favicons" :flags="s.langFlags" @close="$emit('close')" />
|
||||||
</td>
|
</td>
|
||||||
@@ -253,7 +253,7 @@ const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], c
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(c, i) in crawlerRows" :key="i">
|
<tr v-for="(c, i) in crawlerRows" :key="i">
|
||||||
<td class="trail">
|
<td class="trail">
|
||||||
<TrailLink v-if="c.refererStep" :step="c.refererStep" :favicons="favicons" @close="$emit('close')" />
|
<RefererBadge v-if="c.refererBadge" :badge="c.refererBadge" :favicons="favicons" />
|
||||||
<TrailLink v-for="(s, si) in c.pages" :key="si" :step="s" :count="s.count" @close="$emit('close')" />
|
<TrailLink v-for="(s, si) in c.pages" :key="si" :step="s" :count="s.count" @close="$emit('close')" />
|
||||||
<span v-for="(f, fi) in c.readFlags" :key="fi" class="flag" v-html="f.flag" :title="f.name"></span>
|
<span v-for="(f, fi) in c.readFlags" :key="fi" class="flag" v-html="f.flag" :title="f.name"></span>
|
||||||
</td>
|
</td>
|
||||||
@@ -480,16 +480,11 @@ const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], c
|
|||||||
color: var(--error, #c00);
|
color: var(--error, #c00);
|
||||||
}
|
}
|
||||||
|
|
||||||
.visit-table .utm-tag {
|
/* The referer badge outgrows the 8rem trail-link cap (it carries the UTM
|
||||||
display: inline-block;
|
summary too); keep the inline-flex layout from the component. */
|
||||||
|
.visit-table .trail a.referer-badge {
|
||||||
|
display: inline-flex;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 0.05rem 0.4rem;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Same flag chip as the visitor cells (VisitorCell.vue); the flags here
|
/* Same flag chip as the visitor cells (VisitorCell.vue); the flags here
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup>
|
||||||
|
// Referer + UTM as one badge in the analytics visit/crawler tables: the
|
||||||
|
// referer's favicon flush on the left, its host as the link text, then the
|
||||||
|
// UTM summary smaller/muted inside the same badge. The whole badge links to
|
||||||
|
// the referer origin (external, new tab) and carries a single one-fact-
|
||||||
|
// per-line tooltip (badge.title: origin, then each utm pair) — no titles on
|
||||||
|
// the inner elements. Referers are external, so there is no close event.
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
badge: { type: Object, required: true },
|
||||||
|
favicons: { type: Object, default: null },
|
||||||
|
})
|
||||||
|
|
||||||
|
const favicon = computed(() => (props.badge.origin ? props.favicons?.[props.badge.origin] : null))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a class="referer-badge"
|
||||||
|
:href="badge.href || undefined"
|
||||||
|
:title="badge.title"
|
||||||
|
:target="badge.href ? '_blank' : undefined"
|
||||||
|
:rel="badge.href ? 'noopener' : undefined">
|
||||||
|
<img v-if="favicon" class="badge-favicon" :src="favicon" alt="" />
|
||||||
|
<span v-if="badge.label">{{ badge.label }}</span>
|
||||||
|
<small v-if="badge.utm" class="small">{{ badge.utm }}</small>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Browser-chrome chip on a fixed neutral palette (--badge-* in
|
||||||
|
pagerite.css, deliberately unthemed): black-on-transparent and
|
||||||
|
white-on-transparent favicons both stay legible on it, and the text is
|
||||||
|
always dark regardless of the theme's link/text colors. Colors go on the
|
||||||
|
inner elements, so the theme's a / a:hover color rules (which target the
|
||||||
|
anchor) cannot cascade in. */
|
||||||
|
.referer-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35em;
|
||||||
|
padding-right: 0.45em;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: var(--badge-bg);
|
||||||
|
color: var(--badge-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flush with the badge's top/left/bottom edges: a full-height square (the
|
||||||
|
badge has no padding on those sides), corners clipped by the badge's
|
||||||
|
overflow: hidden border-radius. */
|
||||||
|
.badge-favicon {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
object-fit: cover;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referer-badge > span,
|
||||||
|
.referer-badge > small {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referer-badge > span { color: var(--badge-text); }
|
||||||
|
.referer-badge > small { color: var(--badge-muted); }
|
||||||
|
</style>
|
||||||
@@ -177,6 +177,32 @@ function stepOf(path, titles) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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).
|
||||||
|
*/
|
||||||
|
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(' · ')
|
||||||
|
if (!external && !utm) return null
|
||||||
|
return {
|
||||||
|
href: external?.origin || '',
|
||||||
|
label: external?.slug || '',
|
||||||
|
origin: external?.origin || '',
|
||||||
|
utm,
|
||||||
|
title: [
|
||||||
|
...(external ? [external.origin] : []),
|
||||||
|
...Object.entries(utmTags).map(([k, value]) => `${k}=${value}`),
|
||||||
|
].join('\n'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Human-readable relative timestamp. Adapted from cista-storage: uses
|
* Human-readable relative timestamp. Adapted from cista-storage: uses
|
||||||
* ``Intl.RelativeTimeFormat`` for short intervals and a compact date for
|
* ``Intl.RelativeTimeFormat`` for short intervals and a compact date for
|
||||||
@@ -369,9 +395,10 @@ export function mainDomain(host, limit = 24) {
|
|||||||
/**
|
/**
|
||||||
* Group raw crawler hits by client hash and format each group as a row showing
|
* Group raw crawler hits by client hash and format each group as a row showing
|
||||||
* every internal page that crawler visited. Rows are sorted by most recent hit
|
* every internal page that crawler visited. Rows are sorted by most recent hit
|
||||||
* first, with total hits as a tie-breaker. The group's ``refererStep`` is the
|
* first, with total hits as a tie-breaker. The group's ``refererBadge`` is
|
||||||
* latest external referer seen for the crawler — spiders often advertise
|
* the latest external referer seen for the crawler — spiders often advertise
|
||||||
* their own site there — rendered with its favicon like visit referers.
|
* their own site there — rendered as a badge with its favicon like visit
|
||||||
|
* referers.
|
||||||
* ``clients`` maps client hashes to client records.
|
* ``clients`` maps client hashes to client records.
|
||||||
*/
|
*/
|
||||||
export function formatCrawlerRows(crawlers, clients, pageTree, now = Date.now(), site = { multilingual: false, primaryLang: '' }) {
|
export function formatCrawlerRows(crawlers, clients, pageTree, now = Date.now(), site = { multilingual: false, primaryLang: '' }) {
|
||||||
@@ -420,7 +447,7 @@ export function formatCrawlerRows(crawlers, clients, pageTree, now = Date.now(),
|
|||||||
lastSeen: formatWhen(g.lastStart, now),
|
lastSeen: formatWhen(g.lastStart, now),
|
||||||
lastSeenIso: formatWhenIso(g.lastStart),
|
lastSeenIso: formatWhenIso(g.lastStart),
|
||||||
lastSeenLocal: formatWhenLocal(g.lastStart),
|
lastSeenLocal: formatWhenLocal(g.lastStart),
|
||||||
refererStep: stepOf(g.referer, titles),
|
refererBadge: refererBadgeOf(g.referer, titles),
|
||||||
pages: [...g.pages.entries()]
|
pages: [...g.pages.entries()]
|
||||||
.sort((a, b) => b[1].count - a[1].count)
|
.sort((a, b) => b[1].count - a[1].count)
|
||||||
.map(([path, info]) => ({ ...stepOf(path, titles), count: info.count, status: info.status })),
|
.map(([path, info]) => ({ ...stepOf(path, titles), count: info.count, status: info.status })),
|
||||||
@@ -559,9 +586,10 @@ export function formatAbuseRows(abuse, clients, pageTree, now = Date.now()) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Format raw visit records as rows for a technical table. Returns objects
|
* Format raw visit records as rows for a technical table. Returns objects
|
||||||
* with display strings; missing values become "—". ``trail`` starts with the
|
* with display strings; missing values become "—". The external referer
|
||||||
* external referer (when present), then the entry page and any further internal
|
* (when present) and the UTM tags ride along as ``refererBadge``; ``trail``
|
||||||
* pages or external exit origins; consecutive views of the same page (e.g. a
|
* holds the entry page and any further internal pages or external exit
|
||||||
|
* origins; consecutive views of the same page (e.g. a
|
||||||
* language switch re-view) merge into one step that keeps the
|
* language switch re-view) merge into one step that keeps the
|
||||||
* consecutive-distinct rendered languages, summed read time, and the latest
|
* consecutive-distinct rendered languages, summed read time, and the latest
|
||||||
* status. On multilingual sites the rendered languages surface as flag
|
* status. On multilingual sites the rendered languages surface as flag
|
||||||
@@ -599,12 +627,6 @@ export function formatVisitRows(visits, clients, pageTree, now = Date.now(), sit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const distinctLangs = new Set(trail.flatMap((s) => s.langs))
|
const distinctLangs = new Set(trail.flatMap((s) => s.langs))
|
||||||
const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
|
|
||||||
const utmValues = utmKeys.map((k) => (v.utm || {})[k]).filter(Boolean)
|
|
||||||
const utm = utmValues.length ? utmValues.join(' · ') : ''
|
|
||||||
const utmTitle = Object.entries(v.utm || {})
|
|
||||||
.map(([k, value]) => `${k}=${value}`)
|
|
||||||
.join(', ')
|
|
||||||
const dash = (s) => (s || '—')
|
const dash = (s) => (s || '—')
|
||||||
const host = client.host || ''
|
const host = client.host || ''
|
||||||
const isHost = !!host
|
const isHost = !!host
|
||||||
@@ -614,8 +636,7 @@ export function formatVisitRows(visits, clients, pageTree, now = Date.now(), sit
|
|||||||
lastSeenLocal: formatWhenLocal(v.start),
|
lastSeenLocal: formatWhenLocal(v.start),
|
||||||
langDisplay: formatLang(client.lang),
|
langDisplay: formatLang(client.lang),
|
||||||
trail,
|
trail,
|
||||||
refererStep: stepOf(v.referer, titles),
|
refererBadge: refererBadgeOf(v.referer, titles, v.utm),
|
||||||
referer: dash(v.referer),
|
|
||||||
ip: client.ip || '',
|
ip: client.ip || '',
|
||||||
ipDisplay: isHost ? mainDomain(host) : hostIP(client.ip) || client.ip || '—',
|
ipDisplay: isHost ? mainDomain(host) : hostIP(client.ip) || client.ip || '—',
|
||||||
isHost,
|
isHost,
|
||||||
@@ -625,8 +646,6 @@ export function formatVisitRows(visits, clients, pageTree, now = Date.now(), sit
|
|||||||
ua: client.uarite?.pretty || client.ua || '—',
|
ua: client.uarite?.pretty || client.ua || '—',
|
||||||
uaRaw: client.ua || '',
|
uaRaw: client.ua || '',
|
||||||
uaUrl: client.uarite?.url || '',
|
uaUrl: client.uarite?.url || '',
|
||||||
utm: utm || '—',
|
|
||||||
utmTitle,
|
|
||||||
}
|
}
|
||||||
if (site.multilingual && distinctLangs.size) {
|
if (site.multilingual && distinctLangs.size) {
|
||||||
if (distinctLangs.size === 1) {
|
if (distinctLangs.size === 1) {
|
||||||
|
|||||||
@@ -26,6 +26,14 @@
|
|||||||
/* Selection fill for page text and the CodeMirror editors; themes
|
/* Selection fill for page text and the CodeMirror editors; themes
|
||||||
override when the accent tint clashes with accent-colored text. */
|
override when the accent tint clashes with accent-colored text. */
|
||||||
--selection-bg: color-mix(var(--accent) 30%, transparent);
|
--selection-bg: color-mix(var(--accent) 30%, transparent);
|
||||||
|
/* Referer-badge chip in the analytics viewer: a fixed neutral palette,
|
||||||
|
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;
|
||||||
/* Code highlighting palette, consumed by pygments.css: complete light and
|
/* Code highlighting palette, consumed by pygments.css: complete light and
|
||||||
dark sets (background included), resolved by light-dark() from the
|
dark sets (background included), resolved by light-dark() from the
|
||||||
used color-scheme. A theme picks a set simply by declaring
|
used color-scheme. A theme picks a set simply by declaring
|
||||||
|
|||||||
Reference in New Issue
Block a user