新主题 Akiba

新年新气象,趁着年前有空,把用了两年多的主题换了。这次是直接把博客驱动从 Hexo 换成了 Astro,主题则是自己从 Dante 修改过来的。如果你喜欢,可以来 GitHub Repo 下载并替换里面的文章以及页面(这是我的博客项目,并不是主题文件,后期可能会将博客独立出来)。今天就用一篇文档来记录一下这次换主题的过程。

我为什么要从 Hexo 换到 Astro?这个问题其实很好回答,因为前期我也有 Hexo 主题开发的经验,整个开发流程下来让我感觉,Hexo 的主题开发实在是太麻烦了,它的主题只是普普通通地利用一个个的 html 模板来生成,组件复用性并不高。而同为 SSG 框架的 Astro 它拥有基于群岛和组件的设计,便于组件复用。它还支持和其他前端框架搭配使用,且开发流程与目前主流框架无益,上手非常快。同时 Astro 的国际化文档确实写得很好(也有我的一部分功劳w),所以我觉得换到 Astro 是一个不错的选择。

Astro 直接看中文文档就能很快地上手,所以这次换主题的过程还是挺顺利的。不过最开始的时候我将旧 Hexo 博客的所有日志搬到这里的时候,还是遇到了一些问题,这里就记录一下。原主题仅支持英文,不支持中文标签,在调试阶段运行 npm run dev 时,并不会报错,但在每次打开某些文档,以及打开 Tags 页面的时候,会出现严重错误。同时本地构建静态文件的时候,控制台也会弹出错误,中断构建。于是尝试分析问题原因,在每次报错时都会在控制台输出以下提示:

00:00:00 [ERROR] Expected "slug" to match "[^\/#\?]+?", but got ""
  Stack trace:
    at ~\astro-tailwind-blog\node_modules\path-to-regexp\dist\index.js:229:27
    [...] See full stack trace in the browser, or rerun with --verbose.

可以推测是因为某些路由的 slug 为空,导致了正则匹配失败,但是 Astro 并没有给出问题所在。那么,我们需要知道,到底是哪些 slug 导致了这个问题?

在解决这个问题之前,我们首先需要知道,Astro 的路由是如何生成的。在 Astro 中,路由是通过 src/pages 下的文件以及层级架构来生成,比较像 Next 的路由方式,比如 src/pages/blog/[slug].astro 会生成 /blog/:slug 这样的路由,所以我们可以通过这个信息来定位问题。首先,我们直接找到出现问题的页面文件,先使用 WebStorm 的 search in files 功能,找到了项目文件夹下所有包含 slug 的代码文件,其中这次问题出现在了 tags 的二级页面上,即 src/pages/tags/[slug]/[...page].astro。在这个文件里,对路径的处理是这样的:

---
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
    const posts = (await getCollection('blog')).sort(sortItemsByDateDesc);
    const tags = getAllTags(posts);

    return tags.flatMap((tag) => {
        const filteredPosts = getPostsByTag(posts, tag.slug);
        return paginate(filteredPosts, {
            params: { slug: tag.slug },
            pageSize: siteConfig.postsPerPage || 4
        });
    });
}
---

它导出了一个异步的函数 getStaticPaths()。这是一个 Astro 的 API,Astro 官方给出的用法是这样的:

如果页面在文件名中使用动态参数,该组件将需要导出一个 getStaticPaths() 函数。

必须要有该函数,因为 Astro 是静态站点生成器。这意味着整个网站是预构建的。如果 Astro 不知道在构建时生成什么页面,你的用户在访问你的网站时就看不到它。

---
export async function getStaticPaths() {
  return [
    { params: { /* 必需 */ }, props: { /* 可选 */ } },
    { params: { ... } },
    { params: { ... } },
    // ...
  ];
}
---
<!-- 你的 HTML 模板在这里 -->

getStaticPaths() 函数应该返回对象数组,以确定哪些路径会被 Astro 预渲染。

大概的问题已经被发现了,即是这个函数返回的数组中,有一个对象的 params 属性为空,导致了路由生成失败。那么,我们就直接去寻找这个 tag.slug 的定义,在 src\utils\data-utils.ts 中,我们找到了这个函数:

export function getAllTags(posts: CollectionEntry<'blog'>[]) {
    const tags: string[] = [...new Set(posts.flatMap((post) => post.data.tags || []).filter(Boolean))];
    return tags
        .map((tag) => {
            return {
                name: tag,
                slug: slugify(tag)
            };
        })
        .filter((obj, pos, arr) => {
            return arr.map((mapObj) => mapObj.slug).indexOf(obj.slug) === pos;
        });
}

从中不难看出就是这个 slugify() 函数导致了问题,显然它是用来处理非正常标签的,比如 C++ 这样的标签,它会将 C++ 转换为 c,但是这个函数的正则表达式有问题,它会将中文字符也当成非正常标签了,导致了路由生成失败。其实这个时候有两种解决方法,一种是修改正则表达式,另一种是直接将中文标签改为英文标签。我首先尝试了第二种方法,但这并不是一个好的解决方法,因为这样会导致原本的中文标签失效,而且这个问题还会在其他地方出现。所以我还是选择了修改正则表达式。

继续去寻找这个函数的定义,它在 src\utils\common-utils.ts 中:

export function slugify(input?: string) {
    if (!input) return '';

    // make lower case and trim
    var slug = input.toLowerCase().trim();

    // remove accents from charaters
    slug = slug.normalize('NFD').replace(/[\u0300-\u036f]/g, '');

    // replace invalid chars with spaces
    slug = slug.replace(/[^a-z0-9\s-]/g, ' ').trim();

    // replace multiple spaces or hyphens with a single hyphen
    slug = slug.replace(/[\s-]+/g, '-');

    return slug;
}

这个函数的正则表达式是 /[^a-z0-9\s-]/g,它会将所有非英文字符都替换为空格,如果我们只使用中文标签,那直接将 slug = slug.replace(/[^a-z0-9\s-]/g, ' ').trim();注释掉即可。

ok,问题暂时解决了,再次尝试构建,成功通过~