从传统CMS到JAMstack架构:内容即服务与无头CMS实战解析
1. 内容管理系统:一场迟到的“葬礼”与新生
“CMS已死,CMS万岁。”这句话在技术圈里流传了好一阵子,乍一听像个悖论,但如果你像我一样,在过去十几年里从零搭建过几十个网站,从企业官网到电商平台,从个人博客到内容社区都折腾过,你就会明白这句话背后那种复杂的情绪。它不是在宣告一个工具的终结,而是在描述一场深刻的范式转移。我们这些老站长、老开发者,对传统CMS的感情是复杂的——它曾经是我们的“瑞士军刀”,帮我们快速搞定一切;但如今,它又常常让我们在项目后期感到束手束脚,像个穿着不合身盔甲的战士。
所谓的“CMS已死”,死的是那种大而全、试图包办一切、以“管理后台”为中心的旧有形态。想想看,十年前,你要建个网站,第一步是什么?大概率是去研究WordPress、Drupal或者Joomla。你得先装好这个庞然大物,然后在一堆插件和主题里大海捞针,最后往往发现,为了实现一个简单的定制功能,你需要和复杂的钩子(Hooks)、过滤器(Filters)以及可能存在的安全漏洞作斗争。整个网站的交付物,是一个紧密耦合的“黑盒”:内容、表现层、业务逻辑、数据库全都搅和在一起。前端?那只是CMS主题输出的HTML模板罢了。这种模式在Web 2.0时代是王者,因为它降低了内容发布的门槛。但到了今天,当我们需要网站拥有极致的性能、无缝的多端体验(Web、App、小程序、智能设备)、灵活的个性化推荐,以及能与各种第三方服务(CRM、营销自动化、数据分析平台)深度集成时,传统CMS就显得力不从心了。它变得笨重、缓慢,且每一次迭代都像是在给一座老房子做加固,成本高昂且风险不小。
而“CMS万岁”,则是在欢呼一种新范式的诞生:内容即服务。内容不再被锁死在某个特定的后台和数据库里,而是通过结构化的API(如GraphQL或RESTful API)暴露出来,成为一种可以被任何前端技术栈自由消费的“服务”。这时,CMS回归了它的本质——一个强大、专注的内容仓库和协作平台。编辑和运营人员依然在一个友好、熟悉的界面里创作和管理内容,但发布出去的,不再是渲染好的HTML页面,而是一份份纯净的JSON数据。至于这些数据最终以怎样的形式、在何种设备上呈现,则完全交给了独立的前端应用(可以是React、Vue、Next.js、Nuxt.js构建的,也可以是原生App或静态站点)。这种前后端彻底分离的架构,就是我们常说的JAMstack架构。
这种转变对从业者意味着什么?意味着我们终于可以把专业的事交给专业的工具。内容管理者用最适合内容生产的工具(如Sanity、Strapi、Contentful),开发者用最擅长构建交互界面的框架,运维则可以用更简单、更廉价的方式(如CDN)来部署和扩展前端。项目的复杂度和风险被解耦了。我经历过太多这样的时刻:客户只是想在首页加一个动态更新的小模块,在传统CMS里这可能涉及主题修改、插件兼容性测试和数据库查询优化,动辄几天工时;而在新架构下,前端开发者只需要调用一个新的API字段,重新构建部署一下静态前端,可能半小时就搞定了,而且对后端内容管理系统毫无影响。
所以,今天我想聊的,不是某个具体CMS工具的教程,而是这场变革背后的逻辑,以及我们作为构建者,如何在实际项目中理解和运用这种“新CMS”哲学。我会结合我最近用Sanity和Next.js搭建一个内容型网站的全过程,拆解其中的设计思路、技术选型、实操细节,以及那些只有踩过坑才知道的“秘籍”。
2. 新范式核心:为什么“分离”是必然选择
要理解为什么“无头CMS”或“内容即服务”会成为主流,我们需要跳出工具层面,从几个更根本的驱动因素来看。
2.1 性能需求的倒逼:用户体验不容妥协
在移动网络和用户耐心都成为稀缺资源的时代,网站速度就是生命线。Google早已将页面加载速度纳入搜索排名核心算法,更快的网站意味着更好的SEO和更高的转化率。传统动态CMS(如WordPress)的典型工作流程是:用户请求一个页面 → 服务器接收请求 → PHP应用启动 → 查询数据库 → 组合数据与主题模板 → 渲染HTML → 返回给用户。这个链条上的每一个环节都可能成为瓶颈,尤其是在流量高峰时。
而基于JAMstack和新CMS架构的网站,其生产流程是:内容更新 → 触发构建 → 前端应用调用CMS的API获取最新数据 → 生成纯静态的HTML、CSS、JS文件 → 部署到CDN。用户访问时,CDN直接返回这些预先生成的静态文件,速度极快。因为省去了动态查询和渲染的过程,TTFB(首字节时间)可以做到几十毫秒级别。我实测过一个项目,将同样内容的WordPress站点迁移到Next.js + Sanity架构后,Lighthouse性能评分从可怜的45分直接飙到了95分以上,页面加载时间从3秒多降到不到1秒。这种性能提升是肉眼可见的,也是业务方最能直接感知的价值。
2.2 多端体验的统一:内容一次创建,处处展示
今天的品牌触点早已不止于官网。你可能需要内容在官网、移动端H5、iOS/Android App、微信小程序、甚至智能电视或车载屏幕上都有良好的呈现。传统CMS为Web而生,其输出的内容格式和结构严重依赖HTML,很难直接适配其他终端。通常的解决方案是为每个终端单独开发一套接口或适配层,成本高昂且维护困难。
“内容即服务”架构完美解决了这个问题。CMS只负责生产结构化的内容数据(例如,一篇文章包含标题、作者、发布时间、正文、图片数组、相关标签等字段)。各个终端的前端应用独立开发,各自按需通过API消费这些数据,并用自己的方式渲染。内容团队只需要在一个后台维护一套内容,就能同步到所有渠道。我在一个客户项目中,就用Sanity作为唯一的内容源,同时支撑了其官网(Next.js)、会员App(React Native)和内部员工门户(Vue.js),内容更新的同步几乎是实时的,大大降低了跨平台的内容管理成本。
2.3 开发体验的解放:用最好的工具做最擅长的事
对于开发者而言,传统CMS的扩展开发往往是一种“戴着镣铐跳舞”的体验。你必须遵循其特定的PHP版本、插件规范、主题结构,学习其独有的API。而当你想引入一个现代前端框架(如React)或一个新颖的npm包时,集成过程可能异常痛苦。
无头CMS将后端内容管理和前端呈现彻底解耦。后端开发者可以专注于设计合理的内容模型(Content Model)和高效的API;前端开发者则可以完全自由地选择技术栈,享受React/Vue/Svelte等现代框架带来的开发效率和高性能优势。两者通过一份清晰的API文档契约进行协作,并行开发,互不干扰。这种自由度和专业性,是提升团队效率和项目质量的关键。
2.4 安全性与运维成本的降低
传统动态CMS,由于其普及度和复杂的插件生态,一直是黑客攻击的重灾区。核心程序、主题、插件的任何一个漏洞,都可能导致整个站点被入侵。运维需要时刻关注安全更新,部署复杂。
在新的架构下,前端是静态文件,托管在CDN上,没有数据库,没有服务器端执行环境(如果做纯静态渲染),攻击面大大缩小。CMS后台(如Sanity)通常由专业的SaaS服务商维护,安全性更高。运维工作简化为对CDN和构建流程的管理,通常可以通过GitHub Actions、Vercel、Netlify等工具实现完全自动化,成本大幅下降。
3. 实战选型:如何为你的项目挑选“新CMS”
市面上无头CMS的选择很多,从开源自托管到商业SaaS,各有千秋。选型不是找“最好”的,而是找“最合适”的。我通常会从以下几个维度来评估:
3.1 核心维度评估
- 内容模型灵活性:这是CMS的“大脑”。它定义了你内容的类型(如文章、产品、作者)和每个类型包含哪些字段(文本、富文本、图片、引用、数组等)。好的CMS应该能让你像搭积木一样自由定义模型,而不是让你去适应它预设的几种类型。
- API能力与性能:API是否强大(支持GraphQL吗?过滤、排序、分页是否灵活?)、响应是否快速、是否有合理的缓存和限流策略。这直接关系到前端应用的体验。
- 编辑体验:这是给内容运营团队用的,必须直观、高效。富文本编辑器好不好用?能否自定义编辑界面?协作功能(如草稿、预览、工作流)是否完善?
- 媒体资产管理:图片、视频等资源的上传、处理(裁剪、压缩、格式转换)、优化和CDN分发是否顺畅。
- 扩展性与自定义:当标准功能不满足需求时,能否通过自定义组件、插件或代码来扩展?
- 定价与生态:是否符合预算?社区是否活跃?是否有丰富的教程和第三方集成?
3.2 主流工具横向对比
基于以上维度,我整理了一个简单的对比表格,涵盖了我在不同项目中深度使用过的几款工具:
| 特性维度 | Sanity | Strapi | Contentful | WordPress + WPGraphQL |
|---|---|---|---|---|
| 核心类型 | SaaS (可自托管) | 开源自托管 | 商业SaaS | 开源自托管 |
| 内容模型 | 极其灵活,基于JSON schema实时定义,支持自定义输入组件。 | 灵活,通过Admin UI或代码定义,插件市场丰富。 | 灵活,通过UI定义,企业级功能强。 | 基于文章和页面,通过自定义文章类型和字段插件扩展,模型相对传统。 |
| API | 同时提供GraphQL和GROQ(其自研的强大查询语言),性能优秀。 | RESTful和GraphQL API,性能取决于部署。 | 强大的GraphQL和REST API,全球CDN。 | 通过WPGraphQL插件提供GraphQL API,性能受WordPress本身制约。 |
| 编辑体验 | 卓越,实时协作,可深度定制编辑界面(Sanity Studio)。 | 良好,Admin UI功能全面。 | 优秀,界面专业,适合大型团队。 | 熟悉,但编辑器相对传统,现代化需插件。 |
| 媒体管理 | 集成于Sanity Studio,支持即时裁剪和优化。 | 需通过插件或自行配置。 | 优秀,强大的数字资产管理功能。 | 依赖WordPress媒体库,功能基础。 |
| 扩展性 | 高,Studio完全用React构建,可任意定制。 | 高,开源,可修改源码,插件生态好。 | 中,主要通过UI配置和webhook扩展。 | 高,海量插件,但质量参差不齐,易冲突。 |
| 定价 | 免费层慷慨,付费按用量(API调用、带宽)。 | 免费开源,自付服务器成本。 | 昂贵,免费层限制严格。 | 免费开源,自付服务器成本。 |
| 最佳场景 | 需要高度定制编辑界面和复杂内容模型的项目,开发团队强。 | 需要完全控制权、预算有限、偏好开源技术的团队。 | 大型企业,需要开箱即用的企业级服务和支持。 | 已有WordPress生态投资,希望渐进式向API驱动转型的项目。 |
3.3 为什么我这次选择了Sanity?
对于我最近这个内容网站项目,我最终选择了Sanity。原因如下:
- 项目需求:网站内容类型多样(文章、专题、作者、资源下载),且文章内部结构复杂(需要插入交互式图表、代码沙盘等自定义区块)。运营团队希望有一个简洁但强大的编辑后台,能直观地看到这些复杂结构的预览。
- Sanity的优势:
- GROQ查询语言:虽然学习曲线稍陡,但一旦掌握,其查询能力远超普通的GraphQL,能在一句话里完成复杂的数据关联、过滤和投影,极大减少了前端的数据处理逻辑。
- 可定制的Studio:Sanity Studio本身就是一个用React写的开源应用,我可以直接修改其源码,为编辑团队量身打造输入组件。例如,我为“代码沙盘”区块开发了一个带有语法高亮和实时预览的专用输入组件,编辑体验极佳。
- 实时协作与预览:内容编辑时,可以实时看到其他协作者的光标,并且通过配置,可以实时在预览环境中看到内容发布后的效果,这对内容团队来说是个杀手级功能。
- 清晰的版本与增量更新:所有内容变更都有完整的版本历史,并且其API支持增量数据获取,便于实现高效的内容同步策略。
注意:没有“银弹”。对于小型博客或个人项目,也许Vercel/Netlify自带的文件式CMS(如Netlify CMS)或直接使用静态站点生成器(如Hugo、Jekyll)管理Markdown文件更简单。对于强电商逻辑的项目,可能需要将CMS与专门的电商平台(如Shopify、BigCommerce)的API结合。选型的核心是匹配项目复杂度和团队能力。
4. 从设计到实现:构建一个Sanity+Next.js内容站
下面,我将以Sanity和Next.js(App Router)为例,拆解一个完整项目的搭建过程。我会略过基础的安装命令(npm create sanity@latest和npx create-next-app@latest),重点放在设计思路和关键配置上。
4.1 第一步:设计内容模型——项目的基石
内容模型是项目的蓝图,设计得好,后续开发顺风顺水;设计得不好,后期改动成本巨大。我的设计流程如下:
内容清单:与内容运营团队一起,列出所有需要展示的内容类型。例如:
Post(文章)、Author(作者)、Category(分类)、Tag(标签)、Page(独立页面)。字段拆解:为每种类型定义字段。这里的关键是结构化思维。避免使用一个巨大的“富文本”字段来装所有内容。相反,要拆解。
- 以
Post为例:title(string): 标题slug(slug): 唯一标识符,用于生成URLexcerpt(text): 摘要coverImage(image): 封面图author(reference toAuthor): 引用作者categories(array of references toCategory): 关联分类tags(array of references toTag): 关联标签publishedAt(datetime): 发布时间body(array of blocks):这是核心!使用Sanity的block类型,它可以是一个数组,包含多种预定义的“块”,如paragraph、heading、image、code,以及我自定义的chartBlock、quizBlock等。
- 以
在Sanity中定义:在Sanity项目的
schema文件夹下创建对应的文件,例如post.js。// schemas/post.js export default { name: 'post', title: '文章', type: 'document', fields: [ { name: 'title', title: '标题', type: 'string', validation: Rule => Rule.required().min(5).max(100) }, { name: 'slug', title: 'URL标识', type: 'slug', options: { source: 'title', maxLength: 96, }, validation: Rule => Rule.required() }, { name: 'author', title: '作者', type: 'reference', to: [{type: 'author'}] }, { name: 'body', title: '正文内容', type: 'array', of: [ {type: 'block'}, // 基础文本块 {type: 'image', options: {hotspot: true}}, // 支持热点裁剪的图片 {type: 'code'}, // 代码块 {type: 'chartBlock'}, // 自定义的图表块 ] } ], preview: { select: { title: 'title', author: 'author.name', media: 'coverImage', }, prepare(selection) { const {title, author, media} = selection return { title, subtitle: `作者: ${author}`, media, } } } }实操心得:
slug字段务必设置validation: Rule => Rule.required(),并考虑使用source从标题自动生成,同时提供手动修改入口,这是SEO友好的URL基础。preview配置能让内容列表页的预览更直观,提升编辑效率。
4.2 第二步:定制Sanity Studio——提升编辑效率
默认的Studio已经不错,但定制化才能发挥最大威力。我主要做了两件事:
自定义输入组件:为
chartBlock创建了一个更友好的编辑器。- 在
schema中定义chartBlock类型,包含图表类型(下拉选择)、数据(JSON编辑器)、标题等字段。 - 在
components文件夹下创建ChartInput.js,使用@sanity/ui库和react-json-editor,构建一个带实时预览的输入界面。编辑在后台输入数据,旁边就能看到图表的大致效果,极大减少了出错率。
- 在
配置文档列表和过滤:在
sanity.config.js中,可以定制左侧边栏的文档列表视图。我为“文章”列表添加了按状态(草稿/已发布)、按分类过滤的快捷按钮,方便运营团队管理大量内容。
4.3 第三步:Next.js前端集成——消费内容API
这是前端开发者大展拳脚的地方。我们使用Next.js的App Router和Server Components来获取和渲染数据。
环境配置与客户端初始化:
- 安装Sanity客户端库:
npm install @sanity/client next-sanity - 在项目根目录创建
.env.local文件,添加Sanity项目的projectId和dataset。 - 创建
lib/sanity.js文件,配置并导出Sanity客户端实例。
// lib/sanity.js import { createClient } from '@sanity/client' import { apiVersion, dataset, projectId } from '@/env' export const client = createClient({ projectId, dataset, apiVersion, // e.g., '2023-05-03' useCdn: process.env.NODE_ENV === 'production', // 生产环境用CDN加速 })- 安装Sanity客户端库:
使用GROQ查询数据:在Next.js的Server Component中直接查询。
// app/blog/[slug]/page.js import { client } from '@/lib/sanity' import { notFound } from 'next/navigation' export default async function BlogPostPage({ params }) { const query = `*[_type == "post" && slug.current == $slug][0]{ title, publishedAt, "authorName": author->name, body[]{ ..., _type == "image" => { "url": asset->url, "alt": alt }, _type == "chartBlock" => { ..., "data": data } } }` const post = await client.fetch(query, { slug: params.slug }) if (!post) { notFound() } return ( <article> <h1>{post.title}</h1> <p>By {post.authorName} on {new Date(post.publishedAt).toLocaleDateString()}</p> <PortableText value={post.body} /> {/* 使用@portabletext/react渲染 */} </article> ) }注意:GROQ查询中的投影(Projection)非常强大。上面查询中,
"authorName": author->name实现了数据的“连接”(join),直接从author引用中取出作者名字。对于body数组中的图片,我们将其asset引用解析为实际的URL。这样,前端拿到的是已经“扁平化”、易于使用的数据。实现静态生成与增量静态再生:为了极致的性能,我们使用Next.js的SSG/ISR。
- 在
app/blog/[slug]/page.js中,导出generateStaticParams函数,在构建时预生成所有文章的静态页面。
export async function generateStaticParams() { const query = `*[_type == "post" && defined(slug.current)]{"slug": slug.current}` const posts = await client.fetch(query) return posts.map((post) => ({ slug: post.slug })) }- 在
sanity.config.js中配置webhook,当Sanity中有内容更新时,触发Vercel的部署钩子,重新构建网站,或者对特定页面进行增量静态再生。
- 在
4.4 第四步:处理富文本与自定义块
Sanity的body字段是便携式文本(Portable Text),一种基于JSON的开放规范。前端需要用@portabletext/react库来渲染。
- 安装并配置:
npm install @portabletext/react - 创建自定义渲染组件:
这样,我们就将Sanity后台的“块”,映射为了前端的具体React组件。图片用Next.js的// components/PortableTextRenderer.js import { PortableText } from '@portabletext/react' import Image from 'next/image' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import { dracula } from 'react-syntax-highlighter/dist/esm/styles/prism' import Chart from '@/components/Chart' // 自定义图表组件 const components = { types: { image: ({ value }) => ( <div className="my-8"> <Image src={value.url} alt={value.alt || ' '} width={800} height={600} className="rounded-lg" sizes="100vw" style={{ width: '100%', height: 'auto' }} /> {value.caption && <p className="text-center text-sm mt-2">{value.caption}</p>} </div> ), code: ({ value }) => ( <SyntaxHighlighter language={value.language || 'text'} style={dracula}> {value.code} </SyntaxHighlighter> ), chartBlock: ({ value }) => <Chart data={value.data} type={value.chartType} />, }, marks: { link: ({ children, value }) => ( <a href={value.href} className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer"> {children} </a> ), }, } export default function PortableTextRenderer({ value }) { return <PortableText value={value} components={components} /> }Image组件优化,代码块用高亮组件美化,图表块用我们自己的Chart组件渲染。内容与表现完全分离,且前端拥有完全的渲染控制权。
5. 避坑指南与性能优化实战
在实际部署和运营中,会遇到一些典型问题。以下是我总结的“血泪经验”。
5.1 API查询性能优化
GROQ虽然强大,但查询不当也会慢。关键在于只取所需字段和善用管道符。
- 反面教材:
*[_type == "post"]会取出所有文章的所有字段(包括巨大的body内容),在列表页使用会非常慢。 - 正确做法:列表页只取标题、摘要、封面图、发布时间等必要字段。
*[_type == "post"] | order(publishedAt desc) [0...10] { _id, title, slug, excerpt, "coverImageUrl": coverImage.asset->url, publishedAt, "authorName": author->name } - 使用管道符过滤和排序:
| order(publishedAt desc) [0...10]在数据库层面完成排序和分页,比把所有数据取到前端再处理高效得多。
5.2 图片优化策略
图片是性能杀手。Sanity的图片管道非常强大。
在查询时指定尺寸和格式:不要直接使用原始图片URL。
"coverImageUrl": coverImage.asset->url + "?w=800&h=600&fit=crop&auto=format"w和h指定宽高,fit=crop指定裁剪模式,auto=format让Sanity根据浏览器支持自动提供WebP等现代格式。这比在前端用next/image的loader属性更灵活,且不依赖Next.js。结合Next.js Image组件:为了获得最佳的Core Web Vitals评分,特别是LCP(最大内容绘制),建议将Sanity配置为Next.js的Image Loader。
- 在
next.config.js中配置:
module.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.sanity.io', pathname: '/images/**', }, ], }, }- 前端使用:
<Image src={url} width={800} height={600} alt="..." />。Next.js会自动处理响应式图片、懒加载等优化。
- 在
5.3 实时预览的实现陷阱
在Sanity Studio中实现实时预览(Preview)是一个很棒的功能,但需要注意:
- 区分草稿和已发布内容:在预览查询中,需要使用
_id in path("drafts.**")来同时获取草稿和已发布文档。否则,编辑草稿时预览可能看不到最新更改。 - 处理身份验证:预览通常需要在前端应用中进行,这意味着你的Next.js应用需要能处理未发布的内容。一个常见的模式是创建一个带秘钥的预览API路由,Sanity Studio通过这个秘钥来访问预览内容。务必做好秘钥的权限控制和过期管理,防止内容泄露。
- 性能考虑:实时预览意味着每次编辑都会触发前端页面的重新获取和渲染。对于复杂页面,可能会造成Studio卡顿。可以考虑使用防抖(debounce)或只在显式点击“预览”按钮时更新。
5.4 内容迁移与版本管理
从旧CMS迁移到新CMS是常见需求。Sanity提供了强大的数据导入/导出工具和CLI。
- 使用Sanity CLI进行迁移:编写Node.js脚本,从旧数据库(如MySQL)中读取数据,按照定义好的Sanity Schema转换成文档,然后使用
@sanity/client的createOrReplace操作批量导入。 - 关键点:
- 处理媒体文件:图片和文件需要先上传到Sanity的资产存储,获取到
_ref后再关联到文档。 - 保持ID关联:在迁移过程中,尽量保持或映射旧数据的唯一ID,这对于处理内部引用(如文章关联作者)和后续的数据验证非常重要。
- 分批次进行:对于大量数据,务必分批次导入,并做好错误日志记录和重试机制。
- 处理媒体文件:图片和文件需要先上传到Sanity的资产存储,获取到
- 版本回滚:Sanity自动保存所有内容变更的历史。通过其API或Studio,可以查看任何文档的完整修改记录,并在必要时回滚到任意版本。这是一个非常重要的内容安全网。
6. 扩展思考:当CMS成为“数字资产中心”
当我们把CMS从“建站工具”的思维中解放出来后,它的想象力边界被大大拓宽了。它不再仅仅服务于一个网站,而是可以成为整个企业的数字资产中心。
- 跨平台内容分发的枢纽:如前所述,一套内容API可以喂给官网、App、小程序、邮件营销模板、电子书生成器,甚至线下大屏。
- 与业务系统深度集成:通过webhook或反向API调用,可以将CMS与你的CRM、ERP、客服系统连接起来。例如,当CMS发布一篇新的产品教程时,自动在客服知识库中创建一条记录,或向订阅用户推送通知。
- 支持A/B测试与个性化:高级的无头CMS(如Contentful)开始提供内容变体(Variations)和个性化(Personalization)功能。你可以针对不同用户群体发布不同版本的内容,并通过API将用户上下文(如地理位置、浏览历史)传递给CMS,获取个性化的内容流。
- 作为低代码/无代码平台的后台:许多可视化建站工具(如Webflow)和移动端搭建平台,都支持连接无头CMS作为内容源。这使得业务人员可以在不写代码的情况下,搭建出内容驱动的页面和应用。
我个人的体会是,技术选型上的这次“分离”,不仅仅是架构的升级,更是一种思维模式的转变。它要求我们更清晰地定义内容的边界(模型),更严谨地设计数据的契约(API),更开放地思考内容的消费场景(前端)。这个过程起初可能会有更高的学习成本和设计成本,但一旦跑通,它带来的灵活性、性能和可维护性优势,会在项目的整个生命周期中持续带来回报。对于任何有一定复杂度且对内容灵活性有要求的数字项目,拥抱这种“新CMS”哲学,已经从一个可选项,变成了一个必选项。
