总所不周知,我的博客自从上线了banner图功能之后就出现神必bug,具体表现为在文章界面点击导航栏按钮回到主页时会自动触发刷新
由于Fuwari这个博客主题使用了Swup这个东西,这是一个为服务端渲染(SSR)网站设计的页面过渡库,它能让你的网站在页面切换时不再“刷新闪白”,而是平滑过渡。所以按照正常逻辑,点击按钮返回到主页时应该是直接过渡的,所以很大概率是DS魔改的时候改出bug了
第一章:整页刷新
症状:在文章页点顶部导航栏的「主页」返回时,页面会先平滑过渡一下,紧接着整个页面又重新加载了一遍(明显的白屏/重绘)。其他跳转(比如主页点进文章)反而正常。
DS给我的说法是由于Swup拦截失败导致直接触发访问 / 目录导致的,但经过DS的神秘修理问题仍然存在,于是我果断寻求免费打白工的hy3排查一下问题
第二章:hy3神力
排查很简单——这种「Swup 过渡后又来一次硬刷新」的现象,八成是某段 JS 里写了 location.reload()。全局搜了一下,整个 src 里只有一处 window.location.reload(),在 src/components/widget/SideBar.astro 的脚本里:
let wasPostPage = isPostPath(window.location.pathname);
function updateSidebar() { const sticky = document.getElementById('sidebar-sticky'); if (!sticky) return; const nowIsPost = isPostPath(window.location.pathname);
if (nowIsPost) { // 进入文章页:从 DOM 提取标题,手动重建目录 // ... wasPostPage = true; } else { if (wasPostPage) { window.location.reload(); // ← 罪魁祸首 return; } }}window.swup?.hooks?.on('page:view', updateSidebar);逻辑很清楚:Swup 完成一次客户端导航后会触发 page:view 事件,这段钩子检查一下「现在是不是文章页」:
- 主页 → 文章:
nowIsPost=true,把wasPostPage置 true,不刷新; - 文章 → 文章:
nowIsPost=true,不刷新; - 文章 → 主页/归档/关于:
nowIsPost=false且wasPostPage=true,命中else,执行reload()。
所以它只在「从文章页离开」时触发——这也正好解释了为什么我最先注意到的是「返回主页会刷新」。
第一段修复:删掉那段脚本
一开始我以为这段脚本完全是多余的。理由也很充分:
- 侧边栏
SideBar是渲染在<main>内部的,而main正是 Swup 的容器之一,每次导航 Swup 应该已经把侧栏整段替换成目标页面服务端渲染好的版本了; @swup/astro自带的 scripts 插件会自动重跑脚本、重新 hydrate 那些client:only的 Svelte 组件(侧栏里的 RouteSwitch / Categories / UmamiStats 都是),不需要硬刷新兜底。
于是我直接把整个 <script> 块删了。刷新问题确实消失了——然后新 bug 出现了。
第二段:目录(TOC)消失了
症状:删掉脚本后,点进任何一篇文章,左边侧栏不再显示目录了,而是继续显示主页那一套 widgets(分类、统计等)。
这时候我才重新仔细看了布局文件 MainGridLayout.astro,发现一个之前被我忽略的事实:
**#sidebar-sticky并不在 Swup 的容器列表里。**(DS干的好事)
Swup 当时只配置了这两个容器(astro.config.mjs):
swup({ containers: ["main", "#toc"], // ...})也就是说,Swup 在客户端导航时只替换 main 和 #toc 两块内容,侧边栏压根不参与替换。它永远停留在首次整页加载时的状态:
- 首次从主页加载 → 侧栏是 widgets;
- 之后无论你 Swup 导航到哪,侧栏都还是那套 widgets,不会变成文章的目录。
那原来的 updateSidebar 脚本到底是干嘛的?现在清楚了——它根本不是多余的,而是人工补齐了 Swup 没做好的事:
- 进文章页时,客户端从 DOM 现抠标题拼出目录;
- 离开文章页时,因为 widgets 是 Svelte 岛屿不好重建,干脆用
reload()让服务端重新渲染正确版本。
换句话说,那个烦人的刷新,是「侧栏不在 Swup 容器内」这个更底层问题的拙劣补丁。我之前删代码时只看到了补丁的丑,没看到补丁底下那个洞。
最终修复:把侧栏交给 Swup
既然根因是「侧栏没被 Swup 接管」,正确的做法就不是用 JS 手动打补丁,而是让 Swup 自己来替换它。把 #sidebar-sticky 加进容器列表即可:
swup({ containers: ["main", "#toc", "#sidebar-sticky"], // ...})改完之后,每次导航 Swup 都会把 sticky 侧栏整段换成目标页面服务端渲染好的版本:
- 文章页 → 显示
SideBarTOC(服务端按本页 headings 渲染,绝对不会错); - 非文章页 → 显示 RouteSwitch / Categories / UmamiStats;
- 文章 ↔ 文章 → 目录自动更新为当前文章。
Profile 组件在 #sidebar-sticky 之外(在 #sidebar 里),所以不会跟着被重复替换,避免每次导航头像都闪一下。那段 updateSidebar 脚本至此可以彻底删掉,也不需要 reload() 了。
顺手的安全网:transition.css
修这个 bug 的过程中还顺手处理了一个相关隐患。src/styles/transition.css 里原来有一条规则:
/* 之前 */html.js-loaded .onload-animation { opacity: 0;}它的本意是:元素默认透明,等 js-loaded 加到 <html> 上后,靠一次性的 fade-in-up 动画淡入。问题在于 js-loaded 只在首屏由 <head> 里的内联脚本加一次,而 Swup 只换容器、不重跑 <head>。所以客户端导航换进来的新内容,那个一次性动画不会可靠地重放,元素就卡在 opacity:0 看不见。
原来的 reload() 同样把这个问题盖住了——整页刷新会重置一切。既然现在不再依赖 reload,我就把这条静态 opacity:0 删了,让 .onload-animation 默认就是可见的,动画只在能跑的时候提供入场淡入:
/* 现在 */.onload-animation { animation: 300ms fade-in-up; animation-fill-mode: forwards;}html:not(.js-loaded) .onload-animation { animation: none; opacity: 1;}这样即使某次 Swup 导航后动画没重放,内容也按自然状态可见,不会再出现「内容凭空消失」。
复盘
一个 bug,三段修复,踩了两个坑:
- Swup 只替换
containers里的内容。 任何在客户端导航后「状态没更新」的怪现象,先想一下它所在的 DOM 节点到底在不在 Swup 容器里。不在容器里,它就会永远停留在首次加载的状态——这正是目录消失的真正原因。 - 删代码前,先确认它「为什么存在」。 那段
reload()看着像冗余的暴力兜底,实则是「侧栏不在容器内」这个架构问题的临时补丁。直接删掉而不解决根因,bug 只是换了个马甲(从「刷新」变成「目录不显示」)。 - 一次性入场动画 + 客户端路由 = 隐形风险。 凡是「靠 JS 加 class 才可见」的内容,都要想清楚在客户端导航后这个 class/动画是否还会按预期生效。最稳妥的做法是「默认可见,动画只是锦上添花」。
我想说的
只能说主责在DS,我还是太相信Vibe coding神力了直接将博客全权交给Claude Code(DS风味),导致写出一堆bug
不过,能跑就行((