无 Cookie 静态 Nuxt 站点如何做首次触点归因
译注:本文编译自 dev.to 作者 Léo Guillaume(Dibodev)的技术分享,原文发布于 2026 年 9 月 21 日。原文链接见文末。
对于注重隐私的静态站点来说,无 Cookie 分析工具解决了合规问题,却留下了一个空白:当访客填写联系表单时,站长往往无法知道这条线索究竟来自哪里。Léo Guillaume 在 dev.to 上分享了一个约 40 行、零依赖的方案,用 localStorage 为每条线索打上首次触点来源标签。
问题:聚合数据无法回答“这一条从哪来”
作者运营一个基于 SSG + Storyblok 的小型静态 Nuxt 站点,使用无 Cookie 分析:没有同意横幅,服务端也不存储访客信息。隐私方面很理想,但每个自由职业者或小企业都会问的问题却无法回答——每条线索到底来自哪里?
当有人填写联系表单时,作者希望知道:是 Google 自然搜索?来自其他站点的引荐?还是某个特定营销活动?无 Cookie 工具(如内存模式下的 PostHog、Umami)能给出聚合的引荐来源,但很难说明某一条具体线索来自哪个渠道。因此作者加入了一个极小的首次触点归因层——不用 Cookie,不引入库——为每条线索标注访客最初落地时的来源。
用 localStorage 只捕获一次首次触点
首次触点的含义是:在访客第一次落地站点时,记录外部的 document.referrer、落地路径以及 UTM 参数。只存储一次,这样上周通过 Google 找到站点、今天直接回访的访客,仍然归因给 Google——是真正的首次触点,而非末次点击。
// app/composables/useLeadSource.ts
const KEY = 'lead_source'
export function useLeadSource() {
function captureLeadSourceOnce(): void {
if (typeof window === 'undefined') return
try {
if (window.localStorage.getItem(KEY)) return // already captured
const params = new URLSearchParams(window.location.search)
const referrer = document.referrer
const isExternal = referrer !== '' && !referrer.startsWith(window.location.origin)
window.localStorage.setItem(KEY, JSON.stringify({
referrer: isExternal ? referrer : null,
landingPage: window.location.pathname || null,
utmSource: params.get('utm_source'),
utmMedium: params.get('utm_medium'),
utmCampaign: params.get('utm_campaign'),
}))
} catch {
// localStorage throws in private mode or blocked storage: never crash a page over analytics
}
}
function getLeadSource() {
if (typeof window === 'undefined') return null
try {
const raw = window.localStorage.getItem(KEY)
return raw ? JSON.parse(raw) : null
} catch {
return null
}
}
return { captureLeadSourceOnce, getLeadSource }
}作者强调两个关键细节:
- isExternal 判断。 站内导航时
document.referrer是自己的域名。只有真正的外部引荐才保留,否则记为 null(直接访问)。 - 处处 try/catch。 在隐私模式或存储被禁用时 localStorage 会抛异常。分析逻辑绝不应该让页面崩溃。
在客户端只触发一次
// app/plugins/lead-source.client.ts
import { useLeadSource } from '~/composables/useLeadSource'
export default defineNuxtPlugin(() => {
useLeadSource().captureLeadSourceOnce()
})之所以是 .client.ts 插件,是因为 SSG 预渲染阶段不存在 localStorage 和 document.referrer。composable 内部的守卫使其具备幂等性,因此 SPA 导航和刷新都不会覆盖首次触点。
附加到联系表单
在提交时(甚至在表达联系意图时,比如用户第一次在邮箱字段失焦但尚未填完),读取已存储的来源并随载荷一起发送:
const { getLeadSource } = useLeadSource()
const payload = {
// ...name, email, message...
source: getLeadSource(), // { referrer, landingPage, utmSource, ... } or null
}服务端再把它转成通知邮件里的一行标签:
function formatAcquisitionSource(source) {
if (!source) return ''
if (source.utmSource) {
const base = source.utmMedium ? `${source.utmSource} / ${source.utmMedium}` : source.utmSource
return source.utmCampaign ? `${base} (${source.utmCampaign})` : base
}
if (source.referrer) return source.referrer
return 'Direct / unknown'
}这样每封联系邮件都会带上 Acquisition source: google.com 以及落地页信息,作者就能在不使用任何 Cookie 的前提下,逐条知道线索来源。
为什么不直接看 PostHog 或 GA
无 Cookie 分析工具已经在事件上捕获了 $referrer 和 UTM,这对聚合看板来说很合适。作者认为该方案填补了两个缺口:
- 逐条线索。 在分析工具界面里把某一次具体提交与来源关联起来很麻烦,而直接放进邮件则一目了然。
- 首次触点持久化。 在内存或无 Cookie 持久化模式下,工具往往无法把回访访客与其最初来源重新拼接起来。localStorage 以极低成本做到了作者唯一关心的事:线索。
作者总结,整个方案约 40 行、零依赖、无 Cookie、无需同意横幅。对于小企业或自由职业者站点而言,这已经足够解决归因问题。
原文链接
https://dev.to/dibodev/first-touch-attribution-on-a-cookieless-static-nuxt-site-4f16