为什么需要短链服务#

博客里有时要贴一些很长的 URL——GitHub 链接、Grafana 面板、API 文档之类的。长 URL 在 Markdown 里虽然无所谓,但分享到微信、邮件时看起来乱糟糟的。

另一个场景:跳转(或者外部)链接如果能统一走自己的短域名,更整洁,也方便以后追踪点击数据和做链接的安全检查和处理。

为什么选 Sink#

Sink 是一个基于 Cloudflare Workers 的无服务器短链服务,核心特性:

功能 说明
自定义 slug 手动指定或 AI 自动生成
Analytics 访客统计(设备类型、地区、来源)
二维码生成 每条短链自带 QR Code
链接过期 可设置有效期
批量导入导出 JSON / CSV
AI 辅助 Cloudflare Workers AI 生成 slug
每日备份 R2 Bucket 定时备份

关键一点:Sink 100% 运行在 Cloudflare Workers,没有服务器,不需要数据库,免费额度足够个人使用。

与其他自托管短链方案(Shlink、YOURLS、Kutt)相比,Sink 不需要 K8s 部署,不占 Oracle 云的资源,而且它本来就跑在我已有的 Cloudflare 基础设施上,零额外成本。

架构#

用户 → Cloudflare DNS (s.meirong.dev)
         → Cloudflare Worker (sink)
              ├── KV Namespace     短链存储(slug → 目标 URL)
              ├── R2 Bucket        每日 00:00 UTC 自动备份
              ├── Analytics Engine 访客点击统计
              └── Workers AI       AI slug 建议

DNS 解析由 Cloudflare 自动处理,不经过 Cloudflare Tunnel,也不过 K8s 的 Traefik——这是一条独立链路。

与 Homelab 其他服务的对比:

其他服务:Internet → Cloudflare Tunnel → Traefik (K8s) → Pod
Sink:    Internet → Cloudflare Worker(边缘直接响应)

部署过程#

1. 将 Sink 作为 git submodule 引入#

把 Sink 源码作为 submodule 引入 homelab 仓库,方便版本追踪和后续更新:

mkdir -p cloudflare/workers
git submodule add https://github.com/miantiao-me/Sink cloudflare/workers/sink

目录结构:

cloudflare/workers/
├── justfile          # 部署命令
└── sink/             # Sink 源码(submodule)
    ├── wrangler.jsonc
    ├── server/middleware/rate-limit.ts  # 自定义限速中间件
    └── .env          # 本地配置(gitignored)

2. 创建 Cloudflare 资源#

Sink 需要 KV Namespace 和 R2 Bucket,可以通过 Cloudflare API 直接创建(不需要单独权限):

# 创建 KV Namespace
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/storage/kv/namespaces" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"title": "SINK_KV"}'

# 创建 R2 Bucket
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/r2/buckets" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  --data '{"name": "sink"}'

把返回的 KV namespace ID 填入 wrangler.jsonc

3. 配置 wrangler.jsonc#

在 Sink 的 wrangler.jsonc 里添加 Analytics Engine、Rate Limiting 绑定和环境变量:

{
  "vars": {
    "NUXT_HOME_URL": "https://s.meirong.dev",
    "NUXT_REDIRECT_STATUS_CODE": "308",
    "NUXT_CF_ACCOUNT_ID": "your-account-id"
  },
  "analytics_engine_datasets": [
    { "binding": "ANALYTICS", "dataset": "sink" }
  ],
  "ratelimits": [
    {
      "name": "RATE_LIMITER",
      "namespace_id": "1001",
      "simple": { "limit": 30, "period": 10 }
    }
  ],
  "kv_namespaces": [
    { "binding": "KV", "id": "your-kv-namespace-id" }
  ],
  "r2_buckets": [
    { "binding": "R2", "bucket_name": "sink" }
  ]
}

Rate Limiting 设置为 30 请求/10 秒/IP(等价于平均 3/s),这是 Workers Rate Limiting binding 支持的最细粒度(最小 period = 10s)。

4. 添加限速中间件#

