NOTE
迁移背景
自把敝站从 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 修改的主题开始出现,例如 Twilight、Mizuki 等,都是在下比较中意的。最终,在下在左挑右拣中选定了 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.xmlAllow: /sitemap-0.xmlAllow: /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 消息补足,可以等之后再慢慢打磨上线。
在迁移的过程中,在下总是能迸发出最初搭建博客时的,发自内心的快乐,在电脑前一坐就是好几个小时,心无旁骛,无视外界的打扰,把食欲和困意也暂时放在一边,脑子里只考虑自己想要考虑的东西,进入全神贯注的心流状态。哪怕是结束之后只想瘫在床上被睡意裹挟,那种带给在下的充实感与满足感,也依然会从骨子里蔓延开来,告诉在下:博客在迁移,时代在变,但那份对折腾发自内心的热爱与专注,从未改变。




