在抄 Zed 的路上越走越远了,虽然还没用过 Zed 的编辑器,但是 Zed 的 Blog 的确赏心悦目,现在我的 Blog 几乎要 1-1 抄袭 了 Zed。
当然,在抄作业的时候我也改进了一些设计,比如 Zed 的头图并没有明暗自适应,不会根据系统主题切换头图,而在我这里是有这个设计的。
概览
要给 Blog 加上头图大概需要改造以下这几个地方:
- MD 文件中的
front-matter用来指定一篇文章的头图 - Astro Index 的模板文件
/src/pages/index.astro - 在某个地方增加明暗色切换头图的 JS
其中,在首页只展示最新的三篇文章。
毕竟一个个画以往文章的头图实在是有点难为人了,光是在以后给每个文章憋个头图都已经够累了。
MD 文件
在 MD 文件中,我选择的是在 front-matter 中增加一个 headImageLight 和 headImageDark 字段,分别指定明暗色的头图。
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 中需要将所有文章按照时间排序,排序优先级为 置顶 > 普通文章,弃用 则完全不展示。 此外,还需要判断这些文章是否都配置了头图,一切妥当后将其塞入一个最大三个元素的列表中。
---
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 组件,方便复用代码,而这个组件也是这篇文章的核心。
首先编写 ImageCard 的 front-matter,我们需要知道文章的一切信息,比如 ID front-matter 等。
最方便的就是直接给整个 frontmatter 传入,取用需要的即可。
---
const {
title,
description,
headImageLight,
headImageDark,
} = frontMatter;
---至于导入传入的 headImageLight 和 headImageDark 就得去聊聊 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); // 加入到按钮被点击后触发的事件中