一个静态优先、无运行时依赖的赞助 / 项目推广链接组件。第一版为 Eigentime 的 Astro 博客设计,同时把数据模型、平台注册、HTML 渲染和点击归因拆开,便于后续增加 React、Vue、Svelte 或 Web Component 适配器。
它只负责把链接可靠地呈现出来并记录点击来源,不处理支付、账号、订单或赞助者数据。
- 无 JavaScript 也能完成核心动作:Astro 在构建期输出完整的
<nav><ul><a>,不会让赞助入口依赖 hydration。 - 业务语义和平台分离:
kind: "support"表示用途,platform: "afdian"表示服务商。GitHub 仓库也不必被误建模成社交账号。 - 严格输入边界:重复 ID、相对 URL、
javascript:URL、无效枚举或错误布尔值会在构建期直接失败。 - 可扩展而不锁死:内建常见平台推断,但
platform仍允许任意字符串;自建平台和图标通过局部 registry 扩展,不修改全局状态。 - 站点样式优先:默认会复用 Eigentime 已有的
--accent、--panel、--line、--font-mono等 token;其他站点可覆盖--one-*变量。
仓库尚未发布到 npm。开发期可以从相邻目录安装:
npm install ../one-widget发布仓库并打 tag 后,博客可以固定到明确版本,而不是跟随主分支:
# 完全锁定:只有手动改这行才会变
npm install github:wowayou/one-widget#v0.1.0
# 跟随补丁与向后兼容改动:npm update 即可,无需改配置
npm install "github:wowayou/one-widget#semver:^0.1.0"两种方式的差别只在“何时取到新版本”,不在“要不要重新配置”,后者由下面的升级策略保证。
---
import PromotionLinks from "@eigentime/one-widget/astro";
const items = [
{
id: "afdian",
kind: "support",
platform: "afdian",
url: "https://afdian.com/a/eigentime",
label: "在爱发电支持 Eigentime",
order: 10
},
{
id: "github",
kind: "repository",
url: "https://github.com/wowayou/personal-blog",
label: "查看博客源码",
order: 20
}
];
---
<PromotionLinks
items={items}
ariaLabel="支持与项目链接"
tracking={{
sourceParam: "from",
allowedSources: ["blog", "github", "one-stop-job"],
defaultSource: "direct",
unknownSource: "other",
eventName: "support_click"
}}
/>页面会先输出完整静态链接。很小的客户端增强只负责:
- 将
?from=one-stop-job解析为允许的source_project; - 点击时派发
one-widget:clickDOM 事件; - 若页面使用 GTM,则向
window.dataLayer推送同名分析事件。
未知来源统一归入 other,没有参数时归入 direct。来源值会去掉首尾空白并忽略大小写匹配(?from=GitHub 记为配置里的 github),中键“在新标签页打开”也会上报。查询参数不会被拼到付款链接上。
增强脚本是尽力而为的:没有 window/document 时不做任何事;页面监听器抛错或 dataLayer 被定义成非数组时,另一条上报和链接跳转都不受影响。
Eigentime 现有 /zh/support/ 与 /en/support/ 的具体替换方式见 docs/eigentime-integration.md。
import { renderPromotionLinks } from "@eigentime/one-widget";
const html = renderPromotionLinks(items, {
ariaLabel: "Project links",
layout: "wrap",
theme: "inherit"
});同时引入样式:
import "@eigentime/one-widget/styles.css";renderPromotionLinks() 会转义所有文本与属性,并且只允许 http:、https: 和 mailto: 绝对 URL。
interface PromotionItem {
id: string;
kind?: "social" | "support" | "repository" | "website" | "email" | "custom";
platform?: string;
url: string;
label: string;
shortLabel?: string;
icon?: string | false;
enabled?: boolean;
order?: number;
openInNewTab?: boolean;
appearance?: "icon" | "chip" | "button";
emphasis?: "primary" | "secondary" | "quiet";
eventName?: string;
}- 未显式设置
order的条目会以它在数组中的下标作为order,再与其它条目一起排序。因此只给部分条目写order时,未写的那条可能穿插到中间(例如order: 1、下标 2、order: 5会排成1, 2, 5)。要精确控制顺序,建议要么全写、要么全不写。 - 相同
order时,数组顺序作为稳定后备顺序。 enabled: false会保留配置但不渲染,适合临时下线某个平台。全部条目都被禁用时整个组件输出空字符串,不会留下空的<nav>。- 来自 CMS / JSON 的
null可选字段等同于未填写。 - 未填
platform时会根据 URL 推断;显式值始终优先。 - 未填
kind、icon、appearance、emphasis时,才从平台注册与业务语义推导默认值。 appearance: "icon"仍保留aria-label和屏幕阅读器文本,可点击区域固定不小于 44×44px。label/shortLabel自带 emoji(例如❤️ 在爱发电支持)时应设icon: false,否则会和组件默认图标重复出现两个心形。eventName只覆盖该条目的点击事件名,未填时用tracking.eventName。同一组里混放“支持”和“查看源码”时必须给后者单独的事件名,否则源码点击会被统计成支持转化;kind、platform、item_id仍会作为事件参数一起上报。
目标是“升级不需要重新配置”,因此把兼容性写成可执行的约束而不是口头承诺:
- 公开契约由
npm run check里的契约测试锁定:静态 HTML 结构、one-widget__*class、归因data-*属性、one-widget:click与dataLayer的字段名、--one-*token 名。破坏其中任何一项都会让上游测试直接失败,而不是等博客构建时才发现。 - 新增能力一律是可选字段并保留原有默认值(
eventName就是这样加入的):不填等于升级前的行为。 - 0.x 阶段用 minor 表达破坏性变更。
#semver:^0.1.0会取到0.1.x的最新 tag,不会自动跨到0.2.0,所以需要改配置的变更永远不会自动到达博客。 - 每次发布在 CHANGELOG.md 写明“是否需要改调用方配置”,升级时只读这一行即可,不必读 diff。
扩展不会改写全局 registry;每次渲染可以传入自己的定义:
const platforms = {
forgejo: {
defaultKind: "repository",
defaultLabel: "Forgejo",
icon: "forge",
hosts: ["code.example.org"]
}
};
const icons = {
forge: {
viewBox: "0 0 24 24",
paths: [{ d: "M4 4h16v16H4z" }]
}
};
renderPromotionLinks(items, { platforms, icons });组件只接收 SVG viewBox 与 <path d> 数据,不接收任意 SVG/HTML 字符串,减少自定义图标带来的注入面。品牌图标应由调用方在确认商标规范后按需注册;核心包默认使用中性的 heart/code/globe/mail/link 图标。
布局支持 wrap、stack、inline,主题支持 inherit、light、dark。常用覆盖变量:
.my-support-links {
--one-accent: #0d766e;
--one-accent-contrast: #fff;
--one-bg: transparent;
--one-line: color-mix(in srgb, currentColor 24%, transparent);
--one-radius: 3px;
--one-gap: 0.75rem;
}项目没有安装依赖即可运行核心检查:
npm run check
npm run pack:checkAstro 适配器的真实构建 fixture 需要单独安装其测试依赖:
cd test/fixtures/astro
npm install
npm run verify # 默认 astro ^7
npm run verify:astro5 # 换装 astro ^5 后重跑
npm run verify:astro6
npm run verify:astro7最近一次多版本验证:Astro 5.18.2、6.4.8、7.3.5,三个大版本均构建并断言通过。GitHub Actions 会在每次推送时于 Node 20 / 22 / 24 上跑 npm run check,并用 Astro 5 / 6 / 7 构建 fixture。
架构与稳定 API 边界见 docs/architecture.md。