我是如何搭这个网站的

这篇文章按本站目前的实现重新整理建站过程:先搭好静态内容与路由,再做玻璃视觉、阅读层和搜索,最后加入克制动效与可回滚部署。附关键代码示例,也记录一些真正遇到过的问题。

目录
  1. 一、先说清楚:这个网站现在是什么架构
  2. 二、建立 Astro 项目,先让普通页面跑起来
  3. 1. 准备开发环境
  4. 2. 明确静态输出和地址规则
  5. 3. 把职责放进明确的目录
  6. 三、用 Markdown 管理内容,用集合约束字段
  7. 1. 一篇文章就是一个文件
  8. 2. 在构建时拦住错误内容
  9. 3. 只写一份查询规则
  10. 四、先做一套容易阅读的文章模板
  11. 五、搭建液态玻璃首页
  12. 1. 玻璃感来自背景、边缘和层次
  13. 2. 卡片不必同样大小
  14. 3. 主题要在首屏之前确定
  15. 六、让卡片打开阅读层,而不是跳走
  16. 1. 复用静态 HTML,不重复维护一份正文
  17. 2. 开卡片与切换文章,是两种动画
  18. 3. 分享地址也要打开正确内容
  19. 4. 阅读层中的样式必须已经加载
  20. 七、加入文章和作品搜索
  21. 1. 小站不必先搭搜索服务器
  22. 2. 搜索结果的位置比动效更重要
  23. 3. 修复时钟引起的搜索栏抖动
  24. 八、最后再加动效,不让动效决定布局
  25. 1. 果冻效果保持很小
  26. 2. 页脚只在接近真实底部时出现
  27. 3. 搜索只加状态反馈
  28. 九、构建、检查,然后交给 Nginx
  29. 1. 发布的是 dist,不是整个开发目录
  30. 2. Nginx 只负责提供静态文件
  31. 3. 先上传到暂存目录,再切换
  32. 4. 确认浏览器真的拿到了新版本
  33. 十、以后更新网站,按这个顺序做
  34. 十一、目前本站还有哪些缺点
  35. 1. 阅读层还没有完全符合用户的导航习惯
  36. 2. 上栏功能的边界仍然不够清楚
  37. 3. 搜索还是小规模内容下的基础实现
  38. 4. 视觉动效多,但反馈还需要收敛
  39. 5. 无障碍和移动端还不能算验证完成
  40. 6. 玻璃效果有性能和可读性的代价
  41. 7. 代码仍有试验阶段留下的整理债务
  42. 8. 内容、收录和运维也还没有补齐
  43. 接下来应该先改什么

一、先说清楚:这个网站现在是什么架构

这个网站不是在服务器上运行一个完整的前端应用,也没有数据库和在线编辑后台。它目前是一套 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 和脚本地址是本次构建的版本;
  • 搜索能够找到新文章;
  • 阅读层能够打开正文、使用目录并返回;
  • 手机上代码块不会撑破阅读区域。

十、以后更新网站,按这个顺序做

一次普通的文章更新不需要修改整套架构:

  1. 从文章模板复制新的 Markdown 文件,先保持草稿状态。
  2. 写正文,填写标题、摘要、日期和标签。
  3. 本地预览,检查长标题、目录、代码和链接。
  4. 设置为公开,执行类型检查、构建和静态产物验证。
  5. 备份线上版本,把新产物上传到暂存目录。
  6. 验证后切换,打开线上页面检查内容和交互。
  7. 用 Git 提交原始内容,保留构建锁文件和部署回滚记录。

Git 保存的是源代码与内容历史;服务器备份保存的是已经部署过的产物。两者不能互相替代。当前项目只有本地 Git 仓库,没有配置远程仓库,所以跨设备备份仍然是一项需要补齐的工作。

十一、目前本站还有哪些缺点

前面的步骤说明了网站如何实现,不代表它已经是一个成熟的产品。目前的本站更像一个持续迭代的个人设计实验:视觉方向逐渐稳定,但交互、内容和维护流程还没有达到成熟网站的程度。 能构建、能访问,只证明基础链路跑通,不能证明使用体验已经足够好。

1. 阅读层还没有完全符合用户的导航习惯

本站把博客、作品和联系内容都放进首页阅读层,视觉上比较统一,但也改变了常见网页的使用方式。用户通常会期待“点文章就进入文章页面”“浏览器后退就返回列表”“复制地址栏就能分享当前文章”。目前阅读历史主要由页面内的状态栈管理,和浏览器地址、前进后退不是一套完整同步的机制。

文章的原始路由可以分享,并会回到首页打开对应内容,但阅读层内的每次切换没有同步生成一个新的地址栏状态。因此,内部返回按钮虽然能用,浏览器原生导航和当前内容的对应关系仍然不够自然。这不是多加一个返回按钮就能彻底解决的问题,后续需要把内容路由、历史记录和阅读层状态统一起来。

2. 上栏功能的边界仍然不够清楚

上栏同时承担品牌入口、搜索、时钟和配色设置。搜索结果最终调整到了输入框下方,但它和悬停展开抽屉仍然共用一套状态。用户在输入、移出鼠标、切换焦点或打开配色时,可能面对不同的展开与收起时机,规则还不够简单。

