Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5
1435 字
7 分钟
Hugo → Astro 记录第二次博客搬迁
2025-12-24
统计加载中...

这次搬迁的原因是看 MoScenix新博客主题太丝滑了,自己也想弄一个。不查不知道,astro框架下有不少开箱即用的博客,这个博客用了 Mizuki Next Theme。

安装依赖#

astro 是一个 js 框架,所以需要先保证 nodejs 的环境,另外需要 pnpm,如果是用了 homebrew 的 mac 那么只需要

brew install node
npm install -g pnpm

装完之后检查一下

node -v
npm -v
pnpm -v

本地调试#

直接 clone 主题的模板,然后安装需要的包

git clone https://github.com/matsuzaka-yuki/mizuki.git
cd Mizuki
pnpm install

直接

pnpm dev

就可以跑起来测试页了,默认端口是 4321,访问 http://localhost:4321 即可预览。

如果想构建用于在生产环境部署的版本需要

pnpm run build

默认会输出到 ./dist 目录下,预览构建出的静态网页

pnpm preview

如果需要直接部署到服务器上,把 ./dist 目录复制到 /var/www,用 nginx 或者 apache2 部署均可。

配置#

src/config.ts 中的配置,官方有中文文档,很详细。

我配置的过程中遇到了一些迷惑的问题

问题情况
toc 怎么设置好像都没有,很奇怪实际上是因为我的浏览器窗口不够宽,toc 被挤到右边屏幕外了
Twikoo 评论区的 IP 地址莫名其妙都在美国很诡异

迁移文章#

