Astro 博客 URL 优化:从长路径到 NanoID 短链接

完成博客项目重构后,文章从平铺结构迁移到年月层级目录,文件管理问题解决了,但 URL 随之变长:原来的 /blog/25-11-24-16-00 变成了 /blog/2025/11/25-11-24-16-00,29 个字符,分享出去不太好看。

解决思路很直接:给每篇文章分配一个短 ID,路由优先使用这个 ID,没有的话降级回文件路径。选 NanoID 是因为它生成的 8 字符 ID 使用 64 字符集(A-Za-z0-9_-),约 281 万亿种组合,对个人博客来说碰撞概率可以忽略不计,而且 URL 安全,不需要额外编码。

改动概览

整个系统涉及四个地方:content schema 新增 slug 字段、动态路由读取 slug、文章创建脚本自动生成 slug、Admin 后台创建文章时同步生成。

Content schema 加一个可选字段:

// src/content.config.ts
schema: ({image}) =>
  z.object({
    // ... 其他字段
    slug: z.string().optional(),  // 可选,向后兼容旧文章
  })

动态路由的改动只有一行:

// src/pages/blog/[...slug].astro
params: {slug: post.data.slug || post.id}

有 slug 就用 slug,没有就用文件 id,旧文章的链接不会断。

文章创建脚本在生成 frontmatter 时顺带生成 slug:

// tools/scripts/new-post.js
import {nanoid} from 'nanoid';

const slug = nanoid(8);
// 写入 frontmatter: slug: 'w-nIiC2L'

Admin 后台同理,创建文章时如果 frontmatter 里没有 slug 就自动补上:

if (!frontmatter.slug) {
  frontmatter.slug = nanoid(8);
}

所有创建入口用同一套逻辑,不会出现某个入口漏掉的情况。

批量迁移现有文章

新文章会自动获得 slug,但存量文章需要批量处理。写了一个 tools/scripts/add-slugs.js 脚本,递归扫描 src/content/blog/ 下所有 .md 文件,跳过已有 slug 的,给其余的在 pubDate 字段后面插入一行 slug: 'xxxxxxxx'

function addSlugToFrontmatter(content, slug) {
  const lines = content.split('\n');
  const newLines = [];
  let slugAdded = false;

  for (const line of lines) {
    newLines.push(line);
    if (!slugAdded && line.startsWith('pubDate:')) {
      newLines.push(`slug: '${slug}'`);
      slugAdded = true;
    }
  }

  return newLines.join('\n');
}

npm run add-slugs 跑完,所有文章都有了 slug,构建出来的路由全部变成短链接格式。

踩到的几个坑

迁移过程中遇到了三个问题,都不复杂,但值得记下来。

Admin 后台 404。 文章路径里带斜杠(如 2026/01/26-01-07-10-37),Express 路由 /api/posts/:id 默认不匹配含斜杠的参数,导致请求 404。原因是代码里同时存在两个 /api/posts/:id 端点,旧的那个先匹配,新的支持路径的端点根本没机会执行。删掉旧端点,保留用 * 通配符的版本就好了:

app.get('/api/posts/:id(*)', ...)

TypeScript 类型报错。 astro.config.mjsrehypeKatexstrict 回调参数没有类型注解,tsc 报 TS7006。用 JSDoc 加一行注释解决:

strict: (/** @type {string} */ code) => ...

.env 没有生效。 Astro 在执行 astro.config.mjs 时不会自动加载 .env,导致 DEV_PORT 读不到,服务器一直在默认端口 3000 启动。在配置文件顶部手动解析 .env 文件并写入 process.env 就解决了。

几点收获

这次改动规模不大,但有几点值得记住。向后兼容不一定要复杂,一个 || 运算符就能让新旧两套逻辑共存,不需要迁移期、不需要重定向规则。批量迁移脚本写起来也比想象中简单,重点是”跳过已处理的”这个幂等逻辑,跑多少次结果都一样,不用担心重复执行。Express 路由的匹配顺序容易被忽略,路径参数里有特殊字符时要格外注意通配符的写法。