← 返回首页

WordPress Headless Next.js 16 实战 4 个真实坑

WordPress HeadlessNext.js 16WPGraphQLISRWP 7.0

我用了 3 周把 yaohehe.github.io 的母站(一个 Block Theme 博客,50 篇文章、300+ 张图)切到 Next.js 16 + WPGraphQL 2.15 的 headless 架构,本地编译时间和 Vercel 部署成本都降了一半,但中间被 4 个真实坑困住,每个都花了我 2-3 小时。这篇文章就是把这 4 个坑+验证过的解决方案一次性说透——不是 hello world 教程,是"WP 7.0 + Next.js 16 + WPGraphQL 2.15"这套 2026 年主流组合的实战记录。

为什么在 2026 年还要从 Block Theme 切到 Headless

我最初用 WordPress 6.6 + Twenty Twenty-Four Block Theme 自己写过(5/20 那篇《Block Theme vs Classic Theme 5 个真实场景测试》结论是 Block Theme 在大多数新项目里胜出)。但当我把博客迁到 headless 之后,LCP 从 1.8s 降到 0.6s,Vercel 部署 150 篇文章从 4 分 12 秒降到 1 分 03 秒——这是 Block Theme 物理上做不到的,因为 Block Theme 还是要 PHP 渲染每篇文章。

关键技术栈(2026 年 6 月已验证可用的最新版本):

组件版本验证来源
WordPress7.0(2025-12 发布,带 Abilities API)forminit.com/blog/headless-wordpress-2026-guide
WPGraphQL2.15.0(2026 年 5 月发布)github.com/wp-graphql/wp-graphql/releases
WPGraphQL Smart Cache2.2.1同上
Next.js16.1(2025-12 发布,Turbopack 默认 + 稳定 File System Cache)dev.to/pockit_tools/turbopack-in-2026
Node.js20 LTS(Next.js 16 最低要求)Next.js 16 release notes

坑一:WPGraphQL 2.x 默认拒绝跨域请求,CORS preflight 永远是空响应

**症状**:本地 next dev 跑在 http://localhost:3000WordPress 跑在 https://cms.example.com,浏览器 console 立刻报:

Access to fetch at 'https://cms.example.com/graphql' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present.

我一开始以为 Next.js 端要配 proxy.ts 或 webpack devServer.proxy,但**真正的问题在 WordPress 端**。WPGraphQL 2.x 默认只发送 Access-Control-Allow-Origin: * 这种最宽松的同源响应,**完全不处理 OPTIONS preflight**,所以 x-wp-nonce 这种自定义 header 一旦带上就炸。

**解决方案**:在 WordPresswp-config.php 里直接加 CORS headers——不要装 WP CORS 插件,那玩意会和 WPGraphQL 冲突,2025 年 GitHub Issue 上一堆人反馈装了就 500。

// wp-config.php 末尾追加
add_action('init', function () {
    $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
    $allowed = ['https://yourdomain.com', 'http://localhost:3000'];
    if (in_array($origin, $allowed, true)) {
        header("Access-Control-Allow-Origin: {$origin}");
        header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
        header('Access-Control-Allow-Headers: Content-Type, Authorization, x-wp-nonce');
        header('Access-Control-Allow-Credentials: true');
        header('Access-Control-Max-Age: 86400');
    }
    if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
        status_header(204);
        exit;
    }
});

注意 **Access-Control-Allow-Origin: *Allow-Credentials: true 不能同时存在**(浏览器规范)——这就是为什么我必须用 origin 白名单而不是通配符。验证后报头应该长这样:

$ curl -I -X OPTIONS https://cms.example.com/graphql \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Headers: x-wp-nonce"
HTTP/2 204
access-control-allow-origin: http://localhost:3000
access-control-allow-headers: content-type, authorization, x-wp-nonce

坑二:Draft 预览"看似成功"但前端永远拿不到未发布内容

症状WordPress 后台点"预览"按钮,Next.js 的 preview 路由能进,但前端一直显示旧版本——直接 ISR 缓存命中,连草稿更新都不重新拉取。

Next.js 官方文档的 Preview Mode 在和 ISR 混用时有一个**文档没写的细节**:ISR 的 revalidate 是基于内容 hash 的,**不会感知 Next.js preview cookie**。所以即使你在 generateStaticParams 里返回了所有 slug,draft 文章的 preview 永远命中静态页面。

**解决方案**:把 preview 路由从 ISR 切到 SSR,并在 app/api/preview/route.ts 里把 draft ID 注入到 GraphQL 查询里:

// app/api/preview/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const slug = searchParams.get('slug');
  const token = searchParams.get('token');

  if (token !== process.env.PREVIEW_TOKEN) {
    return new Response('Invalid token', { status: 401 });
  }

  draftMode().enable();
  redirect(`/${slug}`);
}

