单文件主题网页制作
这个技能解决什么
交付一个双击就能打开、断网也能跑、单文件零依赖的主题网页,同时做出「像付费模板」的动效质感。
三条主线:① 单文件内嵌 ② 模板动效移植 ③ 无头验证。
四条铁律
- 单文件零依赖:成品只有一个
.html。图片走 base64 data URI;动效全用 vanilla CSS/JS 重写,不引任何 CDN 库(模板原用的 GSAP / Swiper / jQuery / WOW.js 一律不带)。 - 移植动效时配色零改动:新增样式只允许引用页面已有的 CSS 变量(
--paper/--red/--gold/--ink/--line一类),不新增色值。 - 绝不整体重写既有页面:做过可视化编辑的页面会被注入
data-page-node-id之类属性,整体重写会丢节点。改用scripts/page_patch.py做锚点式注入。 - 改动必须无头验证:本机 Chrome + CDP,要求零 JS 异常、零横向溢出,并落盘截图。禁止「改完就说好了」。
工作流 A:从零做一页
- 定内容骨架:首屏 → 各章节(简介 / 时间线 / 贡献 / 作品 / 原文)→ 页脚。每章给一个
id供导航锚点用。 - 定视觉方案:读
references/theme-presets.md取配色 / 字体 / 装饰变量,写进:root。先定变量,再写样式,后续所有样式只消费变量。 - 写占位符版 HTML:图片位置写
src="{{IMG_PORTRAIT}}"这类 token,先不管体积。 - 内嵌图片:用
scripts/inline_images.py把 token 换成 data URI(详见下节)。 - 加动效与交互:按
references/motion-catalog.md挑需要的条目实现。 - 验证:用
scripts/cdp_verify.js跑桌面 + 移动端两轮,看截图确认版式。
图片内嵌(scripts/inline_images.py)
python scripts/inline_images.py page.html \
--img-dir ./assets \
--map IMG_PORTRAIT=portrait.webp IMG_FAMILY=family.webp IMG_BOOK=book.webp
要点:
- 用
str.replace做 token 替换,不要用re.sub。替换串里出现\s、\1会被当成转义序列直接抛bad escape。必须用正则时,替换参数传 lambda(lambda m: ...)而非字符串。 .webp体积小、浏览器全支持,优先于.jpg/.png;三张 6–12KB 的 webp 内嵌后总增量约 20KB,完全可接受。- 先用占位符、后内嵌,比在对话里直接粘贴 base64 省大量 token。
- 内嵌后不再有相对路径依赖,单文件可任意移动。
工作流 B:把模板动效移植进既有页面
- 找到模板真身。从「模板王」这类站点保存的
在线预览 - xxx.html通常只是外壳(iframe + 演示工具条);真身在xxx_files/index.html,脚本资源在同级目录且被改名为main.js.下载、gsap.min.js.下载这种带.下载后缀的形式。动效逻辑读main.js.下载(初始化与参数)+custom-animation.css(关键帧)。 - 列动效清单。逐条记下「触发条件 + 视觉变化 + 参考时长/缓动」,写进
references/motion-catalog.md的表格。 - vanilla 复刻。每条动效改写为纯 CSS 关键帧或原生 JS,统一放在一个
_anim.js里。改写前先确认脚本中不含</script>字符串——脚本会被内联进 HTML,出现该串会截断文档。 - 锚点注入。用
scripts/page_patch.py:先备份 → 样式层追加在</style>前 → 结构片段插在指定锚点标签之后 → 整段<script>替换为_anim.js。 - 验证。所有动效都要有数值探针,不能只看「好像动了」。
锚点注入(scripts/page_patch.py)
page_patch.py 提供 Patcher 类,支持 insert_after_first(pattern, snippet) / replace_first(pattern, snippet),内部是 re.subn(..., lambda m: ..., count=1) + must() 断言:锚点匹配不到直接 sys.exit(1),避免「静默什么都没改」。
每加一个锚点都要断言匹配数为 1。锚点选结构性标签(<section class="hero" id="top" ...>、<div class="tags" ...>)并带 [^>]*>,容忍编辑器追加的属性。
工作流 C:按反馈迭代
用户反馈「某块是空白 / 版面太松」时,先诊断层次,再动手改。
「空白」九成不是留白,是内容被动画藏住了。 排查顺序:先看该元素的 opacity / clip-path / transform 计算值,再看它是否挂了入场观察器。典型死循环见下节。
「太松」是纯数值工作。 收纵向间距时按同比例改 padding / margin,保留各断点的相对关系;改完必须量总高并记录前后差值(例:13193 → 12108 px,-8.2%)。
高频陷阱
细节与复现手法见 references/pitfalls.md。最要命的三条:
clip-path× IntersectionObserver 死循环:入场首帧为clip-path: inset(0 0 100% 0)(视觉面积归零)的元素会被 IO 判定为不可见,于是「加.in→ 判定不可见 → 撤销.in→ 又判定可见」无限振荡,文字永久停在opacity:0。修法:幕布类元素不要放进可逆观察器,改为观察它未被裁剪的父容器、且单向揭示(加.in后unobserve)。animation: ... forwards压住 inline transform:视差/倾斜必须分层,外层做动画、内层用 JS 写 transform。.reveal.in{transform:none}会覆盖 hover 位移:hover 类效果需要补一条更高特异性的规则。
cursor:none(自定义光标)与预加载遮罩都必须有 JS 失效兜底,否则脚本一报错页面就永久空白。
验证(scripts/cdp_verify.js)
零依赖方案:本机 Chrome + --headless=new --remote-debugging-port,用 Node 22 内置的 WebSocket 直连 CDP,不装 puppeteer。
node scripts/cdp_verify.js --file "file:///D:/site/page.html" --out ./.workbuddy/shots
它会报告:
- JS 异常与 console error 全文(必须为空)
- 页面总高与各区块高度
- 横向溢出检测(
scrollWidth > innerWidth),桌面与移动端各一轮 - 全页截图落盘
- 可选
--probe probe.js跑自定义探针并打印其 JSON 结果
验收标准:零异常 + 零横向溢出 + 截图版式无错位 + 每个动效都有探针数值为证。移动端至少过 390px 宽。
交付前检查清单
- [ ] 产物是单个
.html,无<link href="http...">/<script src="http..."> - [ ] 所有
{{TOKEN}}已被替换,无残留 - [ ] 断网打开正常,图片可见
- [ ] 桌面 / 390px 移动端均无横向溢出
- [ ] 零 JS 异常、零 console error
- [ ] 动效逐条有探针证据,不是目测
- [ ] 原配色变量未被新增色值污染
资源索引
| 文件 | 用途 |
|---|---|
references/theme-presets.md |
中式古典期刊风配色 / 字体 / 装饰变量,直接抄进 :root |
references/motion-catalog.md |
15 项可移植动效清单(预加载、光标、揭示、打字机、跑马灯、磁吸、3D 倾斜、滚动红轴…) |
references/pitfalls.md |
陷阱全集 + 诊断脚本写法 |
scripts/inline_images.py |
图片 → data URI 内嵌 |
scripts/page_patch.py |
锚点式补丁注入 + 备份 + 断言 |
scripts/cdp_verify.js |
Chrome CDP 无头验证 + 截图 + 探针 |