Headless CMS 实战指南:解耦架构下的内容管理新范式
先上手再问为什么:Headless CMS 真的香
我第一次接触 Headless CMS 是在去年一个紧急项目里,客户非要在后台自己改文案,但又死活不同意用 WordPress。当时我第一反应是“这不就是个 API 写死的页面嘛”,结果被产品经理一句话怼回来:“你写死了,人家怎么改?” 无奈之下,我花了一晚上试了几个主流 Headless CMS,最后选了 Strapi(后来也试过 Contentful、Sanity),亲测有效,真香。
别管什么“无头架构”“内容即服务”这些高大上的词,咱就看最实际的:怎么从 CMS 拿数据,怎么渲染到页面上。下面这段代码,是我现在项目里最常用的 fetch 方式:
const API_URL = 'https://jztheme.com/api';
async function fetchBlogPosts() {
try {
const res = await fetch(${API_URL}/posts?populate=*);
const data = await res.json();
return data.data;
} catch (error) {
console.error('Failed to fetch posts:', error);
return [];
}
}
注意那个 populate=*,这是 Strapi v4 的坑点——默认不返回关联字段,比如文章的作者、封面图这些,必须显式 populate。我一开始没加,页面全是空的,折腾了半天才发现是这个原因。如果你用的是 Contentful,那它的 GraphQL 查询更灵活,但学习成本略高。
这个场景最好用:动态路由 + 静态生成
我现在做营销页、博客、产品文档这类内容型站点,基本都用 Next.js + Headless CMS 的组合。Next.js 的 getStaticPaths + getStaticProps 简直是为这种场景量身定做的。举个例子,生成所有博客详情页:
// pages/posts/[slug].js
export async function getStaticPaths() {
const posts = await fetchBlogPosts();
const paths = posts.map(post => ({
params: { slug: post.attributes.slug }
}));
return { paths, fallback: 'blocking' };
}
export async function getStaticProps({ params }) {
const res = await fetch(${API_URL}/posts?filters[slug][$eq]=${params.slug}&populate=*);
const data = await res.json();
const post = data.data[0];
if (!post) return { notFound: true };
return { props: { post } };
}
这里我用 fallback: 'blocking' 而不是 false,是因为 CMS 里可能随时新增文章,如果设成 false,新文章的 URL 就会 404。而 blocking 会在首次访问时动态生成页面,虽然慢一点,但胜在省心。亲测有效,上线后没再因为漏跑构建脚本被 PM 叫去开会。
踩坑提醒:这三点一定注意
Headless CMS 用起来爽,但有几个坑我踩过不止一次,分享出来帮你省点时间:
- 图片处理别直接扔 CDN 链接:很多 CMS 会返回原始图片 URL,比如 Strapi 默认给的是本地路径。如果你部署在 Vercel 上,直接用这个路径会 404。建议要么配好 public folder,要么用 Cloudinary 这类服务做中转。我现在的做法是在构建时把图片上传到 S3,然后替换 URL——虽然麻烦,但稳定。
- 权限别开太宽:Strapi 的 Public 角色默认只能读 content-type,但如果你自定义了接口,很可能忘了设置权限。有一次我写了个统计接口,结果没设权限,导致任何人都能调用,差点被刷爆。上线前务必检查 Settings → Users & Permissions Plugin → Roles。
- 本地开发别硬连线上 CMS:我见过同事直接在 dev 环境连生产 CMS,结果一不小心把测试数据写进去了。现在我的习惯是:
.env.local里放测试环境 API,.env.production放正式环境。而且测试环境 CMS 会定期清空,避免污染。
高级技巧:用 GraphQL 精准取数(适合 Contentful)
如果你用的是 Contentful,强烈建议上 GraphQL。它比 REST 灵活太多,尤其当你有嵌套内容(比如文章里嵌 Banner,Banner 里又有 CTA 按钮)时,REST 得发好几轮请求,GraphQL 一次搞定。下面是我常用的一个 query:
const query =
query GetPost($slug: String!) {
postCollection(where: { slug: $slug }) {
items {
title
body
heroImage {
url
description
}
author {
name
avatar {
url
}
}
}
}
}
;
async function fetchPostBySlug(slug) {
const res = await fetch('https://graphql.contentful.com/content/v1/spaces/your-space-id', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: Bearer ${process.env.CONTENTFUL_ACCESS_TOKEN}
},
body: JSON.stringify({ query, variables: { slug } })
});
const data = await res.json();
return data.data.postCollection.items[0];
}
注意:Contentful 的 access token 分 CDA(内容交付)和 CPA(内容预览),别搞混了。CDA 用于生产环境,CPA 用于预览草稿内容。我一般在 preview 模式下切 CPA,方便编辑实时看效果。
不是银弹,但够用
Headless CMS 并不是万能的。比如你要做复杂的用户交互(评论、点赞、登录态内容),它就帮不上忙,还得自己搭后端。但如果你只是需要一个“内容后台”,让非技术人员能改文案、换图、发文章,那它绝对是最省事的方案。我现在的项目里,80% 的页面都靠它驱动,剩下 20% 复杂逻辑才写 API。
另外,别迷信“完全无头”。有些 CMS 其实提供了可视化编辑器(比如 Sanity 的 Studio),体验比传统 WordPress 还要好,编辑能直接在页面上改文字,所见即所得。这种 hybrid 模式我觉得很实用,既保留了灵活性,又降低了使用门槛。
以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如多语言支持、内容版本回滚、Webhook 自动触发构建),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流,比如你们是怎么处理图片优化的?或者有没有试过 DatoCMS?

暂无评论