news 2026/9/15 15:49:20

使用 Next.js 与 Sanity CMS 构建 Corsair 官网博客:配置、内容模型与按需重新验证全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 Next.js 与 Sanity CMS 构建 Corsair 官网博客:配置、内容模型与按需重新验证全解析

使用 Next.js 与 Sanity CMS 构建 Corsair 官网博客:配置、内容模型与按需重新验证全解析

【免费下载链接】corsairConnect your users to their apps项目地址: https://gitcode.com/GitHub_Trending/corsa/corsair

本篇技术指南以 Corsair 开源仓库中的官网(www/目录)为对象,完整讲解其基于 Next.js 15 + Sanity CMS 的博客架构:从本地环境搭建、Sanity 项目初始化、环境变量体系,到post内容模型、GROQ 数据读取、按需重新验证(On-demand Revalidation)以及 Markdown 种子文章迁移流程。读完本文,你将掌握一套可直接复用的"Next.js + Sanity + Portable Text"内容中台搭建方案,并能在自己的项目中落地同样的配置与工作流。

项目概览:官网技术构成

Corsair 官网位于仓库的 www 目录,是一个 Next.js 应用,其 README 明确说明落地页视觉风格深受 twenty.com 启发,而博客内容全部托管在 Sanity CMS 中,正文使用 Portable Text 结构化富文本存储。从 www/package.json 的依赖可以看到完整技术栈:

  • Next.js 15^15.3.2)+ React 19,开发服务器通过next dev --turbopack启动;
  • Sanity 4^4.22.0)作为 CMS,配合next-sanity^11.6.13)做数据读取与 webhook 签名解析;
  • Drizzle ORM^0.44.5)管理自身数据库(认证、集成目录、OSS 认领等业务表);
  • Inngest^3.54.0)承担后台任务,better-auth承担认证,tRPC v11承担 API 层;
  • Tailwind CSS 4负责样式,搭配styled-components与 shadcn 组件体系。

其中博客链路(Sanity)与业务链路(Drizzle 数据库)相互独立:Sanity 只承载博客文章,官网的集成目录、OSS 认领等功能则落在 Postgres + Drizzle 上,这一点从 www/src/db 目录下的多个 schema 文件可以看出。

本地开发环境搭建

README 给出的三步启动流程如下:

cp .env.example .env pnpm install pnpm dev

