飞导航原来基于 Hugo 和 WebStack 主题。用了几年后,页面结构、样式和内容已经很难拆开。继续改主题也能做,只是每动一处都要顾着旧结构。
这次重写只保留链接内容,页面和构建流程改用 Astro。Hugo 本身没有问题,我们只是需要一个更容易调整的页面骨架。
这次重写要解决什么
动手前先把范围定下来:
- 原站链接内容继续使用,不重新手工录入;
- 页面改成更安静的列表,不再使用带描述的大卡片;
- 搜索只保留 Google、Bing 和 Grok;
- favicon 统一从自己的缓存服务获取;
- 网站最终部署到 Cloudflare Pages;
- 旧 Hugo 版本保留在 Git 历史中,需要时可以回退,不再把旧主题文件留在新版目录里。
继续修改 WebStack 也能做出相似的外观,数据迁移、类型校验和博客功能却还得另外补。换成 Astro 后,链接和文章可以走同一套内容系统。
没有直接删除旧数据
原链接保存在:
data/webstack.yml
新版页面不直接读取这个 YAML,而是在每次开发和构建前运行迁移脚本:
data/webstack.yml
↓
scripts/migrate-links.mjs
↓
src/data/links.json
↓
Astro Content Collection
迁移脚本会把旧数据压平成页面需要的结构。每条链接都有分类、分组、显示顺序、标题和网址:
links.push({
category: category.taxonomy,
categoryOrder,
section: group.term,
sectionId,
sectionOrder,
linkOrder,
title: String(link.title),
url: String(link.url),
});
目前迁移结果是:
- 243 条链接;
- 236 个不同网址;
- 7 个一级分类;
- 22 个链接分组。
脚本会核对这四个数字。改完链接却忘了更新预期结果,构建会直接失败。规则有些死,但能拦住 YAML 缩进错误,免得一次少掉几十条链接还没发现。
内容调整也放在迁移阶段
重写过程中,我们删除了一些不再需要的旧分组,把两个“在线工具”合并成一个,并去掉了重复的 TinyPNG。
旧 YAML 里还留着一些历史分组,哪些内容进入新站由迁移脚本决定。例如:
const excludedSections = new Set([
"设计相关\u0000设计工具",
"产品经理\u0000图形创意",
"产品经理\u0000界面设计",
"产品经理\u0000交互动效",
"产品经理\u0000在线工具",
"产品经理\u0000Chrome插件",
]);
旧内容暂时不会丢,不过迁移规则会越写越多。新版稳定后,最终生成的数据应该升为主数据源,到时就可以删掉一批兼容旧结构的代码。
Astro 如何校验这些链接
生成的 JSON 会进入 Astro Content Collection。我们为链接定义了明确结构:
const links = defineCollection({
loader: file("src/data/links.json"),
schema: z.object({
category: z.string().min(1),
categoryOrder: z.number().int().nonnegative(),
section: z.string().min(1),
sectionOrder: z.number().int().nonnegative(),
linkOrder: z.number().int().nonnegative(),
title: z.string().min(1),
url: z.url(),
}),
});
旧 YAML 中有两个标题叫 115 和 1688,解析后成了数字,第一次检查当场报错。迁移脚本后来统一使用 String(link.title)。这类问题靠肉眼很难发现,构建检查倒是抓得很准。
页面为什么没有做成卡片墙
飞导航有两百多个链接。每个条目都放在带阴影、说明和按钮的卡片里,会让页面非常嘈杂。
新版每条链接只显示三样东西:
- favicon;
- 网站名称;
- 域名。
桌面端使用四列,较窄屏幕依次变成三列、两列和单列。条目之间只有细分隔线,鼠标经过时才出现很浅的背景。
这种布局没有视觉主角,扫起来却快。导航站先要让人找到网站,卡片是否抢眼反而排在后面。
搜索功能只做必要部分
首页搜索支持 Google、Bing 和 Grok。切换搜索引擎时只改变表单地址:
const providers = [
{ label: "Google", action: "https://www.google.com/search" },
{ label: "Bing", action: "https://www.bing.com/search" },
{ label: "Grok.com", action: "https://grok.com/" },
];
选择结果会保存在浏览器中,Command / Control + K 可以快速聚焦搜索框。联想词、历史记录和站内搜索暂时没做,首页现在用不上。
favicon 在构建时同步
每个链接组件会提取域名,然后请求主站自己的静态文件:
/favicons/{domain}.png
图标在部署时从 Google 同步到 public/favicons/,随后与 Astro 页面一起交给 Cloudflare Static Assets。以后换上游只需要修改同步脚本,不用改页面。完整实现记录在《我们如何在构建时生成 favicon》中。
博客为什么也放进 Astro
增加博客后,链接数据和 Markdown 文章都由 Astro Content Collection 管理。博客不需要后台数据库,新增文章只需在 src/content/blog/ 放入一个 Markdown 文件。
飞导航本来就通过 Git 更新内容,文章也沿用这条路:提交、构建检查、静态部署。没必要再养一套 CMS。
这次重写得到什么
目前项目可以生成首页、关于页、博客列表、文章详情和 404 页面。生产构建输出普通静态文件,可以直接交给 Cloudflare Pages。
现在各部分的分工很清楚:
- 旧 YAML 负责保存原始链接;
- 迁移脚本负责整理和校验;
- Content Collection 负责类型安全;
- Astro 组件负责页面展示;
- Cloudflare 静态部署分别负责页面与图标服务。
代价也很直接:改链接要重新构建,迁移统计得跟着维护,博客没有在线编辑后台。飞导航目前由个人维护,更新频率也不高,这些麻烦还在可接受范围内。