03 · 数据模型与 Astro DB
书签数据是 三层嵌套:分区 → 卡片组 → 链接。Astro DB 用三张表扁平存储,应用层再组装成树。
// src/lib/bookmarks/types.ts(简化)interface BookmarkData { title: string url: string description?: string badgeText?: string badgeVariant?: string extraLinks?: { title: string; url: string }[] sortOrder: number}
interface BookmarkCardData { title: string sortOrder: number bookmarks: BookmarkData[]}
interface BookmarkSectionData { title: string sortOrder: number stagger: boolean // 公开页卡片是否交错布局 cards: BookmarkCardData[]}stagger 是展示层字段,不影响 DB 关系,但会随 seed 写入 BookmarkSection 表。
Astro DB 表定义
Section titled “Astro DB 表定义”const BookmarkSection = defineTable({ columns: { id: column.number({ primaryKey: true }), title: column.text(), sortOrder: column.number(), stagger: column.boolean({ default: true }), },});
const BookmarkCard = defineTable({ columns: { id: column.number({ primaryKey: true }), sectionId: column.number(), title: column.text(), sortOrder: column.number(), },});
const Bookmark = defineTable({ columns: { id: column.number({ primaryKey: true }), cardId: column.number(), title: column.text(), url: column.text(), description: column.text({ optional: true }), badgeText: column.text({ optional: true }), badgeVariant: column.text({ optional: true }), extraLinks: column.text({ optional: true }), // JSON 字符串 sortOrder: column.number(), },});extraLinks 存 JSON 字符串——Astro DB 无原生 JSON 列,查询时再 JSON.parse。
数据源:TypeScript 文件
Section titled “数据源:TypeScript 文件”真正维护的数据在 db/data/bookmarks.ts:
export const bookmarkSections: BookmarkSectionData[] = [ { title: "常用", sortOrder: 0, stagger: true, cards: [ { title: "文档", sortOrder: 0, bookmarks: [ { title: "Astro", url: "https://astro.build", sortOrder: 0 }, ], }, ], },];为什么用 TS 而不是 JSON?
- 可带类型与注释
- 管理端保存时整文件重写,格式稳定
- 直接进 Git diff,review 友好
Seed 流程
Section titled “Seed 流程”import { bookmarkSections } from "./data/bookmarks";
export default async function seed() { let sectionId = 1, cardId = 1, bookmarkId = 1;
for (const section of bookmarkSections) { await db.insert(BookmarkSection).values({ id: sectionId, /* … */ });
for (const card of section.cards) { await db.insert(BookmarkCard).values({ id: cardId, sectionId, /* … */ });
if (card.bookmarks.length > 0) { await db.insert(Bookmark).values( card.bookmarks.map((b) => ({ id: bookmarkId++, cardId, /* … */ })), ); } cardId++; } sectionId++; }}dev / build 时 Astro DB 自动执行 seed。改 bookmarks.ts 后重启 dev 或触发 HMR 即可刷新内存库。
getBookmarkSections() 分三次查询,按 sortOrder 排序,再用 Map 归并:
export async function getBookmarkSections(): Promise<BookmarkSectionData[]> { const sections = await db.select().from(BookmarkSection).orderBy(asc(BookmarkSection.sortOrder)); const cards = await db.select().from(BookmarkCard).orderBy(asc(BookmarkCard.sortOrder)); const bookmarks = await db.select().from(Bookmark).orderBy(asc(Bookmark.sortOrder));
// cardsBySection、bookmarksByCard → 组装树 return sections.map(/* … */);}书签量级在数百条时,三次查询 + 内存 join 足够;无需 SQL JOIN API。
序列化(管理端写回)
Section titled “序列化(管理端写回)”保存时 serializeBookmarkSections() 会:
- 按数组下标 重算
sortOrder(拖拽排序后保证连续) - 去掉空 optional 字段
- 输出完整 TS 文件(含 interface 与
export const bookmarkSections)
写文件前会备份为 bookmarks.ts.bak。
添加第一个书签
Section titled “添加第一个书签”-
打开
db/data/bookmarks.ts,在任意 card 的bookmarks数组追加一项 -
重启
vpr dev(或等待 DB 重新 seed) -
访问
/bookmarks/,确认新链接出现 -
可选:用
getBookmarkSections在临时 Astro 页面打印调试
从旧 MDX 迁移
Section titled “从旧 MDX 迁移”仓库提供 scripts/migrate-bookmarks.mjs,可从历史 MDX 书签页批量导入到 bookmarks.ts。适合从纯文档方案迁移到结构化数据。
- 三层模型 对应三张 DB 表,TS 文件是唯一可提交数据源
- seed 在构建管线中灌数据;queries 负责树形组装
- sortOrder 由应用层维护,保存时归一化