mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3
2505 字
7 分钟
こんにちは、Astro
NOTE

newskin | 养病中低浮上

迁移背景#

自把敝站从 WordPress 迁到 Hexo,已三年有余。彼时 Astro 才刚刚发布第一个 Beta 版本不久,尚不为在下所知。直到 2023 年底,在下才终于了解到这个推行「群岛架构」的框架,不过当时的 Astro 社区生态还处于起步阶段,对于低技术力的在下来说,将其作为博客框架来用还为时尚早。况且,Volantis 主题的优化做得还不错,在下也没有在博客里塞进大量的 JavaScript 资源,因而 Lighthouse 评分还是看得过去的。另外,敝站的部署早就扔到了 Serverless 服务上运行,对部署时间的长短并不敏感,对服务器渲染更是没有要求,因此,没有必要追求那一点点性能提升而放弃现有成熟的解决方案。

虽然如此,在下依旧对 Astro 的 View Transitions API 印象深刻,无论是切换页面时的体验还是平滑的原生动画,都是老旧的 Hexo 打补丁所不能及的。因此,在下还是在心中暗自决定了下一代的敝站将会由 Astro 驱动,并为此持续关注着社区当中的新主题。

在下最早关注的主题是 fuwari,基于 MD3 风格的设计一下子抓住了在下的眼睛。不过 fuwari 依旧缺乏一些基本的博客功能,例如文章目录(现在已经支持)、评论模块和友链页面,虽然可以自己手搓,不过对于在下来说还是有些费时费力,仅仅只是将其拉到了本地略微研究了一番,便就此作罢。

随着时间的推移,更多有趣的主题开始涌现,其中基于 Hexo 主题 Shoka 设计的 koharu 吸引了在下的注意。Hexo 的 Shoka 本就是在下比较喜欢的主题,但在下同时也注意到更多基于 fuwari 修改的主题开始出现,例如 TwilightMizuki 等,都是在下比较中意的。最终,在下在左挑右拣中选定了 MD3 设计的 Mizuki。

迁移备忘#

比起上次从 WordPress 迁移出来,这次的迁移就要方便的多了。首先,由于文章都是以 MD 文档的形式储存,只需要简单的复制粘贴,改一下 Frontmatter,就可以直接用了,唯一需要注意的是 Volantis 主题的特色功能代码需要适配新主题,不过这充其量只是字符串替换,无论是手动 Ctrl + F 之后批量替换还是直接让 AI 修改都不算费事。

而在上次迁移花费最多精力的评论方面,由于之前使用的是 twikoo 评论系统,只需要引入前端 JS 并连接到之前的数据库,就可以像模块一样方便地在不同的博客之中插拔。

值得一提的是,Mizuki 给 twikoo 写的样式还是基于 1.7.6 的,而在在下部署博客的时候,发现其对当时 twikoo 最新的 1.7.13 的适配有问题,为此拷打了一番 AI,提交了 PR 把这个问题给解决了。

最终,在迁移过程中最花时间的项目落到了对主题的微调上,由于这是在下首次接触到 Astro 项目,花了一点时间才搞明白文件结构,特此把一些重要的修改项目列出来备忘。

字体#

Mizuki 之前的字体配置在 src/config/siteConfig.ts 中调整,不过后来由于一些问题,转而直接使用 Astro 原生的字体 API 配置,需要到 astro.config.mjs 中修改。

Astro 的字体 API 可以非常方便地引用敝站使用的谷歌字体:

export default defineConfig({
fonts: [
{
provider: fontProviders.google(),
name: "Noto Sans",
cssVariable: "--font-body",
weights: [400, 600],
},
{
provider: fontProviders.google(),
name: "Noto Sans SC",
cssVariable: "--font-cjk",
weights: [400, 600],
},
{
provider: fontProviders.google(),
name: "Lobster",
cssVariable: "--font-lobster",
},
]
});

需要注意的是 Mizuki 必须要有 --font-body--font-cjk 这两个 CSS 变量,修改的时候要确保这两个变量被正确配置,不然会报错。

