本文约 5,051 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:想要 CDN 全站静态,又舍不得 WordPress 的后台
做内容站的痛点通常是两难的:纯静态站(Hugo/Jekyll/Astro)性能好但编辑体验差,客户不懂 Markdown;WordPress 后台好用,但每次请求都要跑 PHP 和 MySQL,流量高峰时 TTFB 直接飙到 800ms 以上。Headless 就是在这种张力下出现的折中——WordPress 继续当编辑后台和数据源,前端交给 Node 框架渲染成 HTML/CDN 静态文件。代价也很真实:预览功能要重做、SEO 元信息要自己接管、部署从”传个 FTP”变成两套系统。
原理分析:取数方式决定了整套架构
Headless 的第一步是决定前端怎么拿数据:
- REST API:WordPress 内置,无需插件。优点是零依赖,缺点是字段冗余、多次往返(拿文章 + 作者 + 分类要三四个请求)。
- WPGraphQL:安装同名插件后提供
/graph端点,一次查询取完关联数据;配套的WPGraphQL for WooCommerce扩展覆盖商品、订单、变量商品。对复杂页面尤其划算。
第二步是渲染策略。Next.js 的 Incremental Static Regeneration(ISR,页面级 revalidate)允许页面像静态一样由 CDN 提供,同时按时间或按 webhook 重生。对博客、文档、导购页这种”写少读多”的场景几乎是标配。Astro/Nuxt 也提供了各自的 ISR/混合渲染能力。
第三步是变更联动:内容在 WordPress 里更新后,需要告诉前端”这篇要重生”。做法是在 WordPress 侧挂 save_post(或 wp_after_insert_post)打一个带密钥的 HTTP 请求到前端的 revalidate 接口。
| 组件 | 常见选择 | 说明 |
|---|---|---|
| 后端 | WordPress + WPGraphQL | 域名建议用 admin.xxx.com |
| 前端 | Next.js / Nuxt / Astro | 按团队熟悉度选 |
| 数据 | GraphQL 查询 | 支持 fragment 复用 |
| 触发 | save_post → revalidate webhook | 需共享密钥 |
| 部署 | Vercel/Netlify + 独立 WP 主机 | 两端独立伸缩 |
保姆级实操
步骤 1:约定域名边界。 WordPress 后台域名(如 admin.example.com)与前端域名(如 www.example.com)分开。Hoisting 修改 WordPress 站点地址后,用 REST URL 过滤器把对外输出的地址指回前端:
// 后端站点地址设为 admin.example.com,对外 REST 地址仍由后端提供
add_filter( 'rest_url', function ( $url ) {
return str_replace( 'https://admin.example.com', 'https://www.example.com', $url );
} );
步骤 2:安装 WPGraphQL 并验证。 插件启用后访问 /graph,用 GraphiQL IDE 试查询。注意它默认只暴露公开数据,admin.example.com/graphql 请勿对搜索引擎开放索引(GraphQL 端点本身也不该被索引)。
步骤 3:Next.js 侧用 App Router 取数据。 下面代码是真实可用的 App Router 写法:
const WP_GRAPHQL = process.env.WP_GRAPHQL_ENDPOINT;
async function gql(query, variables = {}, revalidate = 600) {
const res = await fetch(WP_GRAPHQL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
next: { revalidate }
});
if (!res.ok) throw new Error('GraphQL 请求失败:' + res.status);
const json = await res.json();
if (json.errors) throw new Error(json.errors[0].message);
return json.data;
}
export async function generateMetadata({ params }) {
const data = await gql(
`query PostMeta($slug: ID!) {
post(id: $slug, idType: SLUG) { title seo { metaDesc } }
}`,
{ slug: params.slug }
);
return { title: data.post.title, description: data.post.seo?.metaDesc ?? '' };
}
export default async function Page({ params }) {
const data = await gql(
`query Post($slug: ID!) {
post(id: $slug, idType: SLUG) { title content date author { node { name } } }
}`,
{ slug: params.slug }
);
return (
<article>
<h1>{data.post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: data.post.content }} />
</article>
);
}
上例里 seo { metaDesc } 依赖 Yoast SEO 的 WPGraphQL 扩展(WPGraphQL Yoast SEO Addon),没装则要自己在后台用 register_graphql_fields 补充 meta 字段。
步骤 4:WordPress 内容更新触发前端重生:
add_action( 'wp_after_insert_post', function ( $post_id, $post ) {
$trigger = [ 'publish', 'draft', 'trash' ];
if ( ! in_array( $post->post_status, $trigger, true ) || wp_is_post_revision( $post_id ) ) {
return;
}
$slug = rawurlencode( $post->post_name );
$secret = get_option( 'mf_revalidate_secret' );
$endpoint = sprintf( 'https://www.example.com/api/revalidate?slug=%s&secret=%s', $slug, $secret );
wp_remote_get( $endpoint, [ 'timeout' => 5, 'blocking' => false ] );
}, 10, 2 );
注意 blocking => false 很关键,否则编辑每次点发布都要等 HTTP 往返,后台明显卡顿。
步骤 5:前端侧实现重验证接口。 Next.js 里用 revalidatePath 按需刷新具体路径:
// app/api/revalidate/route.js
import { revalidatePath } from 'next/cache';
export async function GET(request) {
const { searchParams } = new URL(request.url);
if (searchParams.get('secret') !== process.env.REVALIDATE_SECRET) {
return Response.json({ message: 'invalid secret' }, { status: 401 });
}
const slug = searchParams.get('slug');
if (!slug) return Response.json({ message: 'missing slug' }, { status: 400 });
revalidatePath(`/blog/${slug}`);
return Response.json({ revalidated: true, now: Date.now() });
}
步骤 6:Nginx 分层与预览解决。 后台域名禁止搜索引擎索引,前端域名不代理 /wp-admin(减少攻击面):
server {
server_name www.example.com;
location / {
proxy_pass http://127.0.0.1:3000; # Node 前端
proxy_set_header Host $host;
}
location /wp-content/uploads/ {
proxy_pass https://admin.example.com; # 图片等静态资源回源后端
}
}
常见踩坑
- 后端与前端共用同一域名并用路径区分(
/blog走 Node,/wp-admin走 PHP),Nginx location 优先级排错导致后台文件被 Node 接管。 - 案例: 某站 Headless 化后没做任何重验证触发,编辑更新文章三天后线上依旧是旧内容,被客户判定为”系统坏了”。
dangerouslySetInnerHTML直接输出 WordPress 正文,若后端被入侵则前端成为 XSS 放大器,应做 HTML 白名单清洗。- 预览功能失效:草稿在 GraphQL 里默认查不到,需要 token 或专门开启 preview 插件。
- 忘了给
wp-json/graph端点加速率限制,被脚本刷到 GraphQL 复杂查询拖垮 PHP。 - 图片 URL 仍是后台域名,前端域名下图片走跨域(多数情况可用,但 CDN 规则与证书要同步)。
- 后台主题从未升级,PHP 版本依旧 7.4,被安全扫描器标记为风险项。WordPress 性能优化与缓存策略
总结
判断要不要 Headless 的标准:内容更新频率低而读取量大、前端团队能维护 Node 服务、且有明确的性能诉求——三条满足两条才值得做;反之纯 WordPress + 全页缓存更划算。行动清单:前后端分域名 → 选 REST 或 GraphQL → 确定 ISR 策略 → 做好重验证钩子 → Nginx 分层 + 图片回源。切忌为了”技术先进”而把自己拖入两套运维。若仍想用 REST 而非 GraphQL,可以参考 WordPress REST API 输出优化。