Sink 基于 Nuxt/Nitro 构建,可以在 server/middleware/ 下添加 Nitro 中间件来接入 Workers Rate Limiting API:

// server/middleware/rate-limit.ts
export default defineEventHandler(async (event) => {
  const { cloudflare } = event.context
  if (!cloudflare?.env?.RATE_LIMITER) return

  const ip = getHeader(event, 'cf-connecting-ip') || 'unknown'
  const { success } = await (cloudflare.env.RATE_LIMITER as any).limit({ key: ip })

  if (!success) {
    setResponseStatus(event, 429)
    setResponseHeader(event, 'Retry-After', '1')
    return { error: 'Too Many Requests' }
  }
})

这个中间件对每个请求都会检查 IP 限速,超限返回 429。

5. 构建和部署#

# 构建(NUXT_HOME_URL 必须在编译时注入)
NUXT_HOME_URL=https://s.meirong.dev pnpm build

# 部署
npx wrangler deploy

注意NUXT_HOME_URL 是 Nuxt 的 public runtimeConfig,会在编译时写入 bundle,必须通过环境变量在 build 阶段注入,而不是 wrangler vars(后者只对运行时私有变量有效)。

6. 绑定自定义域名#

Wrangler 的 custom domain 路由需要 Zone Workers Routes:Edit 权限,但我的 API Token 没有这个权限。直接通过 Cloudflare API 绑定即可:

curl -X PUT "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/workers/domains" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "environment": "production",
    "hostname": "s.meirong.dev",
    "service": "sink",
    "zone_id": "your-zone-id"
  }'

这个绑定是持久的,不需要每次 deploy 重新设置。

7. 设置管理员 Token#

通过 Cloudflare API 设置 Worker secret(NUXT_SITE_TOKEN 是访问 /dashboard 的密码):

curl -X PUT "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/workers/scripts/sink/secrets" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"name": "NUXT_SITE_TOKEN", "text": "your-token", "type": "secret_text"}'

上线效果#

部署完成后:

  • 短链跳转:访问 https://s.meirong.dev/xxx 即跳转到目标 URL(308 永久重定向)
  • 管理面板https://s.meirong.dev/dashboard 用 Site Token 登录,可创建/管理短链
  • Analytics:每次跳转自动写入 Analytics Engine,可在 dashboard 查看访客数据

justfile 封装了常用操作:

# 更新 Sink 版本
update:
    cd sink && git pull origin master && pnpm install && \
    NUXT_HOME_URL=https://s.meirong.dev pnpm build && \
    npx wrangler deploy

踩坑记录#

1. wrangler deploy 每次都报 custom domain 错误

这是因为 Token 缺少 Zone Workers Routes:Edit 权限,但 domain 本身已经通过 API 绑定好了,报错可以安全忽略。Worker 正常服务。

2. Analytics Engine 需要手动激活

第一次 deploy 含 analytics_engine_datasets 绑定时会报错,需要去 Dashboard → Workers & Pages → Analytics Engine 点一次 “Enable”。之后不再需要操作。

3. Rate Limiting period 最小值是 10 秒

Workers Rate Limiting binding 的 period 只支持 10 或 60,没有 1 秒选项。要做到"平均 3 次/秒",配置为 limit: 30, period: 10 即可。

4. NUXT_HOME_URL 必须在 build 时注入

Nuxt 的 public runtimeConfig(如 NUXT_HOME_URL)会在编译时写入 bundle。如果只设置在 wrangler vars 里,运行时 Worker 读的还是编译时的默认值。正确做法是在 build 命令前设置环境变量。

总结#

Sink 是一个很适合 Homelab 场景的短链服务:

  • 零服务器成本:Cloudflare Workers Free Plan 100,000 请求/天,KV、Analytics、AI 全部有免费额度
  • 无需额外运维:不占 K8s 资源,不需要数据库维护,R2 自动备份
  • 功能完整:短链、统计、AI slug、二维码、导入导出一应俱全
  • IaC 友好:源码以 git submodule 形式纳入 homelab 仓库,just deploy 一键更新

如果你也有 Cloudflare 账号和自定义域名,整个部署过程 30 分钟内可以完成。