在 astro.config.mjs 中引入之后,就可以在 src/styles/main.css 中进一步自定义了,例如敝站:

@theme {
--font-sans: var(--font-body), var(--font-cjk), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-display: var(--font-lobster), var(--font-body), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-title: var(--font-rampart-one), var(--font-body), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}

如果在 astro.config.mjs 定义了新的字体变量,不要忘记在 src/layouts/Layout.astro 中引入,否则不会有效果,例如敝站引入了 --font-lobster

<!doctype html>
<html
lang={siteLang}
class="bg-(--page-bg) text-[14px] md:text-[16px]"
data-overlayscrollbars-initialize
data-waves-config-enabled={siteConfig.banner?.waves?.enable ? "true" : "false"}
data-banner-title-config-enabled={siteConfig.banner?.homeText?.enable ? "true" : "false"}
data-post-list-layout-enabled={siteConfig.postListLayout?.enable ? "true" : "false"}
>
<head>
<meta charset="UTF-8" />
<Font cssVariable="--font-body" preload />
<Font cssVariable="--font-cjk" />
<Font cssVariable="--font-jetbrains-mono" />
<Font cssVariable="--font-lobster" />
<Font cssVariable="--font-rampart-one" />
...

顶栏菜单#

Mizuki 使用 Tailwind CSS,只需找到位置,调整起来还是很方便的。

  • 调小元素之间的间距:在 src/components/organisms/navigation/DropdownMenu.astro 的第 44 行及 106 行处分别调小 lg:px-5 的值
  • 防止左侧 LOGO 被挤压后整体下移:在 src/components/organisms/navigation/Navbar.astro 的第 103 行处删除 shrink-0
  • 二级菜单取消右侧保留空白:在 src/components/organisms/navigation/DropdownMenu.astro 的第 153 行处把 min-w-[12rem] 改为 min-w-max

永久链接#

Mizuki 的文章链接默认存在 src/contest/posts 下,使用默认的文件名作链接,默认链接路径为 /posts/文件名/ ,如果不想要前面的 /posts/ ,则需要开启 src/config/permalinkConfig.ts 中的 PermalinkConfig 功能。不过,即便开启永久链接之后,旧有的带 /posts/ 的链接依旧能访问到文章。

这里一些值得注意的问题,一是使用永久链接访问的文章,底部的上下文按钮依旧会导航到带 /posts/ 的链接;二是最底部的主题相关文章和随机文章模块不显示,排查后发现,主要原因出在组件内部的硬编码以及永久链接的路由组件缺失。

首先来到 src/utils/content-utils.ts,让模块和上下文按钮在生成数据时,就携带计算好的文章路径,先将第 45 行 getSortedPosts 处改为:

export async function getSortedPosts() {
const sorted = await getRawSortedPosts();
for (let i = 1; i < sorted.length; i++) {
sorted[i].data.nextSlug = getPostUrl(sorted[i - 1]);
sorted[i].data.nextTitle = sorted[i - 1].data.title;
}
for (let i = 0; i < sorted.length - 1; i++) {
sorted[i].data.prevSlug = getPostUrl(sorted[i + 1]);
sorted[i].data.prevTitle = sorted[i + 1].data.title;
}
return sorted;
}

再将第 382 行处开始的两个循环改为:

for (const s of withTagMatch) {
if (result.length >= maxCount) break;
result.push({
id: s.post.id,
data: s.post.data,
url: getPostUrl(s.post)
});
}
for (const s of withoutTagMatch) {
if (result.length >= maxCount) break;
result.push({
id: s.post.id,
data: s.post.data,
url: getPostUrl(s.post)
});
}

搞定了数据部分,再修改组件逻辑,使其能够读取传入的永久链接。先是上下文按钮的,来到 src/components/PostNavigation.astro 第 33 行处,将 <a> 标签作如下修改,这个链接对应「上一页」:

<a
href={prevSlug.startsWith('/') || prevSlug.startsWith('http') ? prevSlug : getPostUrlBySlug(prevSlug)}
class="w-full font-bold overflow-hidden active:scale-95"
>

