GETs store the resolved page language, activity pings carry <html lang> (including a new ping on in-place language switches, which also keeps the switch's GET out of crawler classification), and trail items/crawler hits surface it. The viewer shows flags discreetly: nothing on single-language sites or primary-language-only rows, one leading flag for uniform visits, transition flags on mixed trails, a flag row per crawler.
73 lines
1.8 KiB
Vue
73 lines
1.8 KiB
Vue
<script setup>
|
||
import { computed } from 'vue'
|
||
import { formatCount, formatReadTime } from './analytics/format.js'
|
||
|
||
const props = defineProps({
|
||
step: { type: Object, required: true },
|
||
count: { type: Number, default: 0 },
|
||
favicons: { type: Object, default: null },
|
||
flags: { type: Array, default: () => [] },
|
||
})
|
||
|
||
defineEmits(['close'])
|
||
|
||
const hasError = computed(() => props.step.status >= 400)
|
||
const favicon = computed(() =>
|
||
props.step.external && props.step.origin ? props.favicons?.[props.step.origin] : null,
|
||
)
|
||
|
||
const title = computed(() => {
|
||
const parts = [props.step.title]
|
||
if (props.step.readSeconds > 0) {
|
||
parts.push(formatReadTime(props.step.readSeconds))
|
||
}
|
||
if (hasError.value) {
|
||
parts.push(`${props.step.status}`)
|
||
}
|
||
return parts.filter(Boolean).join(' — ')
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<a class="trail-link"
|
||
:class="{ error: hasError }"
|
||
:href="step.path"
|
||
:title="title"
|
||
:target="step.external ? '_blank' : undefined"
|
||
:rel="step.external ? 'noopener' : undefined"
|
||
@click="(e) => { if (!step.external) $emit('close') }">
|
||
<small v-if="count > 1" class="muted">{{ formatCount(count) }}×</small>
|
||
<img v-if="favicon" class="favicon" :src="favicon" alt="" />
|
||
<span>{{ step.slug }}</span>
|
||
<span v-for="(f, fi) in flags" :key="fi" class="flag" v-html="f"></span>
|
||
</a>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.favicon {
|
||
width: 1em;
|
||
height: 1em;
|
||
margin-right: 0.25em;
|
||
vertical-align: -0.1em;
|
||
}
|
||
|
||
/* Same flag chip as the visitor cells (VisitorCell.vue). */
|
||
.flag {
|
||
display: inline-flex;
|
||
width: 18px;
|
||
height: 12px;
|
||
margin-left: 0.25em;
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--line);
|
||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2) inset;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.flag :deep(svg) {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
</style>
|