时钟又兼作配色入口,也不是一个一眼就能理解的按钮。桌面悬停和手机点击的操作方式不同,需要进一步验证和统一。搜索位置与时钟宽度问题已经调整过,但这不能代替完整的交互测试。

3. 搜索还是小规模内容下的基础实现

当前搜索只匹配标题、摘要、简介和标签,不检索正文。它没有完整的相关性排序、错字容忍、分词、多关键词组合,也没有“上下方向键选结果、回车确认”的完整键盘选择机制。

对于目前几篇文章,这样足够做基础查找;内容增多后,构建时把所有条目直接放进首页 HTML,会增加首页体积,也未必能提供理想的查询体验。届时应该重新评估独立搜索索引,而不是把这个简单方案无限扩展。

4. 视觉动效多,但反馈还需要收敛

卡片跟随、阅读层展开、正文横向切换、上栏抽屉、底部页脚和搜索淡入都已存在。单独看每个效果可能合理,连续操作时却可能增加等待感和注意力负担。

特别是快速点击、请求较慢、正在切换内容时再次返回,以及长文章中的滚动恢复,都需要更充分的验收。动效参数已经尽量克制,但“少量位移”不自动等于“交互成熟”。后续应优先明确加载状态、错误恢复和操作反馈,再决定哪些动画值得留下。

5. 无障碍和移动端还不能算验证完成

本站已有减少动态效果处理、部分焦点恢复、语义化内容和按钮标签,但这不是完整的无障碍验收。阅读层的键盘焦点约束、屏幕阅读器提示、搜索结果语义和切换通知,仍需系统检查。

手机端还需要覆盖虚拟键盘打开、横竖屏切换、浏览器动态工具栏、小屏长标题等场景。静态构建检查无法判断这些行为,也无法替代真实设备和不同浏览器的测试。目前不能声称所有设备上的体验都已经稳定。

6. 玻璃效果有性能和可读性的代价

大面积背景模糊、饱和度处理、卡片遮罩与动画叠加,可能增加低性能设备的绘制负担。本站没有完成覆盖低端手机和多浏览器的性能基准测试,因此不能仅凭开发机器上流畅,就判断所有访客都能流畅使用。

半透明界面的文字对比也会受背景影响。代码块、摘要、小字号信息在深浅主题中都需要继续检查。后续可以为性能较弱的环境提供更简单的视觉方案,或者减少同时启用模糊的区域。

7. 代码仍有试验阶段留下的整理债务

上栏搜索经历了独立浮层、抽屉双列、输入框下方单列几个版本,一些旧 CSS 规则仍由后续规则覆盖,而不是全部清理掉。实验页与正式页也存在重复的上栏结构。核心交互集中在一个原生脚本里,方便早期调整,但功能继续增加后,状态之间的耦合会越来越明显。

这类问题不会必然让当前页面失效,却会让下一次修改更容易牵动其他地方。比如只调整搜索,看似无关的时钟宽度却影响了搜索栏位置。后续应该清理重复样式、收拢公共组件,并拆分相对独立的交互模块,而不是一直在文件末尾追加覆盖规则。

8. 内容、收录和运维也还没有补齐

目前公开作品为空,文章数量也不多,网站作为“作品集”的内容支撑还不充分。再精致的外壳,也不能代替真实项目、过程说明和持续更新。

为了统一阅读层体验,当前文章路由不参与独立搜索收录,这是明确的取舍,也是传播上的限制。若以后更重视博客的搜索可见性,需要调整页面访问策略。

维护方面,当前没有远程 Git 仓库、没有自动化发布流水线,证书也没有自动续期。发布有备份,但目录切换不是严格的零空档原子发布,也没有完整的自动回滚机制。这些比再加一种视觉效果更值得优先完善。

接下来应该先改什么

我的优先顺序是:

  1. 先让导航可靠:明确阅读层和浏览器前进、后退、分享地址的关系。
  2. 再让操作一致:收敛上栏、搜索、配色入口和返回按钮的状态规则。
  3. 补齐可用性验收:键盘操作、手机虚拟键盘、慢网络和错误恢复。
  4. 整理实现:清理旧样式、减少重复组件,给关键行为加入可重复的测试。
  5. 完善内容与维护:发布真实作品,配置远程备份、证书续期和更可靠的部署流程。
  6. 最后再调视觉细节:保留真正有帮助的动效,删除只增加复杂度的装饰。

这个网站现在可以使用,也能继续作为设计和前端实践的载体,但我不想把“能运行的实验”写成“已经成熟的方案”。记录不足,是为了下一次修改有明确目标,而不是每次只凭感觉换一种效果。

回头看,这个网站最重要的并不是玻璃模糊参数,也不是某一段漂亮动画,而是把内容、页面和交互分开:文章有稳定来源,页面能静态生成,阅读层复用现有内容,部署前有检查,部署后有退路。接下来还需要把这些分别可靠的部分,整合成真正连贯的使用体验。

如果要搭一个类似的网站,我建议也按这个顺序:先让内容能读,再让页面好看,最后让交互自然。 当这三件事分别可靠之后,液态玻璃和动效才会成为加分项,而不是维护负担。

标签

  • 个人网站
  • Astro
  • 前端开发
  • 液态玻璃
  • 静态部署