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,
|
||||
} from './analytics/format.js'
|
||||
import TrailLink from './TrailLink.vue'
|
||||
import RefererBadge from './RefererBadge.vue'
|
||||
import VisitorCell from './VisitorCell.vue'
|
||||
import TransitionGraph from './TransitionGraph.vue'
|
||||
import VisitorCharts from './VisitorCharts.vue'
|
||||
@@ -215,8 +216,7 @@ const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], c
|
||||
<tbody>
|
||||
<tr v-for="(v, i) in visitRows" :key="i">
|
||||
<td class="trail">
|
||||
<TrailLink v-if="v.refererStep" :step="v.refererStep" :favicons="favicons" @close="$emit('close')" />
|
||||
<span v-if="v.utm && v.utm !== '—'" class="utm-tag small muted" :title="v.utmTitle">{{ v.utm }}</span>
|
||||
<RefererBadge v-if="v.refererBadge" :badge="v.refererBadge" :favicons="favicons" />
|
||||
<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')" />
|
||||
</td>
|
||||
@@ -253,7 +253,7 @@ const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], c
|
||||
<tbody>
|
||||
<tr v-for="(c, i) in crawlerRows" :key="i">
|
||||
<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')" />
|
||||
<span v-for="(f, fi) in c.readFlags" :key="fi" class="flag" v-html="f.flag" :title="f.name"></span>
|
||||
</td>
|
||||
@@ -480,16 +480,11 @@ const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], c
|
||||
color: var(--error, #c00);
|
||||
}
|
||||
|
||||
.visit-table .utm-tag {
|
||||
display: inline-block;
|
||||
/* 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 {
|
||||
display: inline-flex;
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* ``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
|
||||
* 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
|
||||
* latest external referer seen for the crawler — spiders often advertise
|
||||
* their own site there — rendered with its favicon like visit referers.
|
||||
* first, with total hits as a tie-breaker. The group's ``refererBadge`` is
|
||||
* the latest external referer seen for the crawler — spiders often advertise
|
||||
* their own site there — rendered as a badge with its favicon like visit
|
||||
* referers.
|
||||
* ``clients`` maps client hashes to client records.
|
||||
*/
|
||||
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),
|
||||
lastSeenIso: formatWhenIso(g.lastStart),
|
||||
lastSeenLocal: formatWhenLocal(g.lastStart),
|
||||
refererStep: stepOf(g.referer, titles),
|
||||
refererBadge: refererBadgeOf(g.referer, titles),
|
||||
pages: [...g.pages.entries()]
|
||||
.sort((a, b) => b[1].count - a[1].count)
|
||||
.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
|
||||
* with display strings; missing values become "—". ``trail`` starts with the
|
||||
* external referer (when present), then the entry page and any further internal
|
||||
* pages or external exit origins; consecutive views of the same page (e.g. a
|
||||
* with display strings; missing values become "—". The external referer
|
||||
* (when present) and the UTM tags ride along as ``refererBadge``; ``trail``
|
||||
* 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
|
||||
* consecutive-distinct rendered languages, summed read time, and the latest
|
||||
* 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 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 host = client.host || ''
|
||||
const isHost = !!host
|
||||
@@ -614,8 +636,7 @@ export function formatVisitRows(visits, clients, pageTree, now = Date.now(), sit
|
||||
lastSeenLocal: formatWhenLocal(v.start),
|
||||
langDisplay: formatLang(client.lang),
|
||||
trail,
|
||||
refererStep: stepOf(v.referer, titles),
|
||||
referer: dash(v.referer),
|
||||
refererBadge: refererBadgeOf(v.referer, titles, v.utm),
|
||||
ip: client.ip || '',
|
||||
ipDisplay: isHost ? mainDomain(host) : hostIP(client.ip) || client.ip || '—',
|
||||
isHost,
|
||||
@@ -625,8 +646,6 @@ export function formatVisitRows(visits, clients, pageTree, now = Date.now(), sit
|
||||
ua: client.uarite?.pretty || client.ua || '—',
|
||||
uaRaw: client.ua || '',
|
||||
uaUrl: client.uarite?.url || '',
|
||||
utm: utm || '—',
|
||||
utmTitle,
|
||||
}
|
||||
if (site.multilingual && distinctLangs.size) {
|
||||
if (distinctLangs.size === 1) {
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
/* 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,
|
||||
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
|
||||
dark sets (background included), resolved by light-dark() from the
|
||||
used color-scheme. A theme picks a set simply by declaring
|
||||
|
||||
Reference in New Issue
Block a user