Skip to content

feat(blog-cms): add database-backed blog CMS (Post/Author/Tag)#19

Open
yumingHe0705 wants to merge 1 commit into
floatboatai:mainfrom
yumingHe0705:feat/blog-cms
Open

feat(blog-cms): add database-backed blog CMS (Post/Author/Tag)#19
yumingHe0705 wants to merge 1 commit into
floatboatai:mainfrom
yumingHe0705:feat/blog-cms

Conversation

@yumingHe0705

Copy link
Copy Markdown

博客 CMS(Post / Author / Tag)—— PR 说明

选题方向:博客 CMS 后台(数据模型、CRUD、slug 校验、发布状态机、权限控制),实现于 Wasp 应用(template/app)内。

概述

为 Open SaaS 应用新增了一个仅管理员可用、基于数据库的博客 CMS。内容编辑可以创建/编辑/删除文章、用标签归类、指定作者,并让每篇文章在一套显式的发布生命周期中流转。同时提供只读的公开查询(仅返回已发布文章),便于后续接入公开博客页或 SEO 流水线。

模板自带的博客是基于 Astro 的静态 Markdown,因此真正的 CMS 更适合放在 Wasp 应用里——数据库、认证、后台管理都在这一侧。

改动内容

  • 数据模型(schema.prisma):新增 PostAuthorTag 三个模型与 PostStatus 枚举。PostTag 为多对多;PostAuthor 为多对一;AuthorUser 为可选的一对一关联。Post 还带有 SEO 字段(metaTitlemetaDescriptioncanonicalUrl)以及 @@index([status, publishedAt]) 索引。
  • 服务端 operations(src/blog-cms/operations.ts):
    • 管理端:getAdminPosts(按状态筛选 + 搜索 + 分页)、getAdminPostByIdcreatePostupdatePostchangePostStatusdeletePostcreateAuthorcreateTaggetAllAuthors
    • 公开端:getPublishedPosts(可按标签过滤)、getPublishedPostBySluggetAllTags
  • slug 逻辑(src/blog-cms/slug.ts):slugify(处理重音字符)、isValidSluggenerateUniquePostSlug(数字后缀解决冲突)。
  • 发布状态机(src/blog-cms/postStatus.ts):集中、可校验的状态迁移规则。
  • 后台 UI(AdminPostsPage.tsxAdminPostEditorPage.tsx)以及侧边栏 “Blog Posts” 入口。
  • 开发用 seed(src/blog-cms/seeds.ts)与 单元测试(slug.test.tspostStatus.test.ts)。

关键设计取舍

  • 作者(Author)与登录账号(User)解耦。 Author 是内容作者档案(name、slug、bio、avatar),可选关联到登录 User。这样把“编辑身份”与“认证账号”分开(支持外部投稿作者、改名、多账号等场景),代价是多一张表和一次 join。对 CMS 而言,这份灵活性值得。
  • 发布生命周期用显式状态机,而非布尔字段。 迁移规则集中在一处(ALLOWED_TRANSITIONS),非法跳转被统一拒绝,也非常易于单测。我刻意禁止 ARCHIVED → PUBLISHED:归档文章必须先恢复为草稿并重新审阅后才能再次上线。publishedAt 仅在首次发布时写入并保持稳定(再次发布不会改写历史时间)。
  • slug 唯一性采用双层保障。 应用层生成对常见情况给出友好可读的 slug(my-postmy-post-2 …);数据库 @unique 约束作为并发下的最终兜底,并把 Prisma 的 P2002 转换为清晰的 409 Conflict。仅当标题或 slug 真正变化时才重算 slug,避免已发布 URL 被悄悄改变。
  • 两层鉴权。 页面用 authRequired 拦截;每个写操作/管理端操作在服务端再次校验 context.user.isAdmin(辅助函数抛 401/403)。公开查询永不泄露草稿——只筛选 status = PUBLISHED
  • zod 全量入参校验,复用项目已有的 ensureArgsSchemaOrThrowHttpError,非法输入快速失败并返回 400
  • 客户端/服务端边界。 客户端不能引入 @prisma/client,因此把状态值镜像到一个客户端安全的 constants.ts,服务端仍使用真正的 Prisma 枚举。
  • 标签更新语义。 更新时 tags 使用 Prisma 的 set(整体替换),与编辑器 UX 一致(表单每次都提交完整的标签选择)。

安全性考虑

  • 所有写路径以及“查看全部(含草稿)”都要求已认证的管理员(服务端校验,而非仅在 UI 隐藏)。
  • 公开只读路径永远只返回 PUBLISHED 文章。
  • 删除不存在的文章返回 404(Prisma P2025),而不是 500。

测试方式

  • 单元测试(wasp test client run)——共 16 个,全部通过:
    • slug.test.ts:slugify(大小写/标点/重音/空输入)、isValidSluggenerateUniquePostSlug(无冲突、冲突后缀、excludePostId)。
    • postStatus.test.ts:所有允许/禁止的迁移 + 同状态幂等。
  • 手动端到端(本地 Postgres):建作者/标签 → 建草稿(自动 slug)→ 发布/取消发布/归档/恢复 → 同名标题的 slug 自动变为 -2 → 编辑 → 筛选/搜索/分页 → 删除;并验证非管理员访问 /admin/posts 会被重定向。
  • Seed(wasp db seed seedBlogContent)会创建 1 位作者、3 个标签、4 篇涵盖所有状态的文章,便于快速人工查看。

本地评审步骤

cd template/app
wasp db migrate-dev          # 创建 Post/Author/Tag 表
wasp db seed seedBlogContent # 可选:填充示例内容
wasp start                   # 然后以管理员身份打开 http://localhost:3000/admin/posts
wasp test client run         # 运行单元测试

提示:注册前把你的邮箱加入 .env.serverADMIN_EMAILS,即可获得管理员权限。

说明 / 局限与后续规划

  • 未提交迁移文件。 模板本身不带 migrations/ 目录,因此以 schema.prisma 作为唯一事实来源,运行 wasp db migrate-dev 即可在本地生成迁移。
  • 暂未做公开博客页面。 公开查询已就绪,但渲染部分刻意留空,以免与现有 Astro 博客重叠;Post 上的 SEO 字段特意预留,便于后续「SEO 发布流水线」消费(sitemap / canonical / metadata)。
  • 正文以原始文本存储(Markdown/HTML)。富文本编辑、封面/图片上传(可复用现有 S3 模块)、软删除、作者级细粒度权限、按标签分页页等都是自然的后续方向。
  • Post ⇄ Tag 目前使用 Prisma 隐式多对多;若后续需要排序或额外元数据,可切换为显式 join 表。

Adds an admin-only blog CMS to the Wasp app:

- Data model: Post, Author, Tag (+ PostStatus enum) with a many-to-many
  Post<->Tag relation and an optional Author<->User link.
- Operations: admin CRUD, public read (published only), tag/author creation,
  with zod input validation and admin-only authorization.
- Slug handling: auto-slugify from title, app-level uniqueness with a numeric
  suffix, and a DB unique constraint as the final guard (P2002 -> 409).
- Publish lifecycle modeled as an explicit state machine (DRAFT/PUBLISHED/
  ARCHIVED); publishedAt is stamped on first publish; archived posts must be
  restored to draft before going live again.
- Admin UI: posts list (filter/search/paginate, inline status transitions,
  delete) and a create/edit editor (inline author/tag creation, SEO fields),
  plus a "Blog Posts" sidebar entry.
- Dev seed (seedBlogContent) and unit tests for slug + status-machine logic.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant