Astro 修改(3) -- 为首页帖子加上自适应头图


在抄 Zed 的路上越走越远了,虽然还没用过 Zed 的编辑器,但是 Zed 的 Blog 的确赏心悦目,现在我的 Blog 几乎要 1-1 抄袭(致敬) 了 Zed。

当然,在抄作业的时候我也改进了一些设计,比如 Zed 的头图并没有明暗自适应,不会根据系统主题切换头图,而在我这里是有这个设计的。

概览

要给 Blog 加上头图大概需要改造以下这几个地方:

  • MD 文件中的 front-matter 用来指定一篇文章的头图
  • Astro Index 的模板文件 /src/pages/index.astro
  • 在某个地方增加明暗色切换头图的 JS

其中,在首页只展示最新的三篇文章。
毕竟一个个画以往文章的头图实在是有点难为人了,光是在以后给每个文章憋个头图都已经够累了。

MD 文件

在 MD 文件中,我选择的是在 front-matter 中增加一个 headImageLightheadImageDark 字段,分别指定明暗色的头图。

headImageLight: "Post999headImagLight.png"
headImageDark: "Post999headImageDark.png"

然后确保 Astro 能够解析到我们新添加的 front-matter,对于我来说,我这边只需要修改 zod 定义即可:

const post = defineCollection({
  loader: glob({ pattern: "**/[^_]*.md", base: "./src/blog" }),
  schema: ({ image }) =>
    z.object({
      author: z.string().default(SITE.author),
      pubDatetime: z.date(),
      modDatetime: z.date().optional().nullable(),
      title: z.string(),
      slug: z.string(),
      pinned: z.boolean().optional(),
      deprecated: z.boolean().default(false),
      tags: z.array(z.string()).default(["others"]),
      headImageLight: z.string().optional(),  
      headImageDark: z.string().optional(),  
      description: z.string(),
    }),
});

export const collections = { post };

Astro 模板

由于我的 Blog 有标记文章为 弃用 置顶 机制,相应的,在首页上展示的三篇文章也需要考虑这两个标记。

首先,在 Astro 模板的 front-matter 中需要将所有文章按照时间排序,排序优先级为 置顶 > 普通文章弃用 则完全不展示。 此外,还需要判断这些文章是否都配置了头图,一切妥当后将其塞入一个最大三个元素的列表中。

Index.astro
---
import { getCollection } from "astro:content";
import getSortedPosts from "@/utils/getSortedPosts";

const posts = await getCollection("posts");

const sortedPosts = getSortedPosts(posts);
const pinnedPosts = sortedPosts.filter(({ data }) => data.pinned && !data.deprecated);
const recentPosts = sortedPosts.filter(({ data }) => !data.pinned && !data.deprecated);

const homepagePosts = [...featuredPosts, ...recentPosts]
  .filter(({ data }) => 
    data.headImageLight && data.headImageLight !== "" &&
    data.headImageDark && data.headImageDark !== ""
  )
  .slice(0, 3);
---

// 略...
<div>
  <ImageCard />
</div>

然后在模板中我使用了一个 ImageCard 组件,方便复用代码,而这个组件也是这篇文章的核心。

首先编写 ImageCardfront-matter,我们需要知道文章的一切信息,比如 ID front-matter 等。
最方便的就是直接给整个 frontmatter 传入,取用需要的即可。

ImageCard.astro
---
const {
  title,
  description,
  headImageLight,
  headImageDark,
} = frontMatter;
---

至于导入传入的 headImageLightheadImageDark 就得去聊聊 Astro 了,或者是说聊聊 Astro 的依赖链了。

Astro 是基于 Vite 的,而 Vite 是基于 Rollup 的,而 Rollup 是基于 ESM 的,所以基于 Astro 的依赖链,我们有如下限制:

  • 所有图片必须导入,除非放在 /public 文件夹下,但这样一来会失去 Astro 提供的图片优化。
  • 所有图片必须使用 import 导入,而不能使用 require
  • 由于 RollUp 的安全限制,Import 不能包含变量。
  • 所有导入必须为相对或者绝对路径。

基于以上限制,可以将所有的头图放在一个特定的目录下,导入这个目录下的所有图片,然后筛选出自己要的图片即可。

import { getImage } from "astro:assets";
import type { ImageMetadata } from 'astro';

// 导入所有图片
const allImages = import.meta.glob<{ default: ImageMetadata }>('/src/data/assets/headImages/*', { eager: true });

const headImageLightPath = `/src/data/assets/headImages/${headImageLight}`;
const headImageDarkPath = `/src/data/assets/headImages/${headImageDark}`;

const headImageLightMeta = allImages[headImageLightPath]?.default;
const headImageDarkMeta = allImages[headImageDarkPath]?.default;

当然上述代码不会都放到 ImageCard.astro 中,比如导入所有图片的代码就可以放到 Index.astro 中,不然有几个首页文章就会导入多少次。导入后通过 Props 传递给 ImageCard 就好。
不过我估计就个人博客的这个体量,导入 114514 次都不会给性能带来多少影响。

在成功导入图片后就得去处理图片的路由和优化了。Astro 提供了一个 getImage() 方法,提供了这些操作。

至于 getImage 所接受的参数则可以查看 Astro Github 上的源码 types.ts
我们的头图并不需要特别高清,质量也不需要太高,所以我的参数的选择是这样的:

---
const format = "webp";
const width = 640;
const height = 400;
const quality = "low";

const headImageLightOptimized = await getImage({
  src: headImageLightMeta,
  format: format,
  width: width,
  height: height,
  quality: quality,
})
const headImageDarkOptimized = await getImage({
  src: headImageDarkMeta,
  format: format,
  width: width,
  height: height,
  quality: quality,
})
---

在我的设置下,可以将一张 2752x1297 体积为 ~1.7Mb 的图片压缩到 ~50Kb 左右,这样的体积非常完美。

拿到优化后的图片后就可以直接在下面引入使用了。

<picture>
    <source
      class="head-image"
      height={headImageDarkOptimized.attributes.height}
      width={headImageDarkOptimized.attributes.width}
      sizes={headImageDarkOptimized.attributes.sizes}
      media="@media (prefers-color-scheme: dark)"
      data-astro-image={headImageDarkOptimized.attributes['data-astro-image']}
      srcset={headImageDarkOptimized.attributes.src}
    />
    <img
      alt={title}
      height={headImageLightOptimized.attributes.height}
      width={headImageLightOptimized.attributes.width}
      loading={headImageLightOptimized.attributes.loading}
      decoding={headImageLightOptimized.attributes.decoding}
      fetchpriority={headImageLightOptimized.attributes.fetchpriority}
      sizes={headImageLightOptimized.attributes.sizes}
      data-astro-image={headImageLightOptimized.attributes['data-astro-image']}
      src={headImageLightOptimized.src}
    />
</picture>

插入一个切换头图的 JS

切换头图的思路和 Mermaid 切换的思路是一样的。参考 Astro 修改(2) — 为博客添加 Mermaid 支持

type Theme = "light" | "dark" | "system";

function updateHeadImage(theme: Theme): void {
    const mediaMap: Record<Theme, string> = {
      system: '(prefers-color-scheme: dark)',
      dark: 'all',
      light: 'none'
    };
    
    // class 是 head-image 的元素
    document.querySelectorAll('.head-image').forEach(el =>
      el.setAttribute('media', mediaMap[theme])
    );
}

updateHeadImage(theme);  // 加入到按钮被点击后触发的事件中


评论加载中……