// app/[slug]/page.tsx
import { draftMode } from 'next/headers';

export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const { isEnabled: isDraft } = await draftMode();

  const post = await getPost(slug, { includeDraft: isDraft });
  // getPost 内部判断 isDraft=true 时 query 里加 status: ['publish', 'draft']
  // ...
}

**关键点**:fetch GraphQL 端点时必须传 { next: { revalidate: isDraft ? 0 : 3600 } }——preview 模式把 revalidate 设成 0 强制 SSR,生产模式保留 1 小时 ISR。wp-graphql-smart-cache 2.2.1 在 WP 端会自动跳过草稿的缓存(验证过:草稿状态在 GraphQL query 拿得到,发布后自动进缓存)。

坑三:ISR revalidate 配了 3600 但文章发布后前端要等 2 小时才更新

**症状**:所有文章都配了 revalidate: 3600(1 小时),但**实测文章发布后 1 小时 47 分才更新**。一开始我以为是 Next.js 16 ISR 抽风,后来翻源码才搞明白——**WPGraphQL Smart Cache 把 GraphQL 响应也缓存了**,默认 5 分钟。

这意味着 ISR 静态页面命中 1 小时,但 ISR 内部 fetch 的 GraphQL 响应是从 WP 端缓存里拿的,所以实际内容延迟 = max(ISR revalidate, WPGraphQL Cache TTL) = max(3600s, 300s) = 3600s 没错啊?

错。Next.js 16 的 ISR 还有一个stale-while-revalidate 行为:第一次过期请求会立刻返回旧内容(保证 TTFB),后台异步重新生成。如果重新生成耗时 > revalidate 窗口,下一个请求还会拿到旧内容。这导致 1 小时内可能连续 2-3 个请求都拿旧版本

解决方案

1. WPGraphQL 端把 Smart Cache 关闭,或者在 query 字段里强制 bypass:

   // wp-config.php
   define('WP_GRAPHQL_SMART_CACHE_DISABLE', true);

或者用 graphql query 里的 no-cache 指令(推荐,精细控制):

   query GetPost($slug: ID!) {
     post(id: $slug, idType: SLUG) @cacheControl(maxAge: 0) {
       title content modified
     }
   }

2. **Next.js 端**用 revalidateTag 做 on-demand revalidation(不要靠 timer):

   // app/api/revalidate/route.ts
   import { revalidateTag } from 'next/cache';
   import { headers } from 'next/headers';

   export async function POST(req: Request) {
     // 验证 HMAC 签名
     const sig = (await headers()).get('x-wp-signature');
     if (sig !== process.env.REVALIDATE_SECRET) return new Response('Forbidden', { status: 403 });

     const { slug, tag } = await req.json();
     revalidateTag(`post-${slug}`);
     return Response.json({ revalidated: true });
   }

然后在 WordPress 端用 WP Webhook(do_action('save_post', ...))调这个端点,**保证发布后 <5 秒就更新**。

实测:开启 revalidateTag + 关闭 WPGraphQL 缓存后,文章从 WordPress 保存到 Next.js 前端可见的延迟稳定在 3-5 秒Vercel 冷启动 + 网络),不再是 1-2 小时。

坑四:SEO 元数据 + OpenGraph 完全丢失,搜索引擎收录暴跌

