写在前面
用了很久 Typecho 之后,我最终还是把博客迁移到了基于 Nuxt 4 和 Nuxt Content 的 blog-v3。
Typecho 很轻、很稳,也足够适合个人博客。它的优势很明显:部署简单、后台好用、插件丰富、服务器要求低。但随着博客内容越来越多,我对博客系统的需求也慢慢变了:
- 想把文章变成 Git 仓库里可管理的 Markdown 文件;
- 想用 VS Code / Cursor / 任意编辑器写文章,而不是登录后台;
- 想把主题、组件、页面、样式都放进同一个工程里维护;
- 想接入现代前端生态,比如 Nuxt、Vue、Vercel、自动构建;
- 想让博客部署更接近“提交即发布”的工作流。
于是就有了这次迁移。
这篇文章不是单纯的“我换了个主题”,而是一次从传统 PHP + 数据库博客,迁移到 Markdown + Git + Nuxt Content 静态/服务端渲染博客的完整记录。下面会尽量详细写清楚每一步,包括备份、导出、转换、图片处理、链接保持、评论迁移和部署上线。
迁移前后架构对比
先看一下迁移前后的差异。
Typecho 时代
Typecho 的典型结构大概是这样:
Nginx / Apache
└─ PHP
└─ Typecho
├─ MySQL / SQLite 数据库
├─ usr/themes/主题
├─ usr/plugins/插件
└─ usr/uploads/附件
文章、页面、评论、分类、标签基本都在数据库里。图片等附件在 usr/uploads 目录。主题和插件则在 usr/themes、usr/plugins。
这种架构的优点是成熟稳定,后台操作方便;缺点是内容和程序耦合较深,迁移、版本管理、批量修改都不如纯文本文件直观。
blog-v3 时代
迁移后的结构更接近现代前端博客:
blog-v3 ├─ content/ │ ├─ posts/ │ │ └─ 2026/ │ │ └─ typecho-to-blog-v3.md │ └─ link.md ├─ app/ │ ├─ pages/ │ ├─ components/ │ └─ app.config.ts ├─ blog.config.ts ├─ nuxt.config.ts └─ package.json
文章直接放在 content/posts/年份/ 下面,每篇文章就是一个 Markdown 文件,顶部用 Front matter 写标题、日期、分类、标签等元信息。
比如本文的文章头部就是这样:
--- title: 从 Typecho 迁移到 blog-v3:一次把博客搬进 Nuxt Content 的完整记录 description: 记录从 Typecho 迁移到 blog-v3 / Clarity 的完整过程,包括备份、文章导出、Markdown 转换、图片迁移、链接保持、评论处理、本地预览和 Vercel 部署。 date: 2026-05-19 12:45:00 updated: 2026-05-19 12:45:00 categories: [技术] tags: [Typecho, Nuxt, Nuxt Content, 博客迁移, Vercel] ---
从此以后,文章就是代码仓库的一部分,可以用 Git 记录变更,也可以通过 Vercel 自动构建发布。
第一步:迁移前一定要完整备份
迁移博客之前,最重要的不是转换工具,而是备份。
建议至少备份三类东西:
- 数据库;
- 附件目录;
- Typecho 配置、主题和插件。
备份数据库
如果你使用的是 MySQL,可以在服务器上执行:
mysqldump -u 数据库用户 -p 数据库名 > typecho-backup.sql
例如:
mysqldump -u typecho -p typecho > typecho-2026-05-19.sql
如果你不确定数据库名,可以查看 Typecho 根目录下的 config.inc.php:
$db = new Typecho_Db('Mysql', 'typecho_');
$db->addServer(array (
'host' => 'localhost',
'user' => 'typecho',
'password' => 'password',
'charset' => 'utf8mb4',
'port' => '3306',
'database' => 'typecho',
), Typecho_Db::READ | Typecho_Db::WRITE);
里面的 database 就是数据库名,prefix 通常是 typecho_。
如果使用 SQLite,则一般只需要备份对应的 .db 文件。
备份附件目录
Typecho 的图片和附件通常在:
usr/uploads/
可以打包:
tar -czf typecho-uploads.tar.gz usr/uploads
也可以直接用 rsync 下载到本地:
rsync -avz root@你的服务器:/网站目录/usr/uploads ./typecho-uploads
备份主题和插件
虽然这次迁移到 blog-v3 后,Typecho 主题和插件基本不会继续使用,但它们里面可能有一些自定义代码、统计脚本、友链配置、备案信息、导航链接等内容。
建议一起备份:
tar -czf typecho-theme-plugin.tar.gz usr/themes usr/plugins config.inc.php
备份完成后,再开始动手迁移,这样即使中间转换失败,也随时可以回滚。
第二步:准备 blog-v3 项目
如果还没有 blog-v3,可以先克隆项目:
git clone https://gh-proxy.com/https://github.com/L33Z22L11/blog-v3.git cd blog-v3
安装依赖:
pnpm install
本地启动:
pnpm dev
默认情况下,文章内容放在:
content/posts/年份/
例如:
content/posts/2026/hello-world.md
blog-v3 使用 blog.config.ts 管理站点基础信息,比如站点标题、作者、邮箱、站点地址、分类、文章类型等。迁移前建议先把这些基础配置改好。
例如:
const basicConfig = {
title: '心记|Mood',
subtitle: '繁华已尽,空散云烟',
description: '繁华已尽,空散云烟',
author: {
name: 'MoodLog',
avatar: 'https://q1.qlogo.cn/g?b=qq&nk=80295940&s=640',
email: 'admin@moodlog.cn',
homepage: 'https://blog.moodlog.cn/',
},
url: 'https://blog.moodlog.cn/',
timeZone: 'Asia/Shanghai',
defaultCategory: '未分类',
}
这里建议优先确认三项:
title:站点名称;url:正式域名;timeZone:时区,国内站点一般用Asia/Shanghai。
第三步:从 Typecho 导出文章
Typecho 文章主要存储在数据库的 contents 表中,分类和标签关系在 relationships、metas 等表中。
如果只是少量文章,可以手动复制。但如果文章比较多,建议写脚本批量导出。
方式一:通过数据库导出
先确认表名。Typecho 默认表前缀一般是 typecho_,文章表通常叫:
typecho_contents
可以查询文章:
SELECT cid, title, slug, created, modified, text, type, status FROM typecho_contents WHERE type = 'post' AND status = 'publish' ORDER BY created DESC;
其中几个字段很关键:
| 字段 | 说明 |
|---|---|
cid | 内容 ID |
title | 文章标题 |
slug | 文章缩略名,常用于固定链接 |
created | 创建时间,Unix 时间戳 |
modified | 修改时间,Unix 时间戳 |
text | 正文内容 |
type | post 是文章,page 是独立页面 |
status | publish 是已发布 |
如果要导出分类和标签,还需要关联 relationships 和 metas 表。
一个常见查询大概是:
SELECT c.cid, c.title, c.slug, c.created, c.modified, c.text, GROUP_CONCAT(m.name) AS metas FROM typecho_contents c LEFT JOIN typecho_relationships r ON c.cid = r.cid LEFT JOIN typecho_metas m ON r.mid = m.mid WHERE c.type = 'post' AND c.status = 'publish' GROUP BY c.cid ORDER BY c.created DESC;
不过 Typecho 的 metas 同时包含分类和标签,如果要区分,可以加上 m.type:
SELECT c.cid, c.title, c.slug, c.created, c.modified, c.text, GROUP_CONCAT(CASE WHEN m.type = 'category' THEN m.name END) AS categories, GROUP_CONCAT(CASE WHEN m.type = 'tag' THEN m.name END) AS tags FROM typecho_contents c LEFT JOIN typecho_relationships r ON c.cid = r.cid LEFT JOIN typecho_metas m ON r.mid = m.mid WHERE c.type = 'post' AND c.status = 'publish' GROUP BY c.cid ORDER BY c.created DESC;
方式二:通过 RSS 导出
如果你不方便直接访问数据库,也可以通过 RSS 获取文章列表。
Typecho 默认 RSS 地址可能是:
https://你的域名/feed/ https://你的域名/feed/rss/ https://你的域名/feed/atom/
RSS 的优点是简单、安全,不用碰数据库;缺点是可能只导出最近几十篇文章,且分类、标签、原始 Markdown 信息不一定完整。
因此,完整迁移更推荐数据库导出。
第四步:把 Typecho 内容转换成 Markdown
Typecho 文章正文的格式取决于你当时用的编辑器:
- 如果原来就是 Markdown,迁移会非常轻松;
- 如果是 HTML,需要转换成 Markdown;
- 如果混合了短代码、插件语法、图片懒加载标签,就需要额外清洗。
blog-v3 文章格式
blog-v3 里一篇文章的基本格式如下:
--- title: 文章标题 description: 文章摘要 date: 2026-05-19 12:00:00 updated: 2026-05-19 12:00:00 categories: [技术] tags: [Typecho, Nuxt] --- ## 正文标题 这里是正文内容。
需要注意:
date建议使用YYYY-MM-DD HH:mm:ss;updated可以使用 Typecho 的修改时间;categories和tags使用数组;- 文件名建议使用英文、拼音或原来的
slug; - 如果想保持旧链接,可以使用
permalink字段。
例如:
permalink: /archives/typecho-to-blog-v3/
这样可以尽量减少搜索引擎和外部链接的损失。
一个简单的转换脚本思路
假设我们已经把 Typecho 数据库导出成 JSON,例如 typecho-posts.json:
[
{
"title": "第一篇文章",
"slug": "first-post",
"created": 1710000000,
"modified": 1710003600,
"text": "## Hello\n\n正文内容",
"categories": "技术",
"tags": "Typecho,博客"
}
]
可以写一个 Node.js 脚本转换:
import fs from 'node:fs'
import path from 'node:path'
function formatDate(timestamp) {
const date = new Date(Number(timestamp) * 1000)
const pad = n => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
function safeFileName(name) {
return String(name || '')
.trim()
.replace(/[\\/:*?"<>|]/g, '-')
.replace(/\s+/g, '-')
.toLowerCase()
}
const posts = JSON.parse(fs.readFileSync('typecho-posts.json', 'utf8'))
for (const post of posts) {
const date = formatDate(post.created)
const updated = formatDate(post.modified || post.created)
const year = date.slice(0, 4)
const dir = path.join('content', 'posts', year)
fs.mkdirSync(dir, { recursive: true })
const categories = String(post.categories || '未分类')
.split(',')
.map(s => s.trim())
.filter(Boolean)
const tags = String(post.tags || '')
.split(',')
.map(s => s.trim())
.filter(Boolean)
const filename = safeFileName(post.slug || post.title)
const markdown = `---
title: ${JSON.stringify(post.title)}
description: ${JSON.stringify(String(post.text || '').replace(/<[^>]+>/g, '').slice(0, 120))}
date: ${date}
updated: ${updated}
categories: [${categories.join(', ')}]
tags: [${tags.join(', ')}]
permalink: /archives/${post.slug || post.cid}/
---
${post.text || ''}
`
fs.writeFileSync(path.join(dir, `${filename}.md`), markdown, 'utf8')
}
这只是一个基础版本,实际迁移时还可以继续增强:
- 自动把 HTML 转 Markdown;
- 自动下载远程图片;
- 自动替换旧附件路径;
- 自动修复标题层级;
- 自动移除 Typecho 插件短代码;
- 自动生成
description。
HTML 转 Markdown
如果 Typecho 文章里主要是 HTML,可以使用 turndown 转换:
pnpm add -D turndown
示例:
import TurndownService from 'turndown'
const turndown = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
})
const markdownBody = turndown.turndown(htmlBody)
转换完后不要急着发布,最好抽样检查几篇文章,尤其是:
- 代码块是否完整;
- 图片是否正常;
- 表格是否错乱;
- 引用块是否丢失;
- 链接是否被错误转义。
第五步:迁移图片和附件
文章迁移最容易踩坑的地方不是正文,而是图片。
Typecho 的图片链接常见形式有:
/usr/uploads/2024/01/example.png https://example.com/usr/uploads/2024/01/example.png
迁移到 blog-v3 后,有几种处理方式。
方案一:继续使用原图片地址
如果旧站点域名不变,或者你还保留原来的 /usr/uploads/ 路径,可以不改图片链接。
优点:
- 最省事;
- 不需要批量替换;
- 老文章几乎不用动。
缺点:
- 依赖旧服务器或旧路径;
- 后续迁移 CDN 时还要再处理;
- 如果旧目录删除,图片会全部失效。
方案二:把图片放到 public 目录
可以把 Typecho 的 usr/uploads 拷贝到 blog-v3 的 public/uploads:
mkdir -p public/uploads cp -r typecho-uploads/* public/uploads/
然后把文章里的路径从:

替换成:

可以用脚本批量替换:
import fs from 'node:fs'
import path from 'node:path'
function walk(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name)
return entry.isDirectory() ? walk(full) : [full]
})
}
for (const file of walk('content/posts')) {
if (!file.endsWith('.md')) continue
const old = fs.readFileSync(file, 'utf8')
const next = old.replaceAll('/usr/uploads/', '/uploads/')
if (next !== old) fs.writeFileSync(file, next, 'utf8')
}
方案三:使用对象存储或图床
如果图片很多,或者希望访问更快,可以把图片上传到对象存储/CDN,例如:
- S3;
- R2;
- OSS;
- COS;
- 又拍云;
- 自建静态资源域名。
然后把文章里的图片统一替换为 CDN 地址。
这种方案更适合长期维护,但第一次配置会稍微麻烦一些。
第六步:处理旧链接和 SEO
迁移博客时,最容易被忽略的是旧链接。
如果搜索引擎已经收录了原来的 Typecho 地址,例如:
https://blog.example.com/archives/123/ https://blog.example.com/archives/typecho-slug/
迁移后如果直接变成:
https://blog.example.com/posts/2026/typecho-slug
那么旧链接可能会 404。
优先使用 permalink 保持旧链接
blog-v3 的文章支持 permalink 字段,可以给文章指定自定义链接。
例如:
--- title: 老文章标题 date: 2024-01-01 12:00:00 permalink: /archives/old-slug/ ---
这样访问旧路径时依然能打开文章。
迁移时建议尽量保留 Typecho 原来的 slug,并把旧链接写进 permalink。
如果链接规则变了,就做重定向
如果你不想逐篇使用 permalink,也可以在部署平台或服务器层做 301 重定向。
比如在 Vercel 项目里可以通过 vercel.json 配置:
{
"redirects": [
{
"source": "/archives/:slug/",
"destination": "/posts/:slug",
"permanent": true
}
]
}
不过重定向规则是否适用,取决于你旧站和新站的 URL 是否能一一对应。对于不规则的旧链接,还是逐篇 permalink 更稳。
第七步:迁移分类和标签
Typecho 的分类和标签可以直接映射到 blog-v3 的 Front matter。
Typecho:
分类:技术 标签:Typecho、Nuxt、博客
blog-v3:
categories: [技术] tags: [Typecho, Nuxt, 博客]
同时,建议在 blog.config.ts 中配置常用分类及图标:
article: {
categories: {
未分类: { icon: 'tabler:circle-dashed' },
技术: { icon: 'tabler:mouse', color: '#33aaff' },
开发: { icon: 'tabler:code', color: '#7777ff' },
杂谈: { icon: 'tabler:message', color: '#33bbaa' },
生活: { icon: 'tabler:leaf', color: '#ff7777' },
},
}
迁移时我更推荐先收敛分类,再迁移文章。因为很多老博客写久了之后,会出现分类过细、标签重复的问题,比如:
前端、Frontend、前端开发;Linux、linux;随笔、杂谈、日记。
迁移正好是一次整理信息架构的机会。
第八步:处理独立页面、友链和导航
Typecho 里除了文章,还可能有独立页面:
- 关于;
- 友链;
- 留言板;
- 归档;
- 项目页。
迁移时不要只导文章,也要检查 typecho_contents 表里 type = 'page' 的内容。
SELECT cid, title, slug, created, modified, text FROM typecho_contents WHERE type = 'page' AND status = 'publish';
在 blog-v3 里,有些页面可以继续使用 Markdown,有些页面更适合做成 Vue 页面。
比如友链,如果只是简单文字,可以写在 Markdown;如果想要卡片、分组、头像、描述,则更适合放在配置文件里,再由页面组件渲染。
我现在的 friend links 就是通过配置维护,页面负责展示。这样好处是:
- 结构更清晰;
- 头像、链接、描述、订阅地址可以统一管理;
- 后续还可以扩展成博友圈、RSS 聚合等功能。
第九步:评论系统怎么处理
评论迁移通常有三种选择。
方案一:不迁移历史评论
这是最省事的方式。
个人博客很多文章的评论并不是核心内容,如果历史评论量不大,可以只保留数据库备份,不迁移到新站。
优点是简单,缺点是老文章下的讨论记录会丢失在前台。
方案二:导出成静态备份页面
可以把老评论导出成 JSON、Markdown 或 HTML,作为存档保留。
比如在每篇文章底部加一个“历史评论存档”折叠块,或者单独做一个评论归档页面。
方案三:迁移到新评论系统
如果新站使用 Twikoo、Waline、Artalk 等评论系统,可以尝试把 Typecho 评论表转换成对应系统的数据格式。
Typecho 评论一般在:
typecho_comments
常用字段包括:
| 字段 | 说明 |
|---|---|
coid | 评论 ID |
cid | 文章 ID |
created | 评论时间 |
author | 评论者 |
mail | 邮箱 |
url | 网址 |
text | 评论内容 |
parent | 父评论 ID |
status | 评论状态 |
如果评论量很大,建议先写脚本导出,再根据新评论系统的导入格式转换。
我个人更倾向于:重要评论做静态备份,新评论交给新的评论系统处理。这样迁移成本低,也不会影响新站结构。
第十步:本地检查
文章转换完成后,不要马上部署,先本地跑一遍。
安装依赖:
pnpm install
启动开发环境:
pnpm dev
然后重点检查:
- 首页文章列表是否正常;
- 文章详情页是否能打开;
- 分类、标签是否正常;
- 图片是否显示;
- 代码块高亮是否正常;
- 内链是否正常;
- RSS / Atom 是否正常;
- sitemap 是否正常;
- 移动端布局是否正常。
正式构建前再执行:
pnpm build
如果构建失败,通常从以下几个方向排查:
- Front matter 写法错误;
- Markdown 中存在未闭合的代码块;
- YAML 数组格式不合法;
- 图片链接或组件语法写错;
- 某些 HTML 标签没有闭合。
其中最常见的是 Front matter 出错。比如标题里有冒号时,最好加引号:
title: "从 Typecho 到 blog-v3:迁移记录"
第十一步:部署到 Vercel
blog-v3 很适合部署到 Vercel。
基本流程是:
- 把项目推送到 GitHub;
- Vercel 导入仓库;
- 设置构建命令;
- 绑定域名;
- 后续提交自动部署。
常见构建配置:
Install Command: pnpm install Build Command: pnpm build Output Directory: .output/public 或由 Nuxt/Vercel 自动识别
如果使用 Vercel CLI,也可以本地部署:
pnpm dlx vercel deploy --prod
绑定域名后,把 DNS 指向 Vercel。生效后访问:
https://你的域名/
再检查:
- 首页;
- 文章页;
- 分类页;
- 标签页;
- 友链页;
- Atom 订阅;
- sitemap;
- 旧链接是否能访问。
第十二步:迁移后的清理工作
上线不是结束,迁移后还有一些收尾工作。
检查死链
可以用爬虫或在线工具检查站内链接是否 404。
重点关注:
- 老文章内链;
- 图片链接;
- 旧附件;
- 友链;
- 文章引用的外部链接。
提交搜索引擎
如果域名没变,搜索引擎会逐步重新抓取。但仍然建议提交 sitemap。
常见地址:
https://你的域名/sitemap.xml
保留旧站备份
即使新站已经稳定运行,也建议至少保留一份旧站备份:
- 数据库 SQL;
usr/uploads;config.inc.php;- 主题和插件;
- 转换脚本。
最好放到本地硬盘和云端各一份。
我踩过的一些坑
1. Front matter 不是随便写的
Markdown 顶部的 YAML 对格式非常敏感。
错误示例:
title: 从 Typecho 到 blog-v3: 迁移记录
这里标题里有英文冒号,可能导致 YAML 解析异常。建议写成:
title: "从 Typecho 到 blog-v3: 迁移记录"
2. 标签数组不要混用奇怪格式
建议统一使用:
tags: [Typecho, Nuxt, 博客]
或者:
tags: - Typecho - Nuxt - 博客
不要一会儿字符串,一会儿数组,否则后续统计和页面展示容易出问题。
3. 图片路径一定要提前规划
图片到底放在 public/uploads,还是继续使用 CDN,最好迁移前就定下来。否则文章导入后再改,会非常麻烦。
4. 旧链接最好不要全部放弃
如果博客已经运行很久,老链接可能散落在搜索引擎、社交平台、别人的文章里。能通过 permalink 保留就尽量保留,不能保留也尽量做 301 重定向。
5. 不要一次性相信自动转换结果
无论是 HTML 转 Markdown,还是批量替换图片,都要抽样检查。尤其是包含代码块、表格、数学公式、嵌入视频的文章,最容易出问题。
推荐迁移流程总结
如果重新来一次,我会按这个顺序做:
- 完整备份 Typecho 数据库和附件;
- 搭建 blog-v3 本地环境;
- 配置
blog.config.ts; - 从数据库导出文章、页面、分类、标签;
- 将正文转换为 Markdown;
- 迁移图片到
public/uploads或 CDN; - 使用
permalink保留旧链接; - 抽样检查文章渲染效果;
- 执行
pnpm build; - 部署到 Vercel;
- 绑定域名;
- 检查 sitemap、RSS、死链和搜索引擎收录。
结语
从 Typecho 迁移到 blog-v3,不只是换了一个博客程序,更像是换了一种写作和维护方式。
过去是“登录后台,写文章,点发布”;现在是“本地写 Markdown,提交 Git,自动部署”。
Typecho 的优雅在于简单直接,blog-v3 的舒服在于自由可控。文章、主题、配置、页面都在仓库里,修改有记录,部署可回滚,长期维护也更安心。
如果你的博客还很轻量,只想要一个稳定后台,Typecho 依然很好;但如果你更喜欢 Markdown、Git、现代前端工作流,并且希望博客能慢慢变成一个可以持续打磨的个人站点,那么 blog-v3 值得一试。
迁移过程会有点折腾,但当你看到所有文章都整齐地躺在 content/posts 里,并且每一次提交都能自动发布时,会觉得这一步很值得。
评论区
评论加载中...