静态站做联盟归因:一个 40 行的 /go/ 跳转页,和它踩过的五个坑

静态站有个绕不过去的缺陷:用户点了你页面上的外链,你什么都不知道。

不是统计不准,是根本没有。没有后端就没有访问日志,<a href="https://example.com"> 一出去,那一次点击就永久消失了。你只能看到「今天有 200 人来过」,看不到「有没有人真的去点了那个按钮」。

我前段时间做了一个 AI 工具目录站,纯静态(Cloudflare Pages),需要知道哪个工具被点得多——因为工具站唯一可能的收入来源是联盟分成,而谈合作时对方第一句话就是「你一个月能导多少量」。答不上来,就没得谈。

这篇文章是这个问题的完整解法,以及中间踩的五个坑。方案本身不复杂,40 行不到的 HTML 页面,但细节错了会静默失效。


一、先把问题定义清楚

要做的事其实是两件,很多人会混成一件:

  1. 记账:这一次点击发生了,落在哪个工具上。
  2. 归因:如果这个人后来注册或付费了,能追溯回这次点击。

第一件事靠自己,第二件事靠联盟平台的 cookie。

混成一件的后果是:你以为加了 ?utm_source=xxx 就算做完统计了。不算。 UTM 参数只能告诉对方站点「流量从哪来」,你的服务器依然没有任何记录——你连自己有没有被点都不知道。

二、为什么不能直连

最省事的写法是这样:

<a href="https://example.com">前往官网</a>

这个链接的错误不在于不好看,在于它绕过了你自己。用户从你的页面直接到了对方服务器,你的域名在整个过程中没有再出现一次。任何统计工具(Cloudflare Web Analytics、百度统计、GA)都看不见这次行为,因为他们的 JS 只在你自己的页面上运行。

所以要插一个中间层:让点击先回到自己的域名,再跳出去。

三、正解:/go/<slug>.html

做法是为每个工具生成一个静态跳转页,全站所有「前往官网」按钮都指向它:

/go/doubao.html   →  目标站
/go/kimi.html     →  目标站

这个页面做的事很简单:

<script>
setTimeout(function () { location.replace("https://目标地址"); }, 400);
</script>

为什么用 location.replace 而不是 location.href:

  • replace 不往浏览历史里塞记录。用户点完浏览器后退,直接回到介绍页,而不是被弹回跳转页再跳一次——那会形成一个退不出去的死循环,很烦人。
  • 延迟 400ms 是为了让页面先渲染出来。0 延迟会让用户看到一瞬间的白屏闪烁,观感上像是点坏了。

这一层加完,Cloudflare 的访问统计里就会出现 /go/doubao.html 这个路径。「哪个工具被点了多少次」这个数据第一次有了。

四、五个坑

坑 1:不屏蔽搜索引擎,等于给自己造薄内容

跳转页对搜索引擎是纯粹的垃圾:没有内容,只有一个跳转。几十上百个这种页面被收录,是标准的 thin content,会拉低整站质量评分。

两件事都要做,缺一不可:

<meta name="robots" content="noindex,nofollow" />
# robots.txt
User-agent: *
Disallow: /go/

只写 noindex 不够——爬虫仍然会抓取页面才能读到这个 meta,白耗抓取预算。robots.txt 从入口就拒绝,两个一起用才干净。

这里有个容易忽略的点:robots.txt 里要为每个 User-agent 单独写。如果文件里列了 17 个爬虫(Googlebot、Bingbot、百度、搜狗、字节……),那 Disallow: /go/ 就要写 17 遍。写在第一个 User-agent 下面的规则只对那一个生效,不会继承给后面的。我第一版就漏了,只对默认 UA 生效。

坑 2:构建顺序反了,按钮全 404

这是最典型的「本地看着对,线上一片 404」。

我的构建脚本分成两段:一段生成工具页(里面有跳转按钮),一段生成跳转页。一开始把跳转页放在后面跑。

页面里的按钮是写死的 /go/doubao.html,跟跳转页在不在没关系——所以生成的 HTML 看起来完全正常,链接也不报错。但它指向的文件当时还不存在。

如果构建到打包为止就结束,这个问题会被掩盖一段时间(因为上一轮构建的旧跳转页还在)。等到某天清理目录后重新构建,才突然全线 404,而且很难想到是顺序问题。

修法很直接:生成跳转页的步骤必须排在生成页面之前。这条已经写进脚本注释,防止以后改流程时再踩。

坑 3:联盟短链不能加参数,加了会 404

一般外链我都会补上 UTM:

https://example.com?utm_source=mysite&utm_medium=referral&utm_campaign=doubao

但联盟链接不行。联盟给的推广链接通常是这种带签名的短链:

https://<平台域名>/s/qyk42d8n87