pnpm dev实际执行的是next dev --turbopack(见 www/package.json),即以 Turbopack 为打包器启动 Next.js 开发服务器。仓库使用 pnpm workspace 管理多包,因此必须在仓库根目录安装依赖后进入www子包运行脚本,corsair@corsair-dev/*系列包均以workspace:*形式引用。

环境变量一览

官网的环境变量由 www/sanity/env.ts 统一定义并提供默认值:

变量用途默认值
NEXT_PUBLIC_SANITY_PROJECT_IDSanity 项目 ID,必填,缺失时assertSanityEnv()会直接抛错
NEXT_PUBLIC_SANITY_DATASETSanity 数据集名称production
NEXT_PUBLIC_SANITY_API_VERSIONSanity API 版本号2025-01-01
SANITY_API_WRITE_TOKEN写操作 API Token(迁移、写入用)
SANITY_REVALIDATE_SECRET按需重新验证 Webhook 的签名密钥

在 www/src/lib/sanity/client.ts 中,读写客户端是分离的:getSanityClient()用于公开读取,在生产环境(NODE_ENV === 'production')自动启用 CDN;getSanityWriteClient()仅在配置了SANITY_API_WRITE_TOKEN时返回,且强制useCdn: false,确保写入基于最新数据。

Sanity 首次配置:从零到可发布

README 给出了完整的首次配置八步流程,下面结合源码逐项展开:

  1. 创建 Sanity 项目:在 Sanity Manage 控制台新建项目。
  2. 填入 Project ID:将项目 ID 写入.envNEXT_PUBLIC_SANITY_PROJECT_ID
  3. 创建 API Token:在 Project → API → Tokens 中新建 Token,权限选Editor,写入SANITY_API_WRITE_TOKEN。迁移脚本 www/scripts/migrate-blog-to-sanity.ts 的错误提示中特别强调:这里的 Editor 指 API Token 的权限类型,而不是项目管理成员角色,Manager 是给人用的项目成员角色,不能作为 API Token 类型使用。
  4. 配置 CORS 白名单:在 Project → API → CORS 中依次添加:
    • http://localhost:3000(本地开发)
    • https://corsair.dev(生产环境)
  5. 部署 Schema
    pnpm sanity:schema

    该命令等价于sanity schema deploy,把 www/sanity/schemaTypes/post.ts 中定义的post文档类型推送到远端项目。

  6. 迁移种子文章(可选)
    pnpm blog:migrate

    www/scripts/seed-data/下的 Markdown 文件导入 Sanity(详见下文迁移章节)。

  7. 打开本地 Studio:访问http://localhost:3000/studio

Studio 的本地配置在 www/sanity.config.ts 中:basePath设为/studio,启用structureTool并挂载自定义结构,开发模式下额外启用visionTool方便调试 GROQ 查询。Sanity CLI 配置 www/sanity.cli.ts 指定了项目 ID、数据集以及托管 Studio 的域名前缀corsair-www

邀请编辑(以 SEO 经理为例)

  1. 在 Sanity Manage → 项目 →Members中邀请协作者,角色选择Editor
  2. 将 Studio 地址https://corsair.dev/studio分享给对方。

编辑无需接触代码仓库即可撰写、修改文章,这正是把内容交给 CMS 而非 git 的核心收益。

内容模型:post 文档类型详解

博客的内容契约定义在 www/sanity/schemaTypes/post.ts。post文档被划分为两个分组(groups):

  • content(内容)title(必填标题)、slug(由标题自动生成,最长 96 字符,注释标明最终 URL 形如/resources/blog/<slug>)、description(摘要,3 行文本框,必填且不超过 300 字符,用作博客列表与 meta description 回退)、author(默认值Corsair Team)、publishedAt(发布时间)、coverImage(封面图,开启 hotspot 裁剪,带 alt 字段)、body(正文,必填)、faqs(可选 FAQ 数组,用于生成 FAQPage JSON-LD)。
  • seo(SEO 与元数据)metaTitle(浏览器标签与搜索结果标题,约 60 字符)、metaDescription(搜索结果摘要,约 155 字符)、targetKeywords(目标关键词标签数组)、socialShareImage(社交分享图)、jsonLd(原始 JSON-LD,可注入一个或多个结构化数据对象,如 Article + FAQPage)。

Portable Text 正文约束

body字段通过defineArrayMember严格限制了可用的富文本能力,与 README 中"内容模型"一节完全对应:

  • 块级样式:Normal、H2、H3、blockquote(引用);
  • 列表:bullet(无序)、number(有序);
  • 行内标记:strong(加粗)、em(斜体)、code(行内代码),以及 link 注解(href允许相对路径,scheme 限定httphttpsmailto);
  • 行内图片:支持 alt 文本(必填)与可选 caption 说明。

这些约束在渲染端由 www/src/components/blog/portable-text-components.tsx 一一映射为 Next.js 组件:h2/h3/blockquote/normal各自拥有排版类名,link组件会根据 href 是否为绝对地址自动决定是否添加target="_blank"noreferrer noopener,图片通过urlForImage走 Sanity 图片 CDN。也就是说,编辑在 Studio 里能用的样式,就是渲染端保证有对应 UI 的样式,两端契约天然一致。

博客数据读取:GROQ 查询与页面渲染

博客的数据读取集中在 www/src/lib/sanity/queries.ts,使用next-sanitydefineQuery定义了三组 GROQ 查询:

  • allPostsQuery:按publishedAt降序拉取全部文章,字段含_idtitleslug.currentdescriptionauthorpublishedAtbody
  • postBySlugQuery:按 slug 精确取单篇文章;
  • allPostSlugsQuery:仅取 slug 列表,用于生成静态路径。

博客首页 www/src/app/blog/page.tsx 调用getAllPosts()渲染文章卡片列表,并内置了空态处理:当posts.length === 0时展示"No articles yet"的虚线占位框。文章详情页位于 www/src/app/blog/[slug]/page.tsx,正文部分由 Portable Text 渲染组件消费。

按需重新验证:发布即生效

由于博客页面在构建时静态生成,Sanity 中发布新文章后需要触发重新验证(Revalidate),这正是 README 中"On-demand revalidation"一节的用途。实现位于 www/src/app/api/revalidate/route.ts,其流程如下:

  1. 从环境变量读取SANITY_REVALIDATE_SECRET,缺失则返回 500;
  2. 使用next-sanity/webhookparseBody解析请求体并校验签名,签名不合法返回 401;
  3. 仅当body._type === 'post'时执行revalidateTag('blog')revalidatePath('/blog'),若载荷携带slug.current,再额外重新验证/blog/<slug>详情页;
  4. 返回{ revalidated: true, now: Date.now() }

在 Sanity 中配置 Webhook

  1. 生成一个随机密钥,写入.env与生产环境的SANITY_REVALIDATE_SECRET
  2. 在 Sanity Manage → API → Webhooks 创建 Webhook:
    • URLhttps://corsair.dev/api/revalidate
    • Dataset:production
    • Trigger on:Create、Update、Delete
    • Filter_type == "post"
    • Secret:与SANITY_REVALIDATE_SECRET相同的值

这样编辑在 Sanity 中发布、更新或删除文章后,官网无需重新部署即可在请求时增量刷新对应页面,实现了"内容即改即生效"。

将 Markdown 种子文章迁移到 Sanity

仓库在 www/scripts/seed-data 中维护了 Markdown 格式的种子文章(如anthropic-introduces-claude-fable-5.md,带titledescriptionpublishedAtauthor的 frontmatter),迁移脚本 www/scripts/migrate-blog-to-sanity.ts 负责将其转换为 Portable Text 并写入 Sanity,转换流水线为:

  1. frontmatter 解析:用gray-matter读取 Markdown 头部元数据;
  2. 正文转 HTML:用marked将 Markdown 编译为 HTML;
  3. HTML 转 Block:用@portabletext/block-toolshtmlToBlocks配合jsdom的 DOM 解析器,把 HTML 转换为符合postschema 的 Portable Text 块数组;
  4. 图片上传:正则提取alt形式的图片引用,将public/下对应文件通过client.assets.upload上传为 Sanity 资产,并生成带alt的 image block;文件缺失或上传失败时仅打印警告并跳过,提示后续可在 Studio 手动补充;
  5. 文档写入:以post-<slug>作为稳定的_id调用client.createOrReplace,实现幂等迁移(重复执行不会产生重复文档)。

脚本在启动时会校验NEXT_PUBLIC_SANITY_PROJECT_IDSANITY_API_WRITE_TOKEN,缺失即退出并输出明确提示。

常用命令速查

README 与 www/package.json 共同定义了官网可用的脚本:

命令说明
pnpm dev以 Turbopack 启动 Next.js 开发服务器
pnpm build/pnpm start生产构建与启动
pnpm sanity:schema部署 Schema 类型到 Sanity
pnpm sanity:deploy部署托管 Studio 至corsair-www.sanity.studio
pnpm blog:migratewww/scripts/seed-data的 Markdown 种子文章导入 Sanity
pnpm db:migrate/db:push/db:studioDrizzle 数据库迁移与可视化操作
pnpm test运行src/**/*.test.ts下的 Node 测试
pnpm inngest:devhttp://localhost:3000/api/inngest为端点本地调试 Inngest 后台任务

