博客

我们为什么用 Astro 重写飞导航

飞导航原来使用 Hugo 和 WebStack 主题。新版保留链接数据,用 Astro 重做页面、内容校验和 Cloudflare 部署结构。

飞导航原来基于 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 中有两个标题叫 1151688,解析后成了数字,第一次检查当场报错。迁移脚本后来统一使用 String(link.title)。这类问题靠肉眼很难发现,构建检查倒是抓得很准。

页面为什么没有做成卡片墙

飞导航有两百多个链接。每个条目都放在带阴影、说明和按钮的卡片里,会让页面非常嘈杂。

新版每条链接只显示三样东西:

  1. favicon;
  2. 网站名称;
  3. 域名。

桌面端使用四列,较窄屏幕依次变成三列、两列和单列。条目之间只有细分隔线,鼠标经过时才出现很浅的背景。

这种布局没有视觉主角,扫起来却快。导航站先要让人找到网站,卡片是否抢眼反而排在后面。

搜索功能只做必要部分

首页搜索支持 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 静态部署分别负责页面与图标服务。

代价也很直接:改链接要重新构建,迁移统计得跟着维护,博客没有在线编辑后台。飞导航目前由个人维护,更新频率也不高,这些麻烦还在可接受范围内。