这个 /s/xxxx 是一段带签名的短码。它跳到目标站时,目标地址里已经带好了归因参数。你再往上挂 ?utm_source=...,运气好是被忽略,运气不好会被当成签名的一部分校验失败,直接 404——而用户看到的是「这个链接坏了」。

所以拼接函数里必须有一个例外分支:

def with_utm(url, slug):
    if not url:
        return url
    # 联盟短链或已带 utm_source 的,原样返回,一个字符都不加
    if "utm_source=" in url or ("/s/" in url and SHORT_LINK_HOST in url):
        return url
    sep = "&" if "?" in url else "?"
    return "%s%sutm_source=%s&utm_medium=%s&utm_campaign=%s&utm_content=%s" % (
        url, sep, UTM_SOURCE, UTM_MEDIUM, UTM_CAMPAIGN, slug)

教训是:「统一处理」在涉及第三方签名时是危险的。 越是看起来整齐划一的拼接逻辑,越要留一个「这个地址我不能碰」的逃生口。

坑 4:目标为空就生成,等于批量造死链

工具清单是数据驱动的,有些条目没有官网,有些官网字段是空的。如果循环里不做判断,脚本会老老实实生成一个跳转页,location.replace("")——点进去原地转圈。

宁可不生成,让按钮走别的兜底逻辑(我是给一个「去搜索引擎找」的按钮),也不要生成一个确定会坏的页面。404 至少是诚实的,转圈是骗人的。

if not target:
    skipped.append(slug)
    continue

顺便把这些 skip 的 slug 打出来。这类输出长期看很有用——它能告诉你数据缺口的规模。

坑 5:只留 JS 跳转,会挡住一部分人

location.replace 会被这些东西干掉:浏览器扩展(隐私类、脚本拦截类)、企业策略、部分内置浏览器。比例不高,但这些人点进来看到的是一个卡住的页面。

所以页面上必须有一个真实可点的链接,排在 JS 后面兜底:

<a class="btn" href="https://目标地址" rel="nofollow noopener">没有自动跳转?点这里 →</a>

rel="nofollow noopener" 里 noopener 是必须的——不加的话新窗口能通过 window.opener 反向操作你的页面。

五、三个我一开始想错了的事

技术做完之后,真正消耗时间的是这三个认知偏差。

第一,点击本身不产生任何收入。

我以为「点击 = 有收益」,实际上联盟的结算方式只有两种:CPS(按成交额分成)和 CPA(按新注册)。没有 CPC。 我核了手里 37 个项目,条件里没有任何一个提到「按点击付费」。

也就是说,一万次点击,如果没有一个人注册,收入是 0。点击的唯一价值是给归因链路留下一个起点——它自己不值钱。

想清楚这点之后,我做的不再是「怎么让更多人点」,而是「怎么让点进来的人注册」。

第二,归因靠 cookie,窗口是 30 天。

用户点你的链接过去时,对方站点在你浏览器里种一个 cookie,里面是你的推广 ID。30 天内这个人注册或付费,就算你的(部分平台是 90 天)。超过窗口,跟你没关系了。

这个机制决定了一件事:你没法「催单」。 数据要等,而且等到的不一定是好结果。

第三,看着到账的钱,不一定是你的钱。

后台会显示「待确认」金额,但它还不是收入:

  • 用户退款,金额会扣回。
  • CPS 一般要 7 天确认,个别平台 90 天。
  • 平台自己还要抽 5%–10% 服务费。
  • 提现还有门槛,没到门槛的钱拿不出来。

所以「后台显示 800 元」和「能取出来 800 元」是两件事。做预算时按最保守的算,不要按后台那个数字算。

六、这个方案不适合什么场景

说清楚边界比推荐方案更有用:

  • 有后端就别这么做。 直接在服务端 302 并写日志,比静态页干净得多,也不会有 JS 被拦的问题。
  • 只想要总量、不关心单条,不必这么麻烦。 在 <a> 上加一个 onclick 埋点就够了。
  • 站上外链只有几个,手动看日志也行。 这套东西的价值随外链数量线性增长,十几条以下属于过度设计。

它的适用场景很窄:纯静态、无后端、外链多、且需要按条目区分数据。 满足这四条,才值得为它写一个构建步骤。

结尾

整套东西加起来不到 40 行 HTML 加一个循环,但它把「不知道」变成了「有数」。技术复杂度很低,难的是想清楚「记账」和「归因」是两件事,以及不要相信「统一拼接」这种看起来整洁的东西。

我自己那个工具站是按这套做的,全站每个「前往官网」都走跳转页——纯静态,没有后端,也没接任何第三方统计脚本:

https://ai-tool-scenes.pages.dev

如果你们也在做静态站 + 联盟的形式,坑 2(构建顺序)和坑 3(短链加参数)值得单独抄下来贴在显示器上,这两个坏起来都是静默的。

posted @ 2026-10-22 16:18  觉醒笔记  阅读(5)  评论(0)    收藏  举报