Skip to Content
指南博客系统

博客系统

博客聚合本地 MDX 文件、数据库发布内容,以及可选 Notion 和 Sanity 来源的公开文章。数据库文章可在 /admin/posts 编辑;外部来源在本应用中只读。

MDX 文件

创建带 frontmatter 的 content/blog/my-post.mdx:

--- title: "My First Post" date: "2026-09-26" summary: "A brief summary" tags: ["Tutorial", "Next.js"] draft: false --- # My First Post Your Markdown content goes here.

Velite 在构建或开发时生成文章。文件名决定公开 URL /blog/my-post。文件名应保持单一路径段。设置 draft: true 后,文章不出现在列表中,也不能直接公开阅读。包括草稿在内的 MDX slug 保留其公开地址,数据库文章不能占用。

新增 frontmatter 字段时,扩展 velite.config.ts 中的 posts schema,再在对应来源与展示层映射生成字段。schema 拥有验证,单独的 TypeScript 声明不会改变生成内容。

数据库发布

打开 /admin/posts/editor 创建文章。保存草稿保持未发布;发布后,公开受众的文章可供阅读。已有文章通过稳定 ID 在 /admin/posts/editor?id=123 编辑;发布修改后的 slug 会改变公开 URL,但保留文章身份。保留的公开别名在文章语言内解析;撤回后,这些地址也停止公开阅读。

命令使用文章 ID、预期版本与请求键。并发编辑被拒绝,不覆盖其他编辑者。响应不确定时,使用相同命令与请求键重试。新请求键代表新操作。草稿、已归档、已删除、私有及不公开列出的文章均不参与公开阅读。

服务端集成使用内容能力:

import { createPostPublisher, createPostEditorialReader } from "~/server/blog"; import { verifyAdminForAction } from "~/server/security/admin-auth"; import type { PostCommand } from "~/server/blog/publishing/contracts"; const authorize = async () => (await verifyAdminForAction()).authorized; const editorial = createPostEditorialReader(authorize); const selection = await editorial.read(123); const catalog = await editorial.catalog({ search: "Tutorial", status: "draft", page: 1 }); async function publish(command: PostCommand) { return createPostPublisher(authorize).execute(command); }

每个查询和命令都检查可信服务端授权。编辑读取区分就绪、无效输入、禁止访问与存储不可用。就绪选择的值为 null 表示文章不存在。目录包含所有受众,排除已删除文章,并按标题字面文本搜索。生命周期总数覆盖全部未删除文章,独立于当前筛选条件。

路由 Actions 在写入确认后处理 Next 缓存失效。应用调用方应使用这些能力,不直接插入数据库行或查询持久化回执。

Notion 集成

创建 Notion 集成,将数据库共享给该集成,并在本地环境配置 NOTION_TOKEN 与 NOTION_DATABASE_ID。列表读取以下属性:

属性名类型含义
TitleTitle文章标题
PublishedDate公开收录必须有值
URLURL必需的 HTTP 或 HTTPS 阅读目的地址
SlugText可选来源标签
SummaryText可选描述
TagsMulti-select可选标签

Notion 条目链接到其外部 URL。本集成不渲染或编辑 Notion 文章正文。需要继续分页的响应会报告不可用;当前未实现分页。

Sanity CMS

配置 SANITY_PROJECT_ID、SANITY_DATASET,必要时配置 SANITY_READ_TOKEN。当前查询读取包含 title、slug、summary、publishedAt、tags 及必需 HTTP/HTTPS url 的 post 文档,排除草稿与版本文档。列表链接到外部目的地址;编辑和发布归 Sanity Studio。

公开阅读

以下是服务端模块操作,不是 HTTP API 路由:

import { getBlogFeed, getPost } from "~/server/blog"; const feed = await getBlogFeed(); const tutorialPosts = feed.posts.filter((post) => post.tags?.includes("Tutorial")); const article = await getPost("my-post");

feed.unavailableSources 标识失败来源。没有失败的空列表是有效空结果;来源失败时应展示部分可用或不可用状态。公开详情对不存在或未发布文章返回 null,来源失败时抛错。MDX 归属优先于冲突的数据库 slug,即使该 MDX 文章是草稿也如此。

数据库发布事实与聚合列表每次重新读取。外部列表快照最多缓存五分钟。不要对公开列表或数据库可读性套长期缓存:已确认撤回必须在下一次服务端读取中反映。

来源选择位于 src/server/blog/runtime.ts;来源适配器实现 reading/contracts.ts 的领域合同。渲染位于 src/components/blog,公开页面将组件与阅读投影组合。/docs 下的文档使用独立 Nextra 流程,不是数据库文章,也不是另一个博客来源。

完整语言版本

公开内容使用明确的 /en/blog 与 /zh-CN/blog URL。原 /blog 链接永久重定向到英文资源。reader 接受可选显式语言:getBlogFeed("zh-CN")、searchBlog(query, "zh-CN")、getPost(slug, "zh-CN")、resolvePost(slug, "zh-CN")。省略语言仍表示英文。

每篇数据库文章是一个完整版本。创建时选择语言与可选翻译键;另一语言的完整文章使用同一键。创建后这些身份字段固定。同一语言内的 slug 与保留别名唯一。各版本独立保存、发布、定时或撤回;发布英文不会发布中文草稿。

本地 MDX frontmatter 接受 locale 与 translationKey。已有顶层文件默认英文,新中文来源位于 content/blog/zh-CN/。Velite 检查重复的 locale/slug 与 locale/translationKey 配对。完整标题、摘要和正文保留在资源中。草稿只占用本语言的 slug,不进入公开搜索。

缺译内容显式提供英文阅读链接,不把英文正文复制到中文 URL。只有真实已发布译文互相声明 hreflang;每个资源有自指规范地址。现有 Notion/Sanity metadata 在供应商提供完整语言合同前仅视为英文。