第 52 行处的 <a> 标签也是同理,这个链接对应「下一页」:

<a
href={nextSlug.startsWith('/') || nextSlug.startsWith('http') ? nextSlug : getPostUrlBySlug(nextSlug)}
class="w-full font-bold overflow-hidden active:scale-95"
>

这样上下文按钮就改好了,接下来是相关文章和随机文章模块中携带的路径,来到 src/components/widget/PostListItem.astro 的第 34 行处,将 <a> 标签作如下修改:

<a
href={post.url || getPostUrl(post as any)}
class="group flex items-center gap-3 px-3 py-3 -mx-1 rounded-lg transition-all hover:bg-black/5 dark:hover:bg-white/5 active:scale-[0.98]"
>

最后,补全永久链接的页面路由中的相关文章和随机文章模块,来到 src/pages/[…permalink].astro,找到:

import {
LastModified,
PostMeta,
PostNavigation,
} from "@components/features/posts";

部分,在 PostNavigation, 后面补上 RandomPosts,RelatedPosts,,接着找到:

import { getSortedPosts } from "@utils/content-utils";

getSortedPosts 这个变量加到 getSortedPosts 后面,再找到 import { licenseConfig } from "src/config"; ,将其改为:

import {
licenseConfig,
randomPostsConfig,
relatedPostsConfig,
} from "src/config";

随后在 Frontmatter 结束前(--- 前)补充:

const relatedPosts = relatedPostsConfig.enable
? await getRelatedPosts(entry, relatedPostsConfig.maxCount)
: [];

最后,在最后(</MainGridLayout> 前)补上:

{
(relatedPostsConfig.enable || randomPostsConfig.enable) && (
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{relatedPostsConfig.enable && relatedPosts.length > 0 && (
<RelatedPosts relatedPosts={relatedPosts} />
)}
{randomPostsConfig.enable && (
<RandomPosts
excludeIds={[entry.id]}
maxCount={randomPostsConfig.maxCount}
/>
)}
</div>
)
}

到这里,问题就算全部解决。不过在下还建议顺便带 /posts/ 的链接加个重定向,来到 src/pages/posts/[…slug].astro,在第 22 行引入 getPostUrl

import { getFileDirFromPath, removeFileExtension, getPostUrl } from "@utils/url-utils";

再于第 97 行 const { entry } = Astro.props; 后加入判断就好了:

if (permalinkConfig.enable) {
const targetUrl = entry.url || getPostUrl(entry);
if (targetUrl && !targetUrl.startsWith("/posts/")) {
return Astro.redirect(targetUrl, 301);
}
}

另外,出于对 SEO 的考虑,建议修改 src/pages/robots.txt.ts,因为 Mizuki 的默认配置只允许首页和 /posts/ 下的地址被爬取:

import type { APIRoute } from "astro";
const robotsTxt = `
User-agent: *
Disallow:
Sitemap: ${new URL("sitemap-index.xml", import.meta.env.SITE).href}
`.trim();
/* 至少也要保证站点地图能被正常爬取
Disallow: /
Allow: /$
Allow: /posts/
Allow: /sitemap-index.xml
Allow: /sitemap-0.xml
Allow: /sitemap-1.xml(如果有)*/
export const GET: APIRoute = () => {
return new Response(robotsTxt, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
});
};

更新 Twikoo 前端#

Mizuki 自带的源在 /assets/js/twikoo.all.min.js,可以选择下载最新版本将其替换,后端不使用腾讯云云函数的只需要 twikoo.min.js 即可。

也可以直接引用官方 CDN:https://unpkg.com/twikoo/dist/twikoo.min.js,需要在 src/components/comment/Twikoo.astro 中替换 TWIKOO_SCRIPT_URL

鼠标指针#

也算是敝站为数不多的自 WordPress 时代以来一直传承下来的特性。直接在 src/layouts/Layout.astro 添加:

