文章管理迁移计划:全文内容入库
507 字
2 分钟
文章管理迁移计划:全文内容入库
文章管理迁移计划:全文内容入库
背景
当前架构中,文章正文存在 .md 文件里,元数据存在 PostgreSQL 的 articles 表里。每次编辑文章需要同时写文件系统和数据库,且 SSR 渲染时需要通过 astro:content 从文件系统读取正文。
目标:将文章正文也存入数据库,彻底消除文件系统和数据库的同步问题。
表结构设计
现有表(不变)
export const articles = pgTable("articles", { id: serial("id").primaryKey(), slug: text("slug").notNull().unique(), title: text("title").notNull().default(""), description: text("description").notNull().default(""), category: text("category").notNull().default(""), tags: text("tags").array().notNull().default([]), coverImage: text("cover_image").notNull().default(""), status: articleStatus("status").notNull().default("published"), pinned: boolean("pinned").notNull().default(false), wordCount: integer("word_count").notNull().default(0), publishedAt: timestamp("published_at"), createdAt: timestamp("created_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(),});新增表
export const articlesContent = pgTable("articles_content", { id: serial("id").primaryKey(), articleId: integer("article_id") .notNull() .references(() => articles.id, { onDelete: "cascade" }), content: text("content").notNull().default(""), createdAt: timestamp("created_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(),});分阶段文件变更
阶段一:数据库层
| 文件 | 操作 |
|---|---|
src/db/schema.ts |
新增 articlesContent 表 |
scripts/migrate-content-to-db.ts |
新建:读取所有 .md 文件,解析 body,写入 articles_content |
drizzle.config.ts |
不变 |
阶段二:Markdown 渲染管道
将文章正文从 DB 渲染为 HTML,脱离 astro:content.render()。
| 文件 | 操作 |
|---|---|
src/utils/markdown-ssr.ts |
新建:基于 unified + 现有插件链(14 个 remark/rehype)的独立渲染函数 |
astro.config.mjs |
将 markdown.processor 中的插件配置提取为可共享变量 |
阶段三:SSR 页面改造
| 文件 | 操作 |
|---|---|
src/pages/posts/[...slug].astro |
从 getSortedPosts() + render(entry) 改为 查 DB + markdown-ssr.ts 渲染 |
src/pages/api/admin/articles/[id].ts |
GET 返回 articles JOIN articles_content;PUT 同时写两张表 |
src/pages/api/admin/articles/index.ts |
GET 列表不查 content 表(窄表优化) |
阶段四:内容工具重写
| 文件 | 操作 |
|---|---|
src/utils/content-utils.ts |
getSortedPosts() 直接从 DB 查询,不再调 astro:content;相应调整排序/过滤/标签/分类函数 |
src/utils/lqip-utils.ts |
如果依赖 .md 文件路径,需适配 |
src/content/posts/ |
.md 文件可保留作备份,但不再是渲染的数据源 |
阶段五:清理
| 文件 | 操作 |
|---|---|
scripts/sync-posts-to-db.ts |
删除(不再需要) |
.github/workflows/ci.yml |
删除 pnpm tsx scripts/sync-posts-to-db.ts 步骤 |
src/content.config.ts |
posts collection 可保留或删除 |
数据流对比
当前
管理员保存 → 写 .md文件 + UPDATE articles新增文章 → 创建 .md文件 + INSERT articles读者查看文章 → SSR → getCollection → 读 .md文件 → render(entry)列表页/首页 → getSortedPosts() → getCollection → 读所有 .md → .find()首次部署 → sync-posts-to-db.ts 从 .md 导入 DB迁移后
管理员保存 → UPDATE articles + UPSERT articles_content新增文章 → INSERT articles + INSERT articles_content读者查看文章 → SSR → SELECT articles JOIN articles_content WHERE slug=xxx → markdown-ssr.ts 渲染 → 返回 HTML列表页/首页 → content-utils.ts 直接 SELECT articles(不查 content 表)首次部署 → migrate-content-to-db.ts 一次性迁移后不再需要markdown-ssr.ts 设计要点
import { unified } from "unified";// 复用 astro.config.mjs 中已有的 remark/rehype 插件// 包括:remarkMath, remarkPlantuml, remarkMermaid, rehypeCallouts,// rehypeKatex, rehypeSlug, rehypeFigure, ...
export async function renderMarkdown(content: string): Promise<string> { const file = await unified() .use(/* 所有现有 remark 插件 */) .use(/* 所有现有 rehype 插件 */) .process(content);
return String(file);}关键:复用 astro.config.mjs 中已配置的 14 个插件,保持渲染结果与当前一致。
迁移脚本 migrate-content-to-db.ts
// 遍历 src/content/posts/ 下所有 .md 文件// 解析 frontmatter → upsert articles 表// 解析 body → upsert articles_content 表// 只跑一次,完成后正文数据就在 DB 里了注意事项
- 删除
schema.ts里的字段不会自动删数据库——只有跑drizzle-kit push才会变更真实数据库 articles_content.article_id设置了ON DELETE CASCADE——删除文章时正文自动跟着删drizzle-kit push不会删现有的articles表——只会在基础上加新表articles_content.md文件保留不动——作为可选的备份来源,不影响功能astro:content的render()不再用于文章——但其他功能(spec 页面、MDX)仍然可用
执行顺序
Phase 1: schema.ts + drizzle-kit push + migrate ← 先做这个Phase 2: markdown-ssr.ts ← 再做这个Phase 3: posts/[...slug].astro API 端点改造 ← 然后改渲染Phase 4: content-utils.ts 重写 ← 然后改工具Phase 5: 清理 sync 脚本 + CI/CD ← 最后文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
NaBCWorkshop