我是如何搭这个网站的
这篇文章按本站目前的实现重新整理建站过程:先搭好静态内容与路由,再做玻璃视觉、阅读层和搜索,最后加入克制动效与可回滚部署。附关键代码示例,也记录一些真正遇到过的问题。
目录
- 一、先说清楚:这个网站现在是什么架构
- 二、建立 Astro 项目,先让普通页面跑起来
- 1. 准备开发环境
- 2. 明确静态输出和地址规则
- 3. 把职责放进明确的目录
- 三、用 Markdown 管理内容,用集合约束字段
- 1. 一篇文章就是一个文件
- 2. 在构建时拦住错误内容
- 3. 只写一份查询规则
- 四、先做一套容易阅读的文章模板
- 五、搭建液态玻璃首页
- 1. 玻璃感来自背景、边缘和层次
- 2. 卡片不必同样大小
- 3. 主题要在首屏之前确定
- 六、让卡片打开阅读层,而不是跳走
- 1. 复用静态 HTML,不重复维护一份正文
- 2. 开卡片与切换文章,是两种动画
- 3. 分享地址也要打开正确内容
- 4. 阅读层中的样式必须已经加载
- 七、加入文章和作品搜索
- 1. 小站不必先搭搜索服务器
- 2. 搜索结果的位置比动效更重要
- 3. 修复时钟引起的搜索栏抖动
- 八、最后再加动效,不让动效决定布局
- 1. 果冻效果保持很小
- 2. 页脚只在接近真实底部时出现
- 3. 搜索只加状态反馈
- 九、构建、检查,然后交给 Nginx
- 1. 发布的是 dist,不是整个开发目录
- 2. Nginx 只负责提供静态文件
- 3. 先上传到暂存目录,再切换
- 4. 确认浏览器真的拿到了新版本
- 十、以后更新网站,按这个顺序做
- 十一、目前本站还有哪些缺点
- 1. 阅读层还没有完全符合用户的导航习惯
- 2. 上栏功能的边界仍然不够清楚
- 3. 搜索还是小规模内容下的基础实现
- 4. 视觉动效多,但反馈还需要收敛
- 5. 无障碍和移动端还不能算验证完成
- 6. 玻璃效果有性能和可读性的代价
- 7. 代码仍有试验阶段留下的整理债务
- 8. 内容、收录和运维也还没有补齐
- 接下来应该先改什么
一、先说清楚:这个网站现在是什么架构
这个网站不是在服务器上运行一个完整的前端应用,也没有数据库和在线编辑后台。它目前是一套 Astro 构建的中文静态个人网站:文章写在 Markdown 文件里,在本地构建成 HTML,再把构建产物交给 Nginx 提供访问。
首页是主要交互界面。介绍、作品、博客和联系分成四张玻璃卡片;点击卡片或搜索结果时,内容在当前页面的阅读层里打开。文章仍有真实的 HTML 路由,只是正常访问会回到首页,并在阅读层展开对应内容。
我把工作分成了三层:
| 层次 | 负责什么 | 使用什么 |
|---|---|---|
| 内容层 | 文章、作品、日期、标签、草稿 | Markdown、Astro 内容集合、Zod |
| 页面层 | 路由、文章模板、首页卡片、元信息 | Astro 组件和布局 |
| 交互层 | 阅读层、搜索、上栏、页脚、动效 | 原生 JavaScript 和 CSS |
部署流程则是:
Markdown / Astro / CSS / JavaScript
↓ 本地构建
dist 静态产物
↓ 上传、验证、切换
Nginx
↓
浏览器加载首页和阅读内容
下面按适合复现这套网站的顺序介绍。代码是关键实现的节选或简化演示,不是把整个项目拆成一篇文章;省略的错误处理、无障碍细节和动效状态会在相应章节说明。旧版本曾经使用过不同方案,这里只讲当前实现,不把旧的 Python 构建和三语内容混进来。
二、建立 Astro 项目,先让普通页面跑起来
1. 准备开发环境
当前项目使用 Astro 7.3.5,项目声明的 Node.js 要求为 >=22.12.0。真正安装时还应检查所选 Astro 版本及其依赖的引擎要求,使用满足要求的 Node.js 版本。
下面是通用终端里的初始化示例。pnpm 是包管理器;它用于安装依赖和执行项目脚本,不需要安装到生产服务器上。
pnpm create astro@latest portfolio
cd portfolio
pnpm install
pnpm astro add mdx sitemap
pnpm add @astrojs/rss
pnpm dev
@latest 会随时间变化。如果要严格复现某一次构建,需要保留项目的包版本和锁文件,而不是每次都重新安装最新依赖。已有项目通常直接执行 pnpm install --frozen-lockfile。
2. 明确静态输出和地址规则
本站保留 /zh/ 前缀,但只发布中文。配置的关键部分如下:
// astro.config.mjs:核心配置节选
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://huxuancheng.top',
output: 'static',
trailingSlash: 'always',
build: { inlineStylesheets: 'never' },
i18n: {
defaultLocale: 'zh',
locales: ['zh'],
routing: { prefixDefaultLocale: true },
},
integrations: [mdx(), sitemap()],
markdown: {
shikiConfig: {
themes: { light: 'github-light', dark: 'github-dark' },
wrap: true,
},
},
});
这里的 sitemap 配置只是入门示例。本站还会排除设计草案、实验页面,以及只作为阅读层数据源的路由,后面会解释原因。
trailingSlash: 'always' 让文章采用 /zh/blog/building-this-site/ 这样的目录式地址。保持路由、链接、canonical 和 Nginx 的文件查找规则一致,比上线后到处补重定向省事得多。
3. 把职责放进明确的目录
本站主要结构可以简化成:
portfolio/
├─ public/
│ └─ glass.js # 浏览器交互脚本
├─ src/
│ ├─ components/
│ │ ├─ GlassHeader.astro # 时钟、搜索、配色抽屉
│ │ └─ Prose.astro # Markdown 正文排版
│ ├─ content/
│ │ ├─ posts/ # 文章 Markdown
│ │ └─ works/ # 作品 Markdown
│ ├─ content.config.ts # 内容字段约束
│ ├─ layouts/BaseLayout.astro # 主题、元信息、公共资源
│ ├─ lib/content.ts # 内容查询、排序、路径工具
│ ├─ pages/[lang]/
│ │ ├─ index.astro # 首页
│ │ ├─ blog/index.astro # 博客列表
│ │ ├─ blog/[slug].astro # 文章详情
│ │ └─ works/ # 作品列表与详情
│ └─ styles/
│ ├─ tokens.css # 颜色、字体、间距变量
│ ├─ glass.css # 玻璃视觉和交互布局
│ └─ article.css # 阅读层里的文章样式
├─ scripts/verify.mjs # 静态产物检查
└─ templates/article.md # 新文章模板
先完成可独立渲染的列表和文章,再加首页阅读层。否则页面打不开时,很难判断是内容出了问题、请求失败,还是动画把它藏起来了。
三、用 Markdown 管理内容,用集合约束字段
1. 一篇文章就是一个文件
文章文件头部放元信息,下面写正文。新文章可以从这样的模板开始:
---
title: "一次界面设计的复盘"
description: "记录一次界面设计中的取舍与改进。"
summary: "从信息层级、交互反馈和视觉一致性三个角度复盘。"
published: "2026-10-06"
lang: zh
draft: true
tags:
- "界面设计"
---
## 背景
先说明问题,而不是直接展示结果。
## 实现
```css
.example {
border-radius: 20px;
}
```
draft: true 的内容不会进入公开列表。确认正文、日期、链接和代码没有问题后,再改成 false 并重新构建。修改 Markdown 不会自动修改服务器上的 HTML。
日期用字符串形式的 YYYY-MM-DD,保留首次发布日期;大幅重写时增加 updated,不要用新的发布日期覆盖历史。
2. 在构建时拦住错误内容
Astro 内容集合不仅负责读文件,也可以校验字段。本站的契约还包含封面替代文本、摘要长度等检查,下面保留最基本部分:
// src/content.config.ts:简化演示
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const date = z.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.refine((value) => !Number.isNaN(Date.parse(value)));
const posts = defineCollection({
loader: glob({
base: './src/content/posts',
pattern: '**/*.{md,mdx}',
}),
schema: z.object({
title: z.string().min(1).max(120),
description: z.string().min(1).max(220),
summary: z.string().max(400).optional(),
published: date,
updated: date.optional(),
lang: z.literal('zh'),
draft: z.boolean().default(false),
tags: z.array(z.string().min(1)).default([]),
}),
});
export const collections = { posts };
作品采用另一个集合,除了共同字段,还需要年份、角色、可选封面和外部链接等信息。当前作品集合还没有公开条目,因此首页显示空状态,而不是用虚构项目凑满卡片。
3. 只写一份查询规则
“过滤草稿”和“按日期排序”不应该在首页、列表、RSS 和搜索里各写一次。本站把它们集中在内容工具里:
import { getCollection } from 'astro:content';
export async function getPosts(lang: 'zh') {
const entries = await getCollection(
'posts',
({ data }) => !data.draft && data.lang === lang,
);
return entries.sort((a, b) => {
const delta = Date.parse(b.data.published)
- Date.parse(a.data.published);
return delta || a.data.title.localeCompare(b.data.title);
});
}
首页搜索也使用这份公开内容查询结果。这样草稿不会因为首页没显示,却意外出现在搜索索引里。
四、先做一套容易阅读的文章模板
文章页保持简单:标题、日期、更新日期、阅读时长、摘要、可折叠目录、正文、标签,以及上下篇导航。不把首页所有装饰都搬进正文。
Astro 动态路由在静态构建时,需要提前列出全部文章地址,再渲染对应内容:
---
// blog/[slug].astro:只演示路由与正文的关键部分
import { render } from 'astro:content';
import { getPosts } from '../../../lib/content';
import { slugOf } from '../../../content.config';
export async function getStaticPaths() {
const posts = await getPosts('zh');
return posts.map((entry) => ({
params: { lang: 'zh', slug: slugOf(entry.id) },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content, headings } = await render(entry);
---
<article>
<header>
<h1>{entry.data.title}</h1>
<time datetime={entry.data.published}>
{entry.data.published}
</time>
<p>{entry.data.summary ?? entry.data.description}</p>
</header>
<Content />
</article>
这是路由示例,不包含本站完整的公共布局、元信息、目录和上下篇逻辑。headings 可以用于生成目录,正文的代码块由构建阶段的语法高亮器处理,不需要浏览器再下载一套高亮库。
正文宽度控制在约 700px,桌面正文约 18px,手机适当缩小;代码和表格允许横向滚动。阅读页面不必追求塞满屏幕,适合连续阅读比“展示更多元素”重要。
五、搭建液态玻璃首页
1. 玻璃感来自背景、边缘和层次
单独加一个 backdrop-filter 并不等于玻璃。背后没有颜色变化时,模糊几乎看不出来;背景太实,玻璃会变成白色面板;文字对比太低,又无法阅读。
本站采用固定壁纸和遮罩,再叠加半透明卡片。下面是同类视觉的简化 CSS:
:root {
--glass-bg: rgb(255 255 255 / .62);
--glass-edge: rgb(255 255 255 / .66);
}
.wallpaper {
position: fixed;
inset: 0;
z-index: -2;
background:
radial-gradient(at 18% 25%, #b8c9ef, transparent 60%),
radial-gradient(at 82% 70%, #e8c6d5, transparent 65%),
#e9edf4;
}
.glass {
min-width: 0;
border: 1px solid var(--glass-edge);
border-radius: 28px;
background: var(--glass-bg);
-webkit-backdrop-filter: blur(20px) saturate(180%);
backdrop-filter: blur(20px) saturate(180%);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / .7),
0 16px 40px rgb(25 35 60 / .10);
}
@supports not (backdrop-filter: blur(1px)) {
.glass { background: #edf0f6; }
}
这段用渐变演示背景层次,并不是本站壁纸资源的完整实现。实际项目的颜色来自统一 CSS 变量,也会针对深色模式调整透明度和文字颜色。
2. 卡片不必同样大小
首页以介绍卡片作为视觉锚点,下面依次承接作品、博客和联系。博客列表共用一块玻璃,不给每一行都启用一层昂贵的背景模糊。最后的落款直接放在背景上,让页面自然收尾。
顶栏和页脚都是带边距的固定玻璃条。它们脱离文档流,所以内容区需要预留空间;但页脚平时只是一条收起的栏,不能按完整展开高度一直预留,否则页面末尾会多出很长一段无意义的滚动距离。
3. 主题要在首屏之前确定
主题切换可以保存到 localStorage,没有手动设置时跟随系统。本站把初始化脚本内联在公共布局的 <head>,先设置根节点属性,再渲染页面,减少“先亮后暗”的闪烁。
try {
const saved = localStorage.getItem('theme');
const theme = saved === 'light' || saved === 'dark'
? saved
: matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
document.documentElement.dataset.theme = theme;
} catch {
// 存储不可用时使用页面默认主题。
}
六、让卡片打开阅读层,而不是跳走
1. 复用静态 HTML,不重复维护一份正文
这里的关键取舍是:文章仍由 Astro 完整生成,而阅读层通过 fetch 获取文章 HTML,用 DOMParser 取出 <main>,再显示在首页内。
这样文章内容只有一个来源,不需要另外维护 JSON 正文,也不用引入整套客户端框架。
// 教学简化版:省略动画、历史栈和焦点恢复
const panel = document.querySelector('.vt-reading');
const body = panel.querySelector('.vt-reading-body');
let requestVersion = 0;
async function loadReading(href) {
const url = new URL(href, location.href);
if (url.origin !== location.origin) return;
const version = ++requestVersion;
panel.hidden = false;
body.textContent = '正在载入内容…';
try {
const response = await fetch(url.pathname);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const doc = new DOMParser().parseFromString(
await response.text(), 'text/html',
);
const main = doc.querySelector('main');
if (!main) throw new Error('没有找到正文');
main.querySelectorAll('script').forEach((script) => script.remove());
if (version !== requestVersion) return;
body.innerHTML = main.innerHTML;
panel.scrollTop = 0;
} catch {
if (version === requestVersion) {
body.textContent = '暂时无法载入此内容,请返回后重试。';
}
}
}
requestVersion 解决的是快速点击多个链接时,旧请求较晚返回、覆盖新内容的问题。关闭阅读层时也要让旧版本失效。
这个方式只用于本站自己构建的可信内容,不应该拿来插入任意外部网页。移除脚本并不等于对不可信 HTML 做了完整安全清洗。
2. 开卡片与切换文章,是两种动画
从首页首次进入时,阅读层外壳从卡片位置展开,其余卡片轻微下沉,顶栏与页脚退出。
已经在阅读层内,再进入一篇文章时,只做正文的水平切换。返回上一级时使用相反方向,不重放一次“从首页卡片展开”的动画。
本站保存了阅读历史中的 HTML、滚动位置和来源卡片。文章有“返回卡片”和“返回博客”,博客列表有“返回卡片”和“返回首页”。此外还要恢复原来的焦点,避免键盘用户返回后不知道自己在哪里。
3. 分享地址也要打开正确内容
直接访问文章路由时,本站的守卫会把地址转成首页深链接:
// 示例:这段在静态文章页 head 中执行
location.replace('/zh/#/blog/building-this-site/');
首页读取 hash,找到内容路径并打开阅读层,再清理 hash。阅读层通过 fetch 取文章时,只解析文档并提取 <main>,不会执行文章 <head> 中的守卫脚本。
这是一种交互选择,不是所有博客都必须这样设计。它让本站的视觉体验保持在首页内,但也意味着:当前这些带守卫的文章路由被标记为 noindex,并从 sitemap 排除。如果目标是让每篇文章获得独立搜索收录,应该允许文章作为完整页面访问,再把阅读层当作增强功能,而不是照搬这套守卫。
4. 阅读层中的样式必须已经加载
这是实际遇到过的问题:抓取文章时只拿 <main>,不会自动把原文档 <head> 中的 CSS 一起带回来。即使 HTML 上有 Astro 生成的样式标记,首页没加载对应样式,排版仍然会丢失。
因此本站在公共布局里加载共享的文章 CSS,用 .vt-reading .post-page 等选择器明确控制阅读层正文。不能只把文章样式放在详情页自己的局部 <style> 里。
七、加入文章和作品搜索
1. 小站不必先搭搜索服务器
目前内容量很小,搜索直接使用构建时生成的标题、摘要、简介和标签。不会搜索正文,也没有模糊匹配服务;它是大小写归一后的子串匹配。
搜索条目只来自非草稿内容。下面演示为一个条目写入可检索字段:
<a
class="vt-search-result"
data-search-text={`${post.data.title} ${post.data.summary ?? post.data.description} ${post.data.tags.join(' ')}`}
href={`/zh/blog/${slugOf(post.id)}/`}
>
<span>文章</span>
<strong>{post.data.title}</strong>
<small>{post.data.summary ?? post.data.description}</small>
</a>
输入时筛选这些条目,点击结果则进入同一套阅读层。作品尚未发布时没有作品结果,但新增公开作品并重新构建后,会一起进入搜索数据。
2. 搜索结果的位置比动效更重要
搜索最初用了独立下拉玻璃面板,与上栏原有的整体展开不协调。后来把结果放进抽屉,又曾经双列铺开,与配色设置挤在一起。最终采用了更直接的布局:
- 结果位于搜索框正下方;
- 左边缘与宽度跟随输入框;
- 单列展示标题和简短摘要;
- 有查询内容时隐藏配色设置;
- 结果仍在上栏玻璃外壳里,不另外套一块玻璃。
下面是对齐思路的节选:
function alignSearchResults() {
const field = searchInput.getBoundingClientRect();
const bar = header.getBoundingClientRect();
header.style.setProperty(
'--search-left',
`${field.left - bar.left - header.clientLeft}px`,
);
header.style.setProperty('--search-width', `${field.width}px`);
}
window.addEventListener('resize', alignSearchResults, { passive: true });
.vt-header-drawer .vt-search-results {
position: absolute;
left: var(--search-left);
width: var(--search-width);
}
真正上线时还要处理抽屉的定位上下文、内边距和高度,这两段只是对齐原理,不是完整布局。
3. 修复时钟引起的搜索栏抖动
顶栏右侧有实时更新的时钟。数字如果采用比例宽度,秒数从 11 变成 88 时,文本宽度可能变化。顶栏使用弹性布局,这个小变化会影响搜索栏的位置。
解决方式不是给搜索栏再加一层动画,而是稳定右侧空间:
.vt-header-toggle {
flex: 0 0 100px;
width: 100px;
box-sizing: border-box;
}
.site-clock {
display: inline-block;
width: 8ch;
white-space: nowrap;
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum';
}
手机端再为按钮设置适合窄屏的固定宽度。固定容器宽度是主要保障,等宽数字则让文字本身也更稳定。
八、最后再加动效,不让动效决定布局
1. 果冻效果保持很小
四张首页卡片会在鼠标进入后轻微跟随指针。实际效果限制在约 1.4px 的位移和 0.4 度左右的旋转,配合缓慢回到原位,而不是大幅扭曲卡片。
实现上用 pointermove 更新目标值,再由 requestAnimationFrame 平滑追踪。只在支持悬停、使用精确指针且没有开启减少动态效果的设备上启用。更强的变形曾经试过,但阅读性和质感反而变差,所以没有保留。
2. 页脚只在接近真实底部时出现
本站页脚有三个状态:普通位置隐藏、接近底部滑入为细条、到真实底部展开。核心判断如下:
const remaining = Math.max(
0,
document.documentElement.scrollHeight
- (window.scrollY + window.innerHeight),
);
document.documentElement.toggleAttribute(
'data-footer-reveal', remaining <= 420,
);
document.documentElement.toggleAttribute(
'data-footer-collapsed', remaining > 12,
);
420px 是接近底部时的显示窗口,12px 是允许滚动误差的底部阈值,不是提前把完整页脚展开。滚动事件由 requestAnimationFrame 合并处理,避免每个事件都重复执行布局计算。
3. 搜索只加状态反馈
搜索框聚焦时增强边框和背景,图标轻微变亮;结果出现时延迟约 70ms,做小幅淡入归位。新出现的匹配项只淡入,不逐条级联跳动。搜索框不再横向拉伸,已经确认的位置保持不变。
@keyframes search-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.search-results {
animation: search-in .34s cubic-bezier(.22,1,.36,1) 70ms both;
}
@media (prefers-reduced-motion: reduce) {
.search-results { animation: none; }
}
动画的作用是解释状态变化,不是证明页面“做了很多动画”。先把布局、焦点和滚动行为做好,再调动画的距离和时长。
九、构建、检查,然后交给 Nginx
1. 发布的是 dist,不是整个开发目录
日常检查流程是:
pnpm check
pnpm build:verified
check 检查 Astro 和 TypeScript;本站的 build:verified 会先运行 Astro 构建,再执行自定义的 scripts/verify.mjs。验证器检查断链、canonical、语言 alternate、sitemap 与 noindex、占位残留、资源体积、RSS 和结构化数据等。
这些检查不等于浏览器验收。上栏位置、移动端宽度、阅读层返回、代码块滚动、搜索筛选和减少动态效果,仍然需要实际打开网页检查。
2. Nginx 只负责提供静态文件
本站线上静态根目录是 /var/www/portfolio-v2。生产服务器不需要为这个站点运行 Node.js 开发服务器。
以下是 Nginx 配置思路的简化示例,不是线上完整配置。证书文件必须事先存在,监听规则、错误页和安全配置应按实际服务器调整:
server {
listen 80;
server_name huxuancheng.top;
return 301 https://huxuancheng.top$request_uri;
}
server {
listen 443 ssl;
server_name huxuancheng.top;
root /var/www/portfolio-v2;
index index.html;
ssl_certificate /etc/nginx/ssl/portfolio/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/portfolio/privkey.key;
location / {
try_files $uri $uri/ =404;
}
}
这里没有使用单页应用常见的 try_files ... /index.html。本站文章和栏目都有真实构建文件,不存在的路由应该返回 404,而不是所有错误地址都伪装成成功访问首页。
域名需要解析到服务器,开放必要端口,按部署地区完成备案要求,并正确配置 HTTPS。证书有有效期,不能把“已经启用 HTTPS”当成“以后不用维护”;当前站点还没有自动续期,续证仍需单独安排。
3. 先上传到暂存目录,再切换
实际发布时,我会先保存当前线上目录和压缩备份,再上传构建包到新的暂存目录,验证后切换到正式目录。不把构建中的临时文件直接写进正在访问的站点。
下面是 Linux 服务器端的发布骨架。压缩包已经提前上传,路径和权限需要按自己的服务器核对;它不包含传输命令,也没有自动失败回滚:
set -eu
stamp=$(date +%Y%m%d-%H%M%S)
live=/var/www/portfolio-v2
stage=/var/www/portfolio-v2-stage-$stamp
backup=/var/www/portfolio-v2-prev-$stamp
# 先确认现有目录,建立压缩备份。
test -d "$live"
mkdir -p /root/motion-backups
tar -czf "/root/motion-backups/site-$stamp.tar.gz" \
-C /var/www portfolio-v2
# 在独立目录中解压,不修改线上文件。
mkdir "$stage"
tar -xzf /tmp/portfolio-dist.tar.gz -C "$stage"
test -f "$stage/zh/index.html"
nginx -t
# 检查暂存目录的权限和内容后,再执行切换。
mv "$live" "$backup"
mv "$stage" "$live"
nginx -s reload
需要说明一个边界:连续两次 mv 之间存在极短的目录空档,这不是严格意义上的单次原子切换,也不等于失败会自动回滚。更严格的零空档发布可以让 Nginx 指向 current 符号链接,用同文件系统内的原子重命名替换链接;这属于可以继续改进的方案,不是本站当前已完成的机制。
回滚时应先核对备份路径,保留当前失败版本,再把确认无误的备份恢复到正式目录。不要在没有检查路径的情况下直接删除线上目录。
4. 确认浏览器真的拿到了新版本
本站的交互脚本位于 public/glass.js,构建后文件名不变。因此修改脚本时,会在公共布局里更新查询版本,例如 glass.js?v=版本标记,避免浏览器继续使用旧脚本。Astro 构建出来的 CSS 则通过带指纹的资源地址引用。
部署后至少确认:
- 首页和文章数据源能正常返回;
- 更新后的正文已经出现,不只是首页能打开;
- CSS 和脚本地址是本次构建的版本;
- 搜索能够找到新文章;
- 阅读层能够打开正文、使用目录并返回;
- 手机上代码块不会撑破阅读区域。
十、以后更新网站,按这个顺序做
一次普通的文章更新不需要修改整套架构:
- 从文章模板复制新的 Markdown 文件,先保持草稿状态。
- 写正文,填写标题、摘要、日期和标签。
- 本地预览,检查长标题、目录、代码和链接。
- 设置为公开,执行类型检查、构建和静态产物验证。
- 备份线上版本,把新产物上传到暂存目录。
- 验证后切换,打开线上页面检查内容和交互。
- 用 Git 提交原始内容,保留构建锁文件和部署回滚记录。
Git 保存的是源代码与内容历史;服务器备份保存的是已经部署过的产物。两者不能互相替代。当前项目只有本地 Git 仓库,没有配置远程仓库,所以跨设备备份仍然是一项需要补齐的工作。
十一、目前本站还有哪些缺点
前面的步骤说明了网站如何实现,不代表它已经是一个成熟的产品。目前的本站更像一个持续迭代的个人设计实验:视觉方向逐渐稳定,但交互、内容和维护流程还没有达到成熟网站的程度。 能构建、能访问,只证明基础链路跑通,不能证明使用体验已经足够好。
1. 阅读层还没有完全符合用户的导航习惯
本站把博客、作品和联系内容都放进首页阅读层,视觉上比较统一,但也改变了常见网页的使用方式。用户通常会期待“点文章就进入文章页面”“浏览器后退就返回列表”“复制地址栏就能分享当前文章”。目前阅读历史主要由页面内的状态栈管理,和浏览器地址、前进后退不是一套完整同步的机制。
文章的原始路由可以分享,并会回到首页打开对应内容,但阅读层内的每次切换没有同步生成一个新的地址栏状态。因此,内部返回按钮虽然能用,浏览器原生导航和当前内容的对应关系仍然不够自然。这不是多加一个返回按钮就能彻底解决的问题,后续需要把内容路由、历史记录和阅读层状态统一起来。
2. 上栏功能的边界仍然不够清楚
上栏同时承担品牌入口、搜索、时钟和配色设置。搜索结果最终调整到了输入框下方,但它和悬停展开抽屉仍然共用一套状态。用户在输入、移出鼠标、切换焦点或打开配色时,可能面对不同的展开与收起时机,规则还不够简单。
时钟又兼作配色入口,也不是一个一眼就能理解的按钮。桌面悬停和手机点击的操作方式不同,需要进一步验证和统一。搜索位置与时钟宽度问题已经调整过,但这不能代替完整的交互测试。
3. 搜索还是小规模内容下的基础实现
当前搜索只匹配标题、摘要、简介和标签,不检索正文。它没有完整的相关性排序、错字容忍、分词、多关键词组合,也没有“上下方向键选结果、回车确认”的完整键盘选择机制。
对于目前几篇文章,这样足够做基础查找;内容增多后,构建时把所有条目直接放进首页 HTML,会增加首页体积,也未必能提供理想的查询体验。届时应该重新评估独立搜索索引,而不是把这个简单方案无限扩展。
4. 视觉动效多,但反馈还需要收敛
卡片跟随、阅读层展开、正文横向切换、上栏抽屉、底部页脚和搜索淡入都已存在。单独看每个效果可能合理,连续操作时却可能增加等待感和注意力负担。
特别是快速点击、请求较慢、正在切换内容时再次返回,以及长文章中的滚动恢复,都需要更充分的验收。动效参数已经尽量克制,但“少量位移”不自动等于“交互成熟”。后续应优先明确加载状态、错误恢复和操作反馈,再决定哪些动画值得留下。
5. 无障碍和移动端还不能算验证完成
本站已有减少动态效果处理、部分焦点恢复、语义化内容和按钮标签,但这不是完整的无障碍验收。阅读层的键盘焦点约束、屏幕阅读器提示、搜索结果语义和切换通知,仍需系统检查。
手机端还需要覆盖虚拟键盘打开、横竖屏切换、浏览器动态工具栏、小屏长标题等场景。静态构建检查无法判断这些行为,也无法替代真实设备和不同浏览器的测试。目前不能声称所有设备上的体验都已经稳定。
6. 玻璃效果有性能和可读性的代价
大面积背景模糊、饱和度处理、卡片遮罩与动画叠加,可能增加低性能设备的绘制负担。本站没有完成覆盖低端手机和多浏览器的性能基准测试,因此不能仅凭开发机器上流畅,就判断所有访客都能流畅使用。
半透明界面的文字对比也会受背景影响。代码块、摘要、小字号信息在深浅主题中都需要继续检查。后续可以为性能较弱的环境提供更简单的视觉方案,或者减少同时启用模糊的区域。
7. 代码仍有试验阶段留下的整理债务
上栏搜索经历了独立浮层、抽屉双列、输入框下方单列几个版本,一些旧 CSS 规则仍由后续规则覆盖,而不是全部清理掉。实验页与正式页也存在重复的上栏结构。核心交互集中在一个原生脚本里,方便早期调整,但功能继续增加后,状态之间的耦合会越来越明显。
这类问题不会必然让当前页面失效,却会让下一次修改更容易牵动其他地方。比如只调整搜索,看似无关的时钟宽度却影响了搜索栏位置。后续应该清理重复样式、收拢公共组件,并拆分相对独立的交互模块,而不是一直在文件末尾追加覆盖规则。
8. 内容、收录和运维也还没有补齐
目前公开作品为空,文章数量也不多,网站作为“作品集”的内容支撑还不充分。再精致的外壳,也不能代替真实项目、过程说明和持续更新。
为了统一阅读层体验,当前文章路由不参与独立搜索收录,这是明确的取舍,也是传播上的限制。若以后更重视博客的搜索可见性,需要调整页面访问策略。
维护方面,当前没有远程 Git 仓库、没有自动化发布流水线,证书也没有自动续期。发布有备份,但目录切换不是严格的零空档原子发布,也没有完整的自动回滚机制。这些比再加一种视觉效果更值得优先完善。
接下来应该先改什么
我的优先顺序是:
- 先让导航可靠:明确阅读层和浏览器前进、后退、分享地址的关系。
- 再让操作一致:收敛上栏、搜索、配色入口和返回按钮的状态规则。
- 补齐可用性验收:键盘操作、手机虚拟键盘、慢网络和错误恢复。
- 整理实现:清理旧样式、减少重复组件,给关键行为加入可重复的测试。
- 完善内容与维护:发布真实作品,配置远程备份、证书续期和更可靠的部署流程。
- 最后再调视觉细节:保留真正有帮助的动效,删除只增加复杂度的装饰。
这个网站现在可以使用,也能继续作为设计和前端实践的载体,但我不想把“能运行的实验”写成“已经成熟的方案”。记录不足,是为了下一次修改有明确目标,而不是每次只凭感觉换一种效果。
回头看,这个网站最重要的并不是玻璃模糊参数,也不是某一段漂亮动画,而是把内容、页面和交互分开:文章有稳定来源,页面能静态生成,阅读层复用现有内容,部署前有检查,部署后有退路。接下来还需要把这些分别可靠的部分,整合成真正连贯的使用体验。
如果要搭一个类似的网站,我建议也按这个顺序:先让内容能读,再让页面好看,最后让交互自然。 当这三件事分别可靠之后,液态玻璃和动效才会成为加分项,而不是维护负担。