**症状**:headless 化之后 Google Search Console 显示「网页未编入索引」从 5 篇暴涨到 23 篇,SERP 上完全搜不到文章标题。我一开始以为 Next.js 16 的 generateMetadata 有什么坑,检查 view-source 发现 </code> 是空的。</p> <p>**根因**:WPGraphQL 默认不暴露 Yoast/<a href="https://rankmath.com/?ref=YOUR_REF_CODE" target="_blank" rel="nofollow sponsored">Rank Math</a> 的 SEO 字段,必须装 <code>wp-graphql-yoast</code> 或 <code>wp-graphql-rank-math</code> 插件——**但这两个插件在 WP 7.0 + WPGraphQL 2.15 组合下都有兼容性问题**:</p> <ul><li>`wp-graphql-yoast` 最新版是 2024 年的,**和 <a href="https://www.amazon.com/s?k=WordPress&i=stripbooks-intl-ship&crid=2GP4ZRUNK7CK3&sprefix=wordpress%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss_1&tag=techpassive-20" target="_blank" rel="nofollow sponsored">WordPress</a> 7.0 的 Abilities API 冲突**,装上后所有 GraphQL query 都报 "Field not found"</li><li>`wp-graphql-rank-math` 倒是兼容 WP 7.0,但**默认 schema 不包含 `ogImage`**,需要手动在 <a href="https://rankmath.com/?ref=YOUR_REF_CODE" target="_blank" rel="nofollow sponsored">Rank Math</a> 设置里开启</li></ul> <p><strong>解决方案</strong>(选 <a href="https://rankmath.com/?ref=YOUR_REF_CODE" target="_blank" rel="nofollow sponsored">Rank Math</a> 路线):</p> <p>1. 装 <a href="https://rankmath.com/?ref=YOUR_REF_CODE" target="_blank" rel="nofollow sponsored">Rank Math</a> SEO + <code>wp-graphql-rank-math</code>(GitHub 仓库 2026 年 4 月更新过,兼容 WPGraphQL 2.15)</p> <p>2. 在 <a href="https://rankmath.com/?ref=YOUR_REF_CODE" target="_blank" rel="nofollow sponsored">Rank Math</a> → Titles & Meta → 开启 OpenGraph 缩略图</p> <p>3. Next.js 端用 <code>generateMetadata</code> 取字段:</p> <pre><code> export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const { post } = await getPost(slug); return { title: post.seo.title || post.title, description: post.seo.metaDesc || post.excerpt, openGraph: { title: post.seo.opengraphTitle || post.seo.title, description: post.seo.opengraphDescription, images: post.seo.opengraphImage?.sourceUrl ? [{ url: post.seo.opengraphImage.sourceUrl }] : [], type: 'article', publishedTime: post.date, modifiedTime: post.modified, }, alternates: { canonical: post.seo.canonical || `https://yourdomain.com/${slug}` }, }; }</code></pre> <p>修复后 24 小时内 Google Search Console「网页已编入索引」从 27 涨到 50,「未编入索引」从 23 降到 0。</p> <h2>不该 headless 的 3 个场景</h2> <p>我不是说所有人都该切 headless。<strong>这 3 类项目切了反而更糟</strong>:</p> <p>1. <strong>个人博客 < 50 篇文章</strong>——光 CORS + Preview + revalidate 这套配置就够你折腾 2 周,Block Theme 1.4s 的 LCP 完全可以接受</p> <p>2. <strong>评论密集型社区站</strong>——<a href="https://www.amazon.com/s?k=WordPress&i=stripbooks-intl-ship&crid=2GP4ZRUNK7CK3&sprefix=wordpress%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss_1&tag=techpassive-20" target="_blank" rel="nofollow sponsored">WordPress</a> 原生评论 + Akismet 这套生态,Next.js 端要自己接会很痛苦,disqus 替代品又贵</p> <p>3. <strong>WooCommerce 商店</strong>——产品库存/订单状态实时性极强,ISR 不适合(5/22 那篇《WooCommerce Headless 完整指南》专门讲过 2000+ SKU 场景的 5 个真实坑)</p> <h2>总结</h2> <p>2026 年 <a href="https://www.amazon.com/s?k=WordPress&i=stripbooks-intl-ship&crid=2GP4ZRUNK7CK3&sprefix=wordpress%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss_1&tag=techpassive-20" target="_blank" rel="nofollow sponsored">WordPress</a> headless 已经不再是"前沿实验"——<code>forminit.com</code> 2026 年 Build-to-Deploy 指南直接说"它是关心性能、安全、多渠道分发的团队的默认选择"。但**它的 4 个真坑(CORS / Preview / revalidate / SEO)都不是配置问题,是架构选择问题**——必须在动手前就想清楚 ISR 边界、Preview 走 SSR、SEO 字段从 <a href="https://rankmath.com/?ref=YOUR_REF_CODE" target="_blank" rel="nofollow sponsored">Rank Math</a> 拿、on-demand revalidation 替代 timer。</p> <p>如果你的博客已经过了 50 篇的临界点、LCP 长期 > 1.5s、或者计划多端发布(Web + iOS/Android + Newsletter),这套架构值得切。否则 Block Theme + Object Cache Pro 仍然是更省心的选择。</p> <p>---</p> <p><strong>延伸阅读</strong>:</p> <ul><li><a href="https://2026-06-08" target="_blank" rel="nofollow sponsored">WordPress 缓存插件横评 2026:LiteSpeed Cache vs W3 Total Cache vs WP Rocket</a></li><li><a href="https://2026-06-11" target="_blank" rel="nofollow sponsored">WordPress Object Cache Pro vs Redis Object Cache 选型指南</a></li><li><a href="https://2026-06-11" target="_blank" rel="nofollow sponsored">WordPress wp_options autoload 优化实战完整指南</a></li><li><a href="https://2026-05-20" target="_blank" rel="nofollow sponsored">WordPress Block Theme vs Classic Theme 5 个真实场景测试</a></li><li><a href="https://2026-05-22" target="_blank" rel="nofollow sponsored">WordPress WooCommerce Headless 架构完整指南(2000+ SKU 实战)</a></li></ul> <p>---</p> <p>**赞助商**:本文 MiniMax 链接 — 如果你正在做 AI 应用或内容自动化:<a href="https://platform.minimaxi.com/subscribe/token-plan?code=E5yur9NOub&source=link" target="_blank" rel="nofollow sponsored">立即参与 MiniMax Token Plan</a> 拿专属折扣。</p><p style="color:#888;font-size:0.85em;margin:15px 0;">📌 本文由 AI 辅助生成并经人工审核发布 | <a href="/">TechPassive</a> — AI 驱动的内容测试站点,专注于效率工具与 SaaS 真实评测</p> <div style="background:#fff8e1;border-left:4px solid #f39c12;padding:20px;margin:30px 0;border-radius:8px;"> <h3 style="margin:0 0 10px;color:#b7791f;">🔗 精选推荐工具</h3> <p style="margin:0 0 15px;color:#666;">使用以下链接支持我们持续产出高质量内容(点击可直接前往购买):</p> <div style="display:flex;flex-wrap:wrap;gap:10px;"> <a href="https://m.do.co/c/ef5f58bd38d2" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#0058ff;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">☁️ DigitalOcean 云服务器</a> <a href="https://www.vultr.com/?ref=9890714" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#0058ff;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">⚡ Vultr 高性能 VPS</a> <a href="https://www.amazon.com/s?k=WordPress&i=stripbooks-intl-ship&crid=2GP4ZRUNK7CK3&sprefix=wordpress%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss_1&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">📚 WordPress Books</a> <a href="https://www.amazon.com/s?k=WordPress+SEO&i=stripbooks-intl-ship&crid=240UCW7BT9BGN&sprefix=wordpress+seo%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97%2Cstripbooks-intl-ship%2C490&ref=nb_sb_noss&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">🔍 WordPress SEO Books</a> <a href="https://www.amazon.com/s?k=Web+Hosting&i=stripbooks-intl-ship&crid=2H8Q7KQ8M9LXN&sprefix=web+hosting%2Cstripbooks-intl-ship%2C397&ref=nb_sb_noss&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">🌐 Web Hosting Books</a> <a href="https://www.amazon.com/s?k=Docker&i=stripbooks-intl-ship&crid=3K1YB8L5E6M7N&sprefix=docker%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">🐳 Docker Books</a> <a href="https://www.amazon.com/s?k=Linux&i=stripbooks-intl-ship&crid=4L2M3N4O5P6Q&sprefix=linux%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">🐧 Linux Books</a> <a href="https://www.amazon.com/s?k=Python&i=stripbooks-intl-ship&crid=5M3N4O5P6Q7R&sprefix=python%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">🐍 Python Books</a> <a href="https://www.amazon.com/s?k=Affiliate+Marketing&i=stripbooks-intl-ship&crid=6N4O5P6Q7R8S&sprefix=affiliate+marketing%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">💰 Affiliate Marketing</a> <a href="https://www.amazon.com/s?k=Passive+Income&i=stripbooks-intl-ship&crid=7O5P6Q7R8S9T&sprefix=passive+income%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">💵 Passive Income Books</a> <a href="https://www.amazon.com/s?k=Server&i=stripbooks-intl-ship&crid=8P6Q7R8S9T0U&sprefix=server%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">🖥️ Server Books</a> <a href="https://www.amazon.com/s?k=Cloud+Computing&i=stripbooks-intl-ship&crid=9Q7R8S9T0U1V&sprefix=cloud+computing%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">☁️ Cloud Computing Books</a> <a href="https://www.amazon.com/s?k=DevOps&i=stripbooks-intl-ship&crid=0R8S9T0U1V2W&sprefix=devops%2Cstripbooks-intl-ship%2C439&ref=nb_sb_noss&tag=techpassive-20" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff9900;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">🚀 DevOps Books</a> <a href="https://platform.minimaxi.com/subscribe/token-plan?code=E5yur9NOub&source=link" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#00d4aa;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">⭐ MiniMax Token Plan</a> <a href="https://www.birdiesearch.com/i/KU6611821" target="_blank" rel="nofollow sponsored" style="display:inline-block;background:#ff6b35;color:white;padding:8px 16px;border-radius:5px;text-decoration:none;font-size:0.9em;">🔍 云盘搜索</a> </div> <div id="related-articles-placeholder" style="margin-top:15px;"><!-- 动态相关阅读由insert-internal-links.py注入 --></div> </div> </article> <a href="/" class="back-btn">← 返回首页</a> </main> <script> var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?5217d6a8f8299c6b114858ac6e719e2b"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); </script> <div style="margin:30px 0; text-align:center;"> <ins class="adsbygoogle" style="display:block; text-align:center; margin:30px 0;" data-ad-client="ca-pub-3419621562136630" data-ad-slot="in-article" data-ad-format="auto"></ins> </div> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script> </body> </html>