洛神
谢谢の博客
文章 0
分类 0
标签 0
⭐ 归档
搜索
📊网站资讯
运行天数315 天
文章数目0 篇
创建于2025-12-22

从 Valaxy(Vue)迁移到 SvelteKit,再加一层 Butterfly 主题和全套 SEO。这篇复盘整个过程的得与失。

为什么重构

旧站是 Valaxy + Vue 3 + Vite,跑了很久,功能齐全。但有几个绕不过去的问题:

  • SEO 靠补丁。SPA 的 HTML 是空壳,我是靠 Worker 在服务端给爬虫注入预渲染正文来凑合的。能工作,但每加一个页面都要维护一套注入逻辑。
  • 主题魔改到头了。为了 Butterfly 风格,我覆盖了 Valaxy 主题的七八个组件,升级一次主题就可能全崩。
  • 管理后台是「能用」级别。Vditor + Tailwind CDN,加载慢,代码三千行,改一处怕全局崩。

所以这次目标很明确:内容先成为 HTML;SEO 内建而不是外挂;后台重写成想维护的样子。

技术栈选择

  • SvelteKit 3.1(SSR/SSG 一体)+ Svelte 5(runes)
  • adapter-static:全站预渲染成静态 HTML(232 个页面的思路来自 acofork 的第 20 次重构)
  • Cloudflare Pages 部署;后端沿用现有的 Workers + D1 + B2(零迁移成本)
  • Markdown 用 marked 渲染,评论换 Twikoo

值得说明的是 acofork.com 的误导:我一开始通过逆向分析判断它"不是 SvelteKit 而是 Preact CSR",后来发现是我的探测方法失效了——它所有未匹配路径都返回 200+首页内容(soft 404),任何探针都会"成功"。它自己 bundle 里的版本历史表写得很清楚:第 20 代就是 SvelteKit。教训:soft 404 的站点,任何"存在性探测"都不可信。

SEO 层(这次的核心投入)

SEO 不是加几个 meta 就完事。我用 Playwright + REA 逆向了 acofork.com 的真实产物,逐值对齐:

  • 标题规则:首页 = 站点名;子页 = 页面标题 | 站名
  • canonical:绝对 URL、统一尾斜杠、每页一个
  • JSON-LD:单 @graph:Person + WebSite + WebPage,文章页加 BlogPosting(11 个键与键序逐一对齐)+ BreadcrumbList
  • robots.txt / sitemap.xml / rss.xml:作为静态文件在构建期生成(sitemap 是 <loc> 唯一子元素、文章倒序)

同时修掉了原站(以及 acofork)的几个缺陷:

缺陷 修复
未知路径返回 200+首页(soft 404) 真实 404 + 独立页面
文章页 og:type=website article
无 dateModified 空值回退发布日期
无分页(174 篇挤一页) 分页 + rel=prev/next
无安全头 HSTS / nosniff / X-Frame-Options / Referrer-Policy

Butterfly 主题:逆向而非照抄

主题样式我全部从 hexo-theme-butterfly 编译后的 CSS 里取值,不是看截图猜的。几个反直觉的事实:

  • 卡片不是玻璃拟态——全站只有固定导航用 backdrop-filter: blur(7px),卡片是不透明的
  • 主色是三件套:#49b1f5(主)、#00c4b6(激活:分页当前页/TOC/::selection)、#ff7242(悬停),没有单一 --primary-color
  • 文章卡片 hover 不位移——只有阴影加深 + 封面 scale(1.1)
  • 头像在侧栏作者卡,不在 banner

有些坑只有部署了才发现:

  • background-attachment: fixed 在 overflow:hidden 的短 banner 里会让图片只显示一条
  • SVG 波浪要无缝循环,路径必须是整数周期 + 用百分比平移(preserveAspectRatio="none" 下用户单位 ≠ CSS 像素)
  • SvelteKit 的 SEO 标签必须包在 <svelte:head> 里,否则渲染进 <body>,爬虫读不到

目录滑块:一个被我理解错三次的需求

"目录要有动画"——我先后做了:抽屉弹出动画(错)、恢复吸顶(错)、放大动画时长(还是错)。最后才明白用户要的是目录左侧那个 4px 竖条跟随阅读位置滑动。

