avatar

鸡排的首页和博客现已基于 Astro 重新呈现


最近看博客很不爽😕,想翻新一下前端,本来叮叮当当搞了一个 Nextjs + Hexo 的组合,但是后来发现还是很麻烦,最后连 Hexo 也删掉,终于舒服了。

谈谈为什么要换

在上一次重构的时候,我的博客从 WordPress 换到了 Hexo,主要动机源于 我不想写 PHP 主题。然后我搬到 Hexo,写了一个基于 Pug 和 Stylus 的主题,而在前端生态又蓬勃发展了几年后,这种糟糕的 DX 已经不再能满足我了。为什么我不能用 JSX 和 Tailwind 来写我的博客主题呢?

Next.js 很好,但有人拖后腿

在这篇文章发布前,我的博客其实已经由 Next.js + Hexo 的方案试运行了几个月时间。但我依然不是满意,主要原因在以下几个方面:

  1. Hexo 内置的 WareHouse JSON 数据库和相关 API 使用体验不佳。

  2. 本地编写草稿时还要处理 Hexo 自身的文件缓存、Next.js 的刷新等等问题。

  3. Hexo API 给出的实例上的很多属性其实是个 getter,需要显式调用一下。

上面提到的一些问题,你都可以通过学习(抄) SukkaW 的代码来解决,我目测全网所有的 Next.js + Hexo 这个标题的文章里对于 Hexo 初始化和数据处理的 lib 都是来自 SukkaW 文章里的那份。 可以说 SukkaW 的这两篇文章是真正的开山鼻祖了: (要是没了他我都不知道该怎么办)

使用 Next.js + Hexo 重构我的博客

React Server Component 初体验与实践 —— 将博客迁移到 Next.js App Router

这之后我丢着博客一段时间没管,也算是冷静思考了一下,然后我问了自己一个问题:“留着 Hexo 的用途是什么?”

答案是显而易见的:前端渲染的工作已经交由别处,而内容本身就作为 markdown 而存在,我所需要的已经都有了,Hexo 对于现在的我来说就是没有意义的,我只需要让前端框架直接读 markdown 就行了。

你好 Astro

在作出移除 Hexo 的决定后,我现在需要我的框架来直接读取 markdown 作为数据,其实 Next.js 也支持这么干,但是隔壁 Astro 的以下特性实在太吸引我了:

  • 提供内容集合的简单处理 API
  • 自带分页功能的动态路由
  • 官方支持的 RSS 和 Sitemap 生成
  • 对 视图过渡动画 (View Transitions) 的官方支持
  • Shiki 支持的 Markdown 代码高亮

我认为这些特性都是博客场景下的重点需求,Next.js 并非做不到以上说的任何一点,但是 Astro 开箱即用。

比如自带分页功能的动态路由,你把数据集和PageSize喂给它,Astro 会将每一页的数据和常见的Pagination属性都作为页面的Props传入,不需要开发者再自行处理数据切分相关的逻辑。

还有 视图过渡动画,对于 Next.js 这样的全栈框架来说支持起来也许很困难,就像这个 discussions 一样: Add support for View Transition API #46300,但是对于 Astro 而言,两行代码就可以启用支持。

剩下的 代码高亮、RSS、Sitemap 功能虽然都是选配,但是作为官方内置支持功能可以在你需要的时候轻松集成,不需要第三方社区插件和长篇大论的独立配置文件。

就此时此刻而言,我认为如果你的需求是几乎没有 JS 部分的纯内容网站,那么 Astro 就是你现在能买到的最好的现代前端化的 SSG 框架。

开工

起步

强烈推荐使用 Astro 的 cli 脚手架创建带有博客模版的项目,不仅附带填充数据,RSS 和 Sitemap 都已启用,只需要按需微调。

定义博客 Schema

首先删除./src/content/blog/中的预填充文章,将你的文章都复制到这个目录下,有子目录也没有关系。

然后按需修改 ./src/content/config.ts中的 schema 定义,对于用过 Zod 或者类似校验库的朋友们来说简直不能再熟悉了。