主要是这个文件头,下面是官方给的规范(然后我就发现了个问题,这个表格的有边框没有贴合😨,强迫症又要犯了。

AttributeDescription
titleThe title of the post.
publishedThe date the post was published.
pinnedWhether this post is pinned to the top of the post list.
priorityThe priority of the pinned post. Smaller value means higher priority (0, 1, 2…).
descriptionA short description of the post. Displayed on index page.
imageThe cover image path of the post.
1. Start with http:// or https://: Use web image
2. Start with /: For image in public dir
3. With none of the prefixes: Relative to the markdown file
tagsThe tags of the post.
categoryThe category of the post.
licenseNameThe license name for the post content.
authorThe author of the post.
sourceLinkThe source link or reference for the post content.
draftIf this post is still a draft, which won’t be displayed.

我要从 Hugo,根据这个表,需要动一下 Frontmatter,主要需要调整

  • date -> published,并删除引号和冗余的详细时间
  • categories -> category,只保留一个类别

另外文章内容也要微调一下

  • 内部 a 标签 /p/xxx -> /posts/xxx//tags/xxx -> /archive/?tag=xxx
  • 内部公式需要统一改成 $ 我是行内公式 $$$ 我是大公式 $$,不能用 \( \)\[ \] 组合

于是我让 GPT 生成了一个脚本处理了 Frontmatter,然后手动处理了一下公式

from pathlib import Path
from shutil import copy2, rmtree
import re
import ast
input_dir = Path.cwd() / "post"
output_dir = Path.cwd() / "output"
def extract_date(date_str: str) -> str:
"""
从 2025-03-02T15:09:48+0000 或 2025-07-26T17:11:04+08:00
提取 2025-03-02
"""
return date_str[:10]
def parse_yaml_front_matter(text: str):
"""
解析 --- YAML ---,返回 (meta_dict, body)
"""
parts = text.split("---", 2)
if len(parts) < 3:
return None, text
raw = parts[1].strip()
body = parts[2].lstrip()
meta = {}
for line in raw.splitlines():
if ":" not in line:
continue
k, v = line.split(":", 1)
meta[k.strip()] = ast.literal_eval(v.strip()) if v.strip().startswith(("[", "'", '"')) else v.strip()
return meta, body
def safe_eval(value: str):
"""
尝试 literal_eval,失败则作为普通字符串
"""
value = value.strip()
try:
return ast.literal_eval(value)
except Exception:
return value.strip("'\"") # 去掉可能的引号
def parse_ini_front_matter(text: str):
"""
解析 +++ INI/TOML 风格 +++
"""
parts = text.split("+++", 2)
if len(parts) < 3:
return None, text
raw = parts[1].strip()
body = parts[2].lstrip()
meta = {}
for line in raw.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
k, v = line.split("=", 1)
meta[k.strip()] = safe_eval(v)
return meta, body
def needs_quotes(s: str) -> bool:
return any(c in s for c in [":", "#", "\n", "\"", "'"])
def to_yaml_front_matter(meta: dict) -> str:
lines = ["---"]
for k, v in meta.items():
if isinstance(v, list):
lines.append(f"{k}: {v}")
elif isinstance(v, bool):
lines.append(f"{k}: {'true' if v else 'false'}")
elif isinstance(v, (int, float)):
lines.append(f"{k}: {v}")
elif isinstance(v, str):
if needs_quotes(v):
lines.append(f'{k}: "{v}"')
else:
lines.append(f"{k}: {v}")
else:
lines.append(f"{k}: {v}")
lines.append("---\n")
return "\n".join(lines)
def choose_category(title: str, categories: list[str]) -> str:
print("\n⚠️ 检测到多个 categories")
print(f"文章标题:{title}")
print("可选分类:")
for i, c in enumerate(categories, 1):
print(f" {i}. {c}")
while True:
try:
choice = int(input("请选择一个分类编号: "))
if 1 <= choice <= len(categories):
return categories[choice - 1]
except ValueError:
pass
print("输入无效,请重新输入。")
def normalize_meta(meta: dict, src_path: Path | None = None) -> dict:
new_meta = {}
title = meta.get("title")
if not title and src_path:
title = src_path.stem
for k, v in meta.items():
if k == "date":
new_meta["published"] = extract_date(str(v))
elif k == "draft":
continue
elif k == "categories":
if isinstance(v, list):
if len(v) == 1:
new_meta["category"] = v[0]
elif len(v) > 1:
chosen = choose_category(title or "(无标题)", v)
new_meta["category"] = chosen
else:
new_meta["category"] = v
else:
new_meta[k] = v
return new_meta
def process_md(src: Path, dst: Path):
text = src.read_text(encoding="utf-8")
if text.startswith("---"):
meta, body = parse_yaml_front_matter(text)
elif text.startswith("+++"):
meta, body = parse_ini_front_matter(text)
else:
dst.write_text(text, encoding="utf-8")
return
if meta is None:
dst.write_text(text, encoding="utf-8")
return
meta = normalize_meta(meta, src)
yaml_block = to_yaml_front_matter(meta)
dst.write_text(yaml_block + body, encoding="utf-8")
def main():
if output_dir.exists():
rmtree(output_dir)
output_dir.mkdir(parents=True)
for src in input_dir.rglob("*"):
rel = src.relative_to(input_dir)
dst = output_dir / rel
if src.is_dir():
dst.mkdir(parents=True, exist_ok=True)
continue
if src.suffix == ".md":
dst.parent.mkdir(parents=True, exist_ok=True)
process_md(src, dst)
else:
dst.parent.mkdir(parents=True, exist_ok=True)
copy2(src, dst)
if __name__ == "__main__":
main()

最近腾讯云有一个可以白嫖的 Pages,但是国内服务器 + 国内域名需要备案,等备完案之后更新下一篇。

部署到 nginx#

因为备案域名需要买服务器,所以我就 79R 买了个 2G + 2G 服务器,作为临时方案,我尝试先部署到服务器的 nginx 上。我本来想按照标准的流程先给服务器配个 Github 的 ssh,然后 clone 项目。发现他这个丐版服务器根本跑不了 pnpm run build,然后我突然灵机一动,我在本地构建一下然后挪到服务器上不就行了,于是喂给 GPT,就有了下面这个脚本。

#!/usr/bin/env bash
set -e
# ===== 配置区 =====
SERVER_USER=youruser
SERVER_HOST=your.server.ip
SERVER_DIR=/var/www/html
ARCHIVE=dist.tar.gz
# ==================
echo "===> Building..."
pnpm build
echo "===> Packing dist/"
rm -f $ARCHIVE
tar -zcvf $ARCHIVE dist
echo "===> Uploading to server..."
scp $ARCHIVE ${SERVER_USER}@${SERVER_HOST}:/tmp/
echo "===> Deploying on server..."
ssh ${SERVER_USER}@${SERVER_HOST} << 'EOF'
set -e
cd /tmp
# 备份旧站点
if [ -d /var/www/html ]; then
sudo rm -rf /var/www/html.bak
sudo mv /var/www/html /var/www/html.bak
fi
# 解压并部署
tar -zxvf dist.tar.gz
sudo mv dist /var/www/html
# 权限(按需)
sudo chown -R www-data:www-data /var/www/html || true
EOF
echo "===> Done!"
# 把包删了
rm -f $ARCHIVE

当然,如果还没成功备案,想把他解析到域名,那可能会

alt text

Hugo → Astro 记录第二次博客搬迁
https://starlab.top/posts/astro-blog/
作者
Star
发布于
2025-12-24
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时