实现:IntersectionObserver 跟踪正文标题 → 计算 active 项的 offsetTop → 驱动滑块 top。过渡曲线 cubic-bezier(0,1,.5,1) 0.25s,数值全部从老版 DOM 实测。

教训:需求里的"动画"指什么,先确认再说。

音乐播放器:上游才是真问题

播放器换成了 APlayer(与老版一致),但部署完发现无声。追了整条链路:

API url → 302 → music.163.com/song/media/outer/url?id=….mp3
        → 302 → music.163.com/404    ← 是网页不是音频

网易云外链已被版权/VIP 封锁,Meting 拿不到直链。我还写了个"逐首探测可播性"的过滤器,结果发现它从原理上就是错的——fetch 探测受 CORS 限制必然失败,而 <audio> 播放不受限,于是大量能播的歌被误杀。已回退。

中间还发现 Meting 官方测试页自己也全线报错——上游版权状态就是这样,与播放器实现无关。最终方案:保留 APlayer,用户家宽 IP 下可播(服务端出口 IP 会被拒绝,这是 IP 相关)。

评论系统:Twikoo + 同域挂载

从 giscus 换成 Twikoo(支持匿名评论,不强制 GitHub 登录)。后端部署在 Cloudflare Workers + D1。

两个坑:

  1. D1 免费额度满了——复用现有 blog 库(Twikoo 的三张表名与博客表不冲突)
  2. 发评论报 code 1000: FinalizationRegistry is not defined——主 Worker 的 compatibility_date 太旧,该 V8 特性未启用。提升到 2026-09-01 解决

最初的独立 Worker 偶发报「请求被跨域策略拦截」,后来直接同域挂载到博客 API 的 /twikoo 路径下——同域就没有跨域这回事。

管理后台重写

新后台:登录(用户名+密码)+ 文章列表 + Markdown 编辑器(11 个工具栏按钮 + 实时预览)+ 相册管理。走现有 Worker API,数据与旧后台互通。

发布后自动重建:Worker 调 GitHub repository_dispatch → Actions 重新预渲染 → 部署 Pages,全程约 1 分钟。

修过一个隐蔽 bug:触发重建的 fetch 没有 await,而 Cloudflare Workers 会在响应返回后取消未完成的子请求——于是重建经常不触发。改成 await Promise.allSettled([...]) 后实测通了。

优点

  • SEO 内建:SSR 直出 + 全套结构化数据,不再依赖 Worker 注入补丁
  • 真实 404:不再是 200 + 首页内容
  • 每页恰好一个 h1:正文 h1 自动降级,构建期保证
  • 主题可维护:所有样式是普通 CSS + 设计令牌,不依赖主题框架版本
  • 后台好用:干净、快、移动端可用
  • 构建产物小:纯静态 HTML + 少量 JS

缺点

  • 动态性下降:adapter-static 意味着新文章要等自动重建(约 1 分钟)才上线;旧版是即时显示
  • 多了一层构建链:发布 → dispatch → Actions → build → deploy,任何一环挂了站点就不更新
  • Svelte 5 runes 有学习曲线:$effect 里读写自己的依赖会无限重跑(打字机就这么坏过一次)
  • Theme 生态换血:Valaxy 的插件/主题机制全部作废,需要什么得自己写
  • 搜索没做:旧版的 fuse 搜索没迁移

数字

  • 构建产物:13 个静态 HTML 页 + 资源,~900KB
  • svelte-check:0 error / 0 warning
  • SEO 验证:13 项断言全过(每页 1 个 h1、JSON-LD 键序、真实 404、结构合规)
  • 端到端:发布 → 自动重建 → 上线,实测约 1 分钟

最后

这次重构最大的收获不是换了框架,而是把「内容 → HTML → SEO → 后台 → 评论」整条链路重新理了一遍。每个环节现在都有明确的职责和验证方式。

以及一个朴素的教训:逆向别人的站点时,别相信任何"存在性探测"——尤其当那个站有 soft 404 的时候。

SvelteKit重构SEOButterflyCloudflare