实践要点与踩坑提示

  • 写 Token 权限易混淆:迁移脚本的报错信息明确指出,SANITY_API_WRITE_TOKEN必须是Editor 权限的 API Token,而非项目成员角色;把 Manager 成员角色误当作 API Token 类型是最常见的配置错误。
  • CORS 必须双环境配齐:本地与生产各需要一个 CORS 来源,缺一不可,否则对应环境下 Studio 无法正常读写。
  • 重新验证链路环环相扣SANITY_REVALIDATE_SECRET必须在 Sanity Webhook 的 Secret 与官网环境变量中保持一致,签名校验失败会直接返回 401。
  • 空库有兜底 UI:博客首页在无文章时渲染"尚未发布"占位状态,新项目初始化后不会出现白屏。

至此,从 Sanity 项目初始化、post内容模型定义、GROQ 查询、Studio 嵌入,到 Webhook 按需重新验证与 Markdown 迁移,一套完整的 Next.js + Sanity 博客内容管线已全部打通。这套方案既适合 Corsair 这类需要"编辑自助发文 + 官网自动刷新"的开源项目,也可作为任意 Next.js 站点接入 Sanity CMS 的参考模板。

【免费下载链接】corsairConnect your users to their apps项目地址: https://gitcode.com/GitHub_Trending/corsa/corsair

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 15:48:10

隐藏积分:基于行为埋点的协作价值量化方法

1. 项目概述&#xff1a;这不是彩蛋&#xff0c;是行为数据的自然结晶“WorkBuddy 的「隐藏积分」”——这名字一出来&#xff0c;我就知道它绝不是什么游戏化噱头或运营部门临时起意的营销小动作。干了十多年产品与用户增长&#xff0c;我见过太多团队把“积分”做成空转的齿轮…

作者头像 李华
网站建设 2026/9/15 15:47:56

IEEE Trans参考文献编译崩坏的四大根源与校准方案

1. 为什么IEEE Trans论文的参考文献总在最后关头“爆雷”&#xff1f; 我帮实验室三个博士生改过IEEE Trans投稿稿&#xff0c;每次到最后编译参考文献环节&#xff0c;总有至少一人卡住&#xff1a;有的引用编号全乱&#xff0c;有的作者名缩写错位&#xff0c;有的期刊名突然…

作者头像 李华
网站建设 2026/9/15 15:46:02

烟台区县Shapefile数据处理:从拆包到坐标系转换与修复

简介&#xff1a;面向GIS开发、地理数据分析与城市规划等场景&#xff0c;这份资源提供烟台市各区县的行政区划矢量边界数据&#xff0c;格式为通用的Shapefile&#xff0c;可在ArcGIS、QGIS等主流平台直接加载使用。压缩包共14个文件&#xff0c;包含2个.shp几何文件、2个.shx…

作者头像 李华
网站建设 2026/9/15 15:44:16

Niushop V5 DEV版:开源商城系统的消息队列与插件钩子实战解析

简介&#xff1a;Niushop开源商城V5&#xff08;DEV开发版&#xff09;是一套基于PHP构建的前后端全开源商城系统&#xff0c;面向中大型新零售、网店与多门店场景&#xff0c;帮助开发者快速搭建并深度定制商城平台。压缩包大小78.76MB&#xff0c;包含2000个文件&#xff0c;…

作者头像 李华