const blog = defineCollection({
  type: "content",
  schema: z.object({
    title: z.string(),
    description: z.string().optional(),
    pubDate: z.coerce.date().optional(),
    updatedDate: z.coerce.date().optional(),

    // 这些都是 Hexo 的老朋友
    date: z.coerce.date(),
    permalink: z.string(),
    categories: z.array(z.string()).optional().nullable(),
    tags: z.array(z.string()).optional().nullable(),
    photos: z.array(z.string()).optional().nullable(),
    draft: z.boolean().optional(),
  }),
});

不出意外地话,你现在可以使用 getCollection("blog")方法来获取你的博客数据了。

分页

Astro 内置非常强大的分页功能支持,以最简单的文章列表分页为例:

假定列表页面都位于 page/目录下,新建一个 src/pages/page/[page].astro

export async function getStaticPaths({
  paginate,
}: {
  paginate: PaginateFunction;
}) {
  const posts = await getCollection("blog");
  return paginate(posts, { pageSize: 10 });
}
// 所有分页数据都在 "page" 参数中传递
const { page } = Astro.props;

console.log(page.data.length, page.data);

现在 page.data 就是一个长度为 10 的文章数组,直接在下方的模版中使用这个变量 Map 生成文章卡片即可。对于分页器而言,可以直接使用 page.url.nextpage.url.prev,如果你还有更多需求,这是Page的类型说明:

export interface Page<T = any> {
  /** result */
  data: T[];
  /** metadata */
  /** the count of the first item on the page, starting from 0 */
  start: number;
  /** the count of the last item on the page, starting from 0 */
  end: number;
  /** total number of results */
  total: number;
  /** the current page number, starting from 1 */
  currentPage: number;
  /** number of items per page (default: 25) */
  size: number;
  /** number of last page */
  lastPage: number;
  url: {
    /** url of the current page */
    current: string;
    /** url of the previous page (if there is one) */
    prev: string | undefined;
    /** url of the next page (if there is one) */
    next: string | undefined;
  };
}

Markdown 样式

自己处理 Markdown 样式太痛苦了,你可以使用 Tailwind 的预设 Markdown 样式: tailwindcss-typography再简单微调一下,就可以得到非常不错的效果!

具体的步骤可以直接看官方文档:recipes/Tailwind Typography

代码高亮

自带的代码高亮主题好像有点丑,在这里挑一个你喜欢的然后在配置文件里换上:

// astro.config.mjs
import { defineConfig } from "astro/config";

export default defineConfig({
  markdown: {
    shikiConfig: {
      theme: "one-light",
    },
  },
});

RSS 和 Sitemap

RSS 和 Sitemap 已经默认开启,因此你只需要修改 astro.config.mjs中的 site url 和 consts中的网站名称和描述即可。

如果你也想要一个像点我一样好看的 rss.xml 样式,在这里下载文件并放到 public/rss/styles.xml

// rss.xml.js
export async function GET(context) {
  const posts = await getAllPost();
  return rss({
    title: SITE_TITLE,
    description: SITE_DESCRIPTION,
    site: context.site,
    stylesheet: "/rss/styles.xsl",
    items: posts.map((post) => ({
      title: post.data.title,
      pubDate: post.data.date,
      // 这里我禁用了 RSS 里的 description 和 customData 生成,修改了 link 的路径
      //   description: post.data.description,
      //   customData: post.data.customData,
      link: `/${post.data.permalink}`,
    })),
  });
}

视图过渡

只需两行,请在你的 Layout.astro 中导入并使用即可:

import { ViewTransitions } from "astro:transitions";
<html lang="en">
  <head>
    ...
    <ViewTransitions />
  </head>
  <body>
  </body>
</html>

最后

如果你还需要更多的动态路由页面,比如 Tag 和 Category,这两种一般是嵌套分页,可以参阅文档:嵌套分页

你还可以给自己加一个 404 报错页面: 404 页面

致谢

本文数次修改,早期成稿时曾参考:

使用 Next.js 与 Hexo 重构博客系统(原文已 404)