AstroBlog 的视觉样式分成两套需要同时维护的系统:CSS 负责静态外观,Swup 负责无完整刷新页面时的生命周期。真正容易出问题的是页面切换以后旧 DOM、旧 class 和旧事件监听仍然存在,因此这里重点说明样式入口、主题状态和页面过渡如何协作。
CSS 入口和层顺序
普通前台页面的全局入口是 src/styles/main.css,独立文章详情使用 src/styles/pages/reading.css。它不承载具体页面样式,只负责 Tailwind、预置样式、设计令牌和分层文件的导入。Layout.astro 在 HTML 中声明:
/* 各入口声明相同层顺序,组件 scoped 样式默认不分层。 */
@layer theme, reset, tokens, base, components, utilities, overrides;颜色、排版、间距、圆角和 z-index 等跨组件值放在 src/styles/tokens/;跨页面的基础元素放在 base/;公共组件放在 components/;具体页面放在 pages/;后台则有独立的 admin/ 分层。新增样式时先判断它属于令牌、公共组件还是页面特例,再选择对应文件。
颜色使用语义变量,而不是在组件里重复十六进制值:
.panel {
background: var(--card-bg);
color: var(--deep-text);
border: 1px solid var(--border-default);
}普通页面的 light 是蓝白主题,dark 是粉白主题,并非传统黑底;粉白主题通过 :root.dark 覆盖同一组变量。组件只消费 --card-bg、--deep-text、--border-default 等语义名,因此修改主题时不需要逐个搜索组件。新增共享配色须有对应授权,在 tokens/colors.css 维护并同步契约。字体角色与语言回退继续从 tokens/typography.css 读取,唯一 Web 字体加载入口是 fonts.css。
主题初始化流程
主题有 light、dark 和 system 三种状态。Layout.astro 的内联脚本在渲染前读取 localStorage,并把结果写入 document.documentElement;页面可见后,setting-utils.ts 的 setTheme() 负责更新 class、data 属性和存储值,initThemeListener() 监听系统主题变化。
首屏脚本不能依赖尚未加载的组件。主题基础状态必须在 <head> 阶段完成,否则页面会先以浏览器默认颜色绘制,再跳到博客主题。运行时脚本可以继续操作壁纸模式和现有设置,但不能重复定义主题状态。
独立文章样式
ReadingLayout.astro 输出 html[data-reading-page][data-swup-layout="reading"]。阅读页从 pages/reading.css 加载文章排版、媒体和阅读工具,从 tokens/reading.css 读取 --reading-* 固定白色配色;不导入 main.css 或 tokens/colors.css,也不运行主站主题和壁纸初始化。文章列表与数字分页继续使用普通布局。
阅读页共用既有字体、圆角和层级令牌,不另设字体体系。reading-language.ts 规范化 HTML 语言标签,保留 :lang() 字体回退。ReadingToc.astro 与 ReadingDock.astro 复用同一份 headings,由 reading-page.ts、reading-dock.ts 同步当前位置。
ArticleContent.astro 加载独立富内容样式和固定白色代码组件。代码由 Shiki 在构建期高亮,复制与换行由 code-block-controls.ts 管理;正文提示框、标题锚点、GitHub 卡片等渲染受 rehype-content-boundaries.mjs 限制。关于等共享 Markdown 保留旧排版和 Bonus 展开行为,唯一非文章例外是友链本站信息代码块。具体令牌与授权边界统一见仓库 AGENTS.md 的独立文章模块章节。
进入、离开文章及跨布局历史导航均使用完整页面导航。阅读文档不读写 localStorage.theme,返回主站仍使用原有偏好。文章评论区与侧栏保持当前实现,后续修改由用户单独指导。
Swup 的容器和生命周期
astro.config.mjs 中的 Swup 配置同时替换 #swup-container、Banner 覆盖层和左右侧栏动态容器。音乐页、后台、文章阅读页和其他独立页面被显式忽略,因为它们使用不同的布局骨架,不能把主站容器强行塞进去。
点击链接
↓
content:replace 替换容器并同步布局 class
↓
page:view 初始化主题、评论、Mermaid、目录等页面脚本点击阶段只处理导航反馈,不能过早查询新页面 DOM;content:replace 之后才依据新路径调整 Banner 高度和侧栏状态;page:view 适合启动依赖完整内容的功能。将初始化代码放在错误阶段,会出现内容已经替换但脚本仍引用旧节点的情况。
事件清理和重复初始化
需要在页面切换后重新运行的脚本,通常使用 AbortController 保存监听器生命周期。页面被替换时调用 cleanup,终止 resize、scroll、布局切换和自定义事件监听,再由新页面建立新的 controller。没有清理的监听器会在连续切换页面后叠加,表现为同一个请求发送多次或一个动画执行多遍。
文章列表的瀑布流就是一个典型例子。PostPage.astro 在脚本重新执行前调用上一次清理函数,监听 astro:before-swap,并在图片加载、窗口 resize、布局切换和封面可见性改变时重新计算。新增页面脚本时,应当沿用这种可取消的生命周期,而不是把匿名监听器永久挂到 window。
动画和可访问性
Swup 只负责容器切换,组件自己的动画仍由 CSS transition、requestAnimationFrame 或 GSAP 处理。页面过渡动画不能依赖元素高度变化,否则侧栏和 Banner 的尺寸变化会触发跳动。设置 prefers-reduced-motion 时应关闭不必要的位移、自动播放和复杂过渡,保留内容可见性和焦点移动。
导航和弹窗在页面切换后需要重新建立焦点关系。Swup 配置打开 accessibility,但具体组件仍要提供按钮语义、关闭方式和可见焦点。新增脚本时,不要只测试鼠标点击,还要测试键盘从旧页面切换到新页面后焦点是否落在可操作元素上。
修改样式时的检查顺序
修改颜色或字体,先查 src/styles/tokens/;修改卡片、按钮或浮层,查 components/ 和 z-index 令牌;修改单页布局,查 pages/;修改后台界面,查 admin/;修改页面切换逻辑,查 astro.config.mjs、Layout.astro 和相关组件的 cleanup。
# 检查 CSS 层级、令牌、z-index 和紧凑文字契约。
pnpm verify:styles
# 运行 Astro 模板和内容集合类型检查。
pnpm check
# 执行主站浏览器回归,包括 Swup、主题和首页壁纸场景。
pnpm verify:browser样式检查通过只能说明令牌和层级契约没有被破坏,不能替代浏览器回归。涉及 Swup、主题、侧栏或壁纸时,应在首页、文章列表和文章详情之间检查刷新、后退、前进、不同桌面窗口宽度和键盘焦点;本站不维护移动端兼容性。
层级令牌和浮层关系
浮动控制栏、搜索框、评论图片灯箱、后台弹窗和壁纸覆盖层都使用 src/styles/tokens/z-index.css 的语义变量。新增浮层时先确定它属于页面内容、导航、模态框还是全屏遮罩,再使用对应的 --z-* 令牌。直接写一个更大的 z-index 只能暂时解决遮挡,页面切换或嵌套弹窗时仍可能出现层级倒置。
Swup 脚本的最小模式
页面专用脚本应当有“初始化 + 清理”两部分:初始化查找当前容器并绑定监听器,清理取消监听器、计时器和动画。可以使用以下结构:
// 一个 controller 统一管理当前 Swup 页面创建的监听器。
const controller = new AbortController();
window.addEventListener('resize', recalculate, { signal: controller.signal });
// Swup 替换容器前统一中止,避免旧 DOM 监听器残留。
document.addEventListener('astro:before-swap', () => controller.abort(), { once: true });不要把 DOM 节点保存到全局变量后跨页面复用。Swup 替换容器后,旧节点已经不属于当前文档;缓存旧节点会造成事件触发但界面没有变化,甚至阻止垃圾回收。
CSS 和 Tailwind 的取舍
Tailwind 工具类适合页面级组合,分层 CSS 适合跨组件契约和复杂状态。一个组件同时需要主题变量、动画状态和响应式断点时,通常把基础布局放在组件样式,把颜色和层级放在 tokens,不要在模板中堆叠所有条件类。修改后要同时检查生产压缩,因为 Tailwind 只会保留扫描到的类名,动态拼接的类必须有静态候选或改用 CSS 变量。
页面切换后的表单和媒体
评论、搜索和音乐等组件可能持有输入值、AbortController 或 MediaElement。Swup 替换容器前要取消未完成的请求,替换后重新查找表单和媒体节点;如果播放器属于全局布局,则不能随着正文容器销毁。新增页面级表单时,先确认它在主容器还是独立布局中,再决定生命周期由组件自己管理还是由 Layout 统一管理。
样式回归清单
修改样式后,在桌面浏览器检查普通页面蓝白/粉白、独立白色阅读、后台亮/暗、首次加载与 Swup 切换,并核对完整导航返回、壁纸边界和字体实际来源。浏览器检查前先构建最新 dist/ 并准备 Chrome/Chromium;文章与共享 Markdown 隔离可分别运行 pnpm verify:browser --article-reading-only 和 pnpm verify:browser --content-boundary-only。verify:styles 只能检查约定的变量和层级,字体加载、文本溢出、焦点可见性和动画时序仍需要浏览器验证。