<style>
/* 1. 默认状态 */
html, body {
cursor: url('https://PATH_TO_YOUR_DOMAIN/normal.cur'), auto;
}
/* 2. 悬停与点击状态 */
a, button, [role="button"], summary, select, option {
cursor: url('https://PATH_TO_YOUR_DOMAIN/selecting.cur'), pointer;
}
a:active, button:active {
cursor: url('https://PATH_TO_YOUR_DOMAIN/selected.cur'), pointer;
}
/* 3. 文本输入与选择状态 */
input[type="text"], input[type="search"], textarea, [contenteditable="true"] {
cursor: url('https://PATH_TO_YOUR_DOMAIN/text.cur') 16 16, text;
}
/* 4. 等待/后台加载状态 */
.is-loading, .loading-state {
cursor: url('https://PATH_TO_YOUR_DOMAIN/waiting.cur'), wait;
}
</style>

适配动态页#

敝站的动态页基于 Mastodon embed timeline widget 制作,用于展示自己在 Mastondon 发表的内容,本质上是直接引用 JS。不过需要注意的是,Astro 需要把 JS 内容单独拿出来,用 script is:inline 放到 UI 孤岛中,否则将不会被加载。

另外,似乎必须完全刷新一次页面才能正确加载出页面内容,因此加入了强制刷新代码:

<script>
initMastodon();
document.addEventListener('astro:page-load', initMastodon);
</script>

最后是对主题颜色的适配:

<style is:global>
.mt-container,.mt-container[data-theme],.mt-dialog,.mt-dialog[data-theme] {
--mt-txt-max-lines: none;
--mt-preview-max-lines: none;
--mt-color-bg: rgba(255, 255, 255, 0);
--mt-color-bg-hover: rgba(233, 238, 241, 0);
--mt-color-line-gray: var(--line-color);
--mt-color-contrast-gray: var(--content-meta);
--mt-color-content-txt: var(--color-neutral-700);
--mt-color-hashtag: var(--primary);
--mt-color-link: var(--btn-content);
--mt-color-error-txt: var(--admonitions-color-caution);
--mt-color-btn-bg: var(--btn-regular-bg);
--mt-color-btn-bg-hover: var(--btn-regular-hover);
--mt-color-btn-txt: var(--btn-content);
--mt-color-backdrop: #00000090;
--mt-color-placeholder: #60698425
}
.dark .mt-container,.dark .mt-container[data-theme],.dark .mt-dialog,.dark .mt-dialog[data-theme] {
--mt-color-bg: rgba(255, 255, 255, 0);
--mt-color-bg-hover: rgba(233, 238, 241, 0);
--mt-color-content-txt: var(--color-neutral-300);
--mt-color-backdrop: #00000090;
--mt-color-placeholder: #60698425
}
</style>

迁移小结#

这次的迁移,在下大约花了一个周末的时间完成。除了上述必要的一些适配之外,在下还删掉了一部分文章封面,虽然封面确实能提高页面美观程度,但是给每一篇文章都加上封面反而让网站显得过于花哨,删掉一部分与文章相关不大的封面之后,看起来有了呼吸的空间,错落交致之间反而提升了整体的美观度;另一方面,也能变相加快网页的加载速度。

对于既往文章的修订当然也是必不可少的,不过对于这些琐碎的部分,就不再一一赘述。此外,由于架构迁移,旧有的一些功能,例如剧集页无法继续提供,不过有动态页的 NeoDB 消息补足,可以等之后再慢慢打磨上线。

在迁移的过程中,在下总是能迸发出最初搭建博客时的,发自内心的快乐,在电脑前一坐就是好几个小时,心无旁骛,无视外界的打扰,把食欲和困意也暂时放在一边,脑子里只考虑自己想要考虑的东西,进入全神贯注的心流状态。哪怕是结束之后只想瘫在床上被睡意裹挟,那种带给在下的充实感与满足感,也依然会从骨子里蔓延开来,告诉在下:博客在迁移,时代在变,但那份对折腾发自内心的热爱与专注,从未改变。

こんにちは、Astro
https://champhoon.xyz/log/hello-astro/
作者
澄沨
发布于
2026-07-25
许可协议
CC BY-NC-SA 4.0

目录