我已经将十多个 WordPress 网站迁移到生产环境中的 Next.js,我要告诉你一件可能会让你惊讶的事情:实际的代码迁移并不是难点。真正的难点在于切换。那个让你祈祷一切不会出错的恐怖时刻。好消息是什么?有了正确的方案,你可以让这一刻变得无聊。在运维中,无聊正是你想要的。

这是我们在 Social Animal 用于生产切换的方案。它不是理论上的——它是从真实迁移中总结出来的经验,其中真实的收入都在线。我们讨论的是日营收 50,000 美元的电子商务网站、月浏览量数百万的内容发布商,以及 5 分钟停机时间意味着 CEO 会给你打电话的 SaaS 营销网站。

目录

零停机 CMS 迁移:WordPress 到 Next.js 切换方案

为什么零停机比你想象的更重要

让我们用数字说话。谷歌 2024 年的研究显示,页面加载延迟 1 秒大约会导致 7% 的转化率下降。现在想象你的网站根本无法访问。哪怕只有 5 分钟。

以下是真正的风险:

停机时长 收入影响(日营收 10,000 美元的网站) SEO 影响 用户信任影响
5 分钟 ~35 美元损失 最小化(如果是孤立事件)
30 分钟 ~208 美元损失 Googlebot 可能注意到 中等
2 小时 ~833 美元损失 Google Search Console 爬虫错误
24 小时 10,000 美元以上损失 反链接风险 严重

但这不仅仅是收入问题。搜索引擎在不断爬取。如果 Googlebot 在迁移期间访问你的网站并获得 500 错误,这些 URL 可以在几小时内从索引中消失。我见过一个网站因为有人在午餐时间进行"快速迁移"而损失了 40% 的有机流量。

目标不仅是零停机。而是在转换期间对用户和爬虫零可见变化。

迁移架构概览

在我们进入各个阶段之前,让我们看看我们的目标架构。基本模式是并行运行两个系统,然后原子性地转移流量。

                    ┌─────────────────┐
                    │   Cloudflare /   │
                    │   负载均衡器     │
                    └────────┬────────┘
                             │
                    ┌────────┴────────┐
                    │   流量路由器    │
                    │  (基于权重)    │
                    └────┬───────┬────┘
                         │       │
              ┌──────────┴──┐ ┌──┴──────────┐
              │  WordPress  │ │   Next.js   │
              │  (蓝色)    │ │  (绿色)   │
              │   源站      │ │  在 Vercel  │
              └──────────┬──┘ └──┬──────────┘
                         │       │
              ┌──────────┴──┐ ┌──┴──────────┐
              │  MySQL 数据库│ │  无头 CMS   │
              │             │ │(Sanity/等)  │
              └─────────────┘ └─────────────┘

关键洞察:你不仅仅在迁移前端。你在迁移内容层、渲染层和交付层——它们可以各自独立迁移。

第一阶段:内容迁移和 API 层

这是大多数团队一开始犯错的地方。他们尝试先构建 Next.js 前端,然后再处理内容。不要这样做。从内容开始。

选择你的无头 CMS

你的 WordPress 内容需要一个新的家。这个选择对迁移复杂性影响很大:

CMS 从 WP 迁移的难度 实时同步可能 定价(2025) 最适合
Sanity 高(结构化内容映射良好) 是,通过 webhooks 免费套餐,然后 99 美元/月 复杂的内容模型
Contentful 中等(需要字段映射) 300 美元/月(团队) 企业团队
Strapi 高(类似数据库驱动的模型) 自托管免费,云版本从 29 美元/月 完全控制
WordPress REST API N/A(作为无头 CMS 保留) 已同步 现有托管成本 快速获胜
Payload CMS 自托管免费 开发者优先的团队

我们在我们的无头 CMS 开发能力页面上深入讨论了无头 CMS 选择,但简短版本:对于大多数 WordPress 迁移,Sanity 或 Payload CMS 给你最好的迁移路径。

设置内容同步

这是关键部分:在并行运行阶段,内容需要同时存在于两个系统中。你有两种策略:

策略 A:一次性迁移 + 冻结 将所有内容迁移到新 CMS,然后冻结 WordPress 编辑。这适用于小网站,但当编辑需要继续发布时会崩溃。

策略 B:持续同步(推荐) 设置一个同步管道,以近实时的方式将 WordPress 更改复制到新 CMS。

// 示例:WordPress webhook 处理程序,同步到 Sanity
// 这作为无服务器函数运行(Vercel/AWS Lambda)

import { createClient } from '@sanity/client';

const sanity = createClient({
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: 'production',
  token: process.env.SANITY_WRITE_TOKEN,
  apiVersion: '2025-01-01',
  useCdn: false,
});

export async function POST(request) {
  const payload = await request.json();
  const { post_id, post_title, post_content, post_status } = payload;

  if (post_status !== 'publish') return new Response('Skipped', { status: 200 });

  try {
    await sanity.createOrReplace({
      _id: `wp-${post_id}`,
      _type: 'post',
      title: post_title,
      body: convertGutenbergToPortableText(post_content),
      migratedFrom: 'wordpress',
      wpId: post_id,
      _updatedAt: new Date().toISOString(),
    });

    return new Response('Synced', { status: 200 });
  } catch (error) {
    console.error('Sync failed:', error);
    return new Response('Sync error', { status: 500 });
  }
}

你还需要 WordPress 端。我们使用一个在 save_post 上触发的简单插件:

// wp-content/plugins/headless-sync/headless-sync.php
add_action('save_post', function($post_id, $post) {
    if (wp_is_post_revision($post_id)) return;
    
    wp_remote_post(SYNC_ENDPOINT_URL, [
        'body' => json_encode([
            'post_id' => $post_id,
            'post_title' => $post->post_title,
            'post_content' => $post->post_content,
            'post_status' => $post->post_status,
        ]),
        'headers' => [
            'Content-Type' => 'application/json',
            'X-Sync-Secret' => SYNC_SECRET,
        ],
    ]);
}, 10, 2);

运行此同步至少 2 周后再切换。你想捕捉边界情况——奇怪的短代码、自定义文章类型、不能清晰映射的 ACF 字段。

零停机 CMS 迁移:WordPress 到 Next.js 切换方案 - 架构

第二阶段:构建 Next.js 应用

我不会在这里涵盖完整的 Next.js 构建过程——这值得有自己的文章(我们在Next.js 开发方面有深厚的专业知识)。但有一些特定于迁移的问题对零停机很重要。

URL 奇偶性是不可协商的

你的 WordPress 网站上存在的每一个 URL 必须解析到 Next.js 网站上的相同内容。每一个。

这意味着:

  • /blog/my-post-slug/ 必须工作(包括尾部斜杠,如果 WordPress 使用的话)
  • /category/technology/ 必须工作或重定向
  • /wp-content/uploads/2024/03/image.jpg 必须重定向到你的新图像 CDN
  • /feed/ 必须仍然返回有效的 RSS/Atom
  • 分页 URL 如 /blog/page/2/ 必须工作

尽早构建一个 URL 审计脚本:

# 使用 WP-CLI 导出所有 WordPress URL
wp post list --post_type=post,page --post_status=publish \
  --fields=ID,post_name,post_type,guid --format=csv > urls.csv

# 也从任何 SEO 插件获取重定向
wp db query "SELECT * FROM wp_redirection_items" --format=csv > redirects.csv

然后针对你的 Next.js 构建验证它们:

// validate-urls.mjs
import { readFileSync } from 'fs';
import { parse } from 'csv-parse/sync';

const urls = parse(readFileSync('./urls.csv'), { columns: true });
const NEXT_BASE = 'https://staging.yoursite.com';

for (const row of urls) {
  const res = await fetch(`${NEXT_BASE}/${row.post_name}/`, {
    redirect: 'manual'
  });
  
  if (res.status >= 400) {
    console.error(`BROKEN: /${row.post_name}/ → ${res.status}`);
  }
}

性能基线

在切换之前,你的 Next.js 网站至少需要与 WordPress 一样快。听起来很明显,但我见过团队对新堆栈如此兴奋,以至于他们忘记了进行基准测试。

使用 CrUX 数据或 Lighthouse CI 从你的 WordPress 网站捕获核心网络指标,然后匹配或超过它们。如果你的 WordPress 网站的 LCP 为 2.1s,而你的 Next.js 网站为 3.4s,你还没有准备好。

第三阶段:并行部署设置

现在我们进入使零停机成为可能的基础设施。

并行运行

两个系统同时运行,都在提供真实流量。但在任何给定时间只有一个是"主要的"。

对于 Vercel 上的 Next.js(我们最常见的设置),你将 Next.js 应用部署到自定义域,如 next.yoursite.com 或使用 Vercel 的预览 URL。你的 WordPress 网站继续在 yoursite.com 上运行。

# 如果你使用 Nginx 作为反向代理
# 这是并行运行配置

upstream wordpress {
    server wordpress-origin.internal:80;
}

upstream nextjs {
    server next-yoursite.vercel.app:443;
}

server {
    listen 443 ssl;
    server_name yoursite.com;

    # 在并行运行期间:镜像流量到两个后端,
    # 但仅从 WordPress 提供响应
    location / {
        mirror /mirror;
        proxy_pass http://wordpress;
    }

    location = /mirror {
        internal;
        proxy_pass https://nextjs$request_uri;
    }
}

这个镜像配置将每个请求发送到两个后端,但只返回 WordPress 响应。你的 Next.js 应用被真实流量击中,而用户看不到它。检查你的 Next.js 日志是否有错误、404 和缓慢响应。

合成监控

设置不断验证两个系统返回等效内容的监控:

// canary-check.mjs — 每 5 分钟通过 cron 运行
const CRITICAL_URLS = [
  '/',
  '/blog/',
  '/about/',
  '/contact/',
  '/blog/most-popular-post/',
];

for (const path of CRITICAL_URLS) {
  const [wpRes, nextRes] = await Promise.all([
    fetch(`https://yoursite.com${path}`),
    fetch(`https://next.yoursite.com${path}`),
  ]);

  if (wpRes.status !== nextRes.status) {
    alert(`Status mismatch on ${path}: WP=${wpRes.status} Next=${nextRes.status}`);
  }

  // 比较标题标签作为内容奇偶性检查
  const wpTitle = (await wpRes.text()).match(/<title>(.*?)<\/title>/)?.[1];
  const nextTitle = (await nextRes.text()).match(/<title>(.*?)<\/title>/)?.[1];

  if (wpTitle !== nextTitle) {
    alert(`Title mismatch on ${path}`);
  }
}

运行此程序至少 1 周,警报为零,然后再进行切换。

第四阶段:蓝绿部署配置

蓝绿部署意味着有两个相同的生产环境——蓝色(当前 WordPress)和绿色(新 Next.js)——并在它们之间原子性地切换。

使用 Cloudflare Workers 进行流量路由

这是我们首选的方法,因为它使你能够立即进行全局流量切换,而无需 DNS 传播延迟。

// Cloudflare Worker 用于蓝绿路由
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    
    // 从 KV 存储读取活跃环境
    const activeEnv = await env.CONFIG.get('active_environment') || 'blue';
    
    // 可选:基于百分比的金丝雀路由
    const canaryPercent = parseInt(await env.CONFIG.get('canary_percent') || '0');
    const useGreen = activeEnv === 'green' || 
                     (canaryPercent > 0 && Math.random() * 100 < canaryPercent);
    
    const origin = useGreen
      ? 'https://next-yoursite.vercel.app'
      : 'https://wp-origin.yoursite.com';
    
    const originUrl = new URL(url.pathname + url.search, origin);
    
    const response = await fetch(originUrl, {
      method: request.method,
      headers: {
        ...Object.fromEntries(request.headers),
        'Host': new URL(origin).hostname,
        'X-Forwarded-Host': url.hostname,
      },
      body: request.method !== 'GET' ? request.body : undefined,
    });
    
    const newResponse = new Response(response.body, response);
    newResponse.headers.set('X-Served-By', useGreen ? 'green-nextjs' : 'blue-wordpress');
    
    return newResponse;
  }
};

这个方法的美妙之处:从 WordPress 切换到 Next.js 是一次 KV 存储写入。无需 DNS 更改。无需传播。即时切换。

# 切换到绿色(Next.js)
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/active_environment" \
  -H "Authorization: Bearer ${CF_TOKEN}" \
  --data "green"

# 如果出现问题,回滚到蓝色(WordPress)
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/active_environment" \
  -H "Authorization: Bearer ${CF_TOKEN}" \
  --data "blue"

金丝雀路由

不要一次性将 100% 的流量翻转。从金丝雀开始:

  1. 1% 流量到 Next.js — 观察 1 小时是否有错误
  2. 10% 流量 — 监控 2 小时
  3. 50% 流量 — 监控 4 小时
  4. 100% 流量 — 监控 24 小时
  5. 停用 WordPress — 在 100% 运行 1 周后

第五阶段:DNS 切换策略

如果你无法使用 Cloudflare Workers(或类似的边缘路由解决方案),你需要在 DNS 级别处理切换。这更棘手,因为 TTL 传播。

切换前的 DNS 准备

至少在切换前 48 小时:

  1. 将你的 DNS TTL 降低到 60 秒(从典型的 3600 或 86400)
  2. 等待旧 TTL 过期
  3. 验证低 TTL 是否处于活跃状态:dig yoursite.com +short 应该显示 TTL ~60
# 检查当前 TTL
dig yoursite.com A +noall +answer
# 应该显示类似以下内容:
# yoursite.com.  60  IN  A  76.76.21.21

DNS 切换

TTL 为 60 秒,更新你的 DNS A/CNAME 记录意味着全球传播约 5-10 分钟(某些解析器忽略低 TTL,但大多数在 2025 年尊重它们)。

# 如果迁移到 Vercel
# 更新 CNAME 指向 cname.vercel-dns.com
# 或更新 A 记录指向 Vercel 的 IP 地址:76.76.21.21

陷阱:SSL 证书时序

这里有一个会让人犯错的地方。当你将 DNS 切换到 Vercel(或任何新主机)时,你的域名的 SSL 证书需要在新主机上配置,然后才能切换。否则,你会得到一个 HTTPS 无法工作的时间窗口。

在 Vercel 上,在 DNS 切换之前在项目设置中添加你的自定义域。Vercel 将尝试通过 HTTP-01 或 DNS-01 质询来配置证书。如果你使用 Cloudflare 代理的 DNS,你可能需要临时禁用代理(橙色云 → 灰色云)以使证书配置工作。

第六阶段:切换检查清单

这是我们在切换日使用的检查清单。打印出来。勾选每一项。

切换前(T-24 小时)

  • 所有内容已同步并在新 CMS 中验证
  • URL 奇偶性验证 100% 通过
  • SSL 证书在新主机上已配置
  • DNS TTL 确认为 60 秒
  • 回滚程序已记录和测试
  • 所有来自 WordPress SEO 插件的重定向已迁移到 next.config.js 或边缘中间件
  • robots.txtsitemap.xml 在 Next.js 上正确生成
  • 分析追踪在 Next.js 上验证(GA4、GTM 等)
  • 表单提交端到端测试
  • RSS 源已验证
  • 关键页面上的 OpenGraph 标签已验证
  • 404 页面已测试

切换(T-0)

  • 通知利益相关者:"迁移开始"
  • 设置金丝雀为 1% → 监控 30 分钟
  • 增加到 10% → 监控 1 小时
  • 检查 Google Search Console 是否有爬虫错误
  • 增加到 50% → 监控 2 小时
  • 增加到 100%
  • 向 Google Search Console 提交更新的网站地图
  • 验证 Googlebot 可以访问所有关键页面(使用 URL 检查工具)
  • 测试所有表单、电子商务流程、身份验证流程

切换后(T+24 小时)

  • 在 CrUX 仪表板中监控核心网络指标
  • 检查 Google Search Console 的覆盖问题
  • 验证所有分析数据流正确
  • WordPress 源站仍在运行(至少保留 2 周)
  • 使用 Screaming Frog 对新网站进行全站爬取

第七阶段:切换后监控和回滚

监控内容

在你的监控工具中设置仪表板(我们使用 Vercel Analytics、Datadog 和 Google Search Console 的组合)追踪:

  • 错误率:任何 5xx 响应?任何 4xx 上升?
  • 响应时间:P50、P95、P99 延迟
  • 核心网络指标:LCP、FID/INP、CLS
  • Search Console:爬虫统计、覆盖报告、索引状态
  • 业务指标:转化率、反弹率、每个会话的页面

回滚计划

你的回滚需要是单个命令。而不是 15 步流程。单个命令。

使用 Cloudflare Worker 方法:

# 单命令回滚
wrangler kv:key put --namespace-id=$NS_ID "active_environment" "blue"

使用基于 DNS 的切换:

# 通过 Cloudflare API 预先编写的 DNS 回滚
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{record_id}" \
  -H "Authorization: Bearer ${CF_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"content": "old-wordpress-ip-address"}'

在切换后至少 2 周内保持 WordPress 运行。不要做英雄。你关闭 WordPress 的那一刻就是你发现一个你忘记迁移的页面的时刻。

常见故障模式及其预防方法

在做了这么多次之后,以下是我最常看到的故障:

1. 忘记带有前端路由的 WordPress 插件 该联系表单插件创建 /wp-json/contact-form-7/ 端点。那个 WooCommerce 安装有 /my-account//cart/。映射每个插件的 URL 足迹。

2. 内容中硬编码的 wp-content URL 你的内容中的图像引用 /wp-content/uploads/。你需要重定向或重写规则将这些指向你的新资产 CDN。

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/wp-content/uploads/:path*',
        destination: 'https://cdn.yoursite.com/uploads/:path*',
        permanent: true,
      },
    ];
  },
};

3. 忘记 XML 网站地图 Google Search Console 指向 /sitemap.xml。你的 Next.js 应用需要生成一个。使用 next-sitemap 或在你的应用的路由处理程序中构建它。

4. 身份验证和会话问题 如果你的 WordPress 网站有登录用户,他们的 cookie 在新堆栈上不会工作。分别规划用户迁移。

5. 转换期间的 CDN 缓存污染 如果 Cloudflare 在缓存响应,切换到 Next.js 后你可能会提供陈旧的 WordPress 页面。在切换后立即清除 CDN 缓存。

# 切换后清除整个 Cloudflare 缓存
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer ${CF_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"purge_everything": true}'

如果你正在规划迁移并希望专家来处理繁重工作,请查看我们的定价页面直接联系我们。我们已经做过足够多次了,以至于我们的方案在所有正确的方面都经过了实战检验。

对于使用其他框架构建的网站,我们也进行基于 Astro 的迁移,对于不需要太多交互性的内容繁重网站可能更快。

常见问题

WordPress 到 Next.js 迁移通常需要多长时间? 对于中等复杂度的网站(100-500 页面、自定义文章类型、一些动态功能),计划端到端需要 8-12 周。仅内容迁移和并行运行阶段就需要 3-4 周。实际切换,如果你已经做好了准备工作,大约需要 4 小时的主动工作。不要让任何人告诉你这可以在一个周末完成。

我可以使用 WordPress 作为无头 CMS 而不是迁移内容吗? 绝对可以,对于某些团队这是正确的选择。你将 WordPress 作为你的 CMS,使用 REST API 或 WPGraphQL 向 Next.js 提供内容,并仅迁移前端。这大大缩短了迁移时间表,因为你跳过了内容迁移阶段。权衡是你仍在维护一个 WordPress 安装,带有所有的更新和安全开销。

迁移期间我的 SEO 排名会发生什么? 通过适当的零停机迁移,你的排名应该保持稳定。谷歌的 John Mueller 已经确认,如果内容、URL 和技术 SEO 元素保持等效,更改你的 CMS 不应该影响排名。最大的风险是损坏的 URL(导致 404)、改变的内部链接结构以及页面速度下降。我们的方案特别解决了这三个问题。

如何在 Next.js 中处理 WordPress 表单? 你有几种选择:使用 Formspree 或 Basin 等表单服务,在 Next.js 中构建直接处理提交的 API 路由,或使用你的无头 CMS 的表单功能(Sanity 没有原生表单,但 Payload CMS 有)。对于具有条件逻辑的复杂表单,我们通常构建自定义 API 路由并在前端使用 React Hook Form。

我应该为 Next.js 部署使用 Vercel、Netlify 还是自托管? 对于大多数团队,Vercel 是 Next.js 的最简单选择。它由同一个团队构建,ISR、中间件和图像优化等功能在那里工作效果最好。Vercel 的 Pro 计划每个用户每月 20 美元,涵盖大多数生产需求。如果你有特定的合规要求或需要保留在 AWS 上,你可以使用 standalone 输出模式自托管。Netlify 也适用,但在历史上对 Next.js 功能支持的滞后性。

蓝绿部署和金丝雀部署有什么区别? 蓝绿是二进制的:所有流量要么去旧系统(蓝色),要么去新系统(绿色)。金丝雀部署逐渐将一定百分比的流量从旧系统转移到新系统。在实践中,我们结合了两者。我们设置蓝绿基础设施(两个完整环境),但在实际切换期间使用金丝雀风格的基于百分比的路由。这给了你逐步部署的安全性和仅管理两个环境的简单性。

如何将 WordPress 重定向迁移到 Next.js? 从你使用的任何 WordPress 插件(重定向、Yoast、RankMath)导出你的重定向。将它们转换为 next.config.js 中的 Next.js 重定向格式。对于有数百个重定向的网站,改为使用中间件——它性能更高,可以处理模式匹配。请注意 next.config.js 重定向在 Vercel 上有大约 1,000 个条目的实际限制,之后构建时间会受影响。

如果切换后出现问题,我可以回滚到 WordPress 吗? 可以,这是不可协商的。在切换后至少 2 周内保持你的 WordPress 实例运行。使用本文中描述的 Cloudflare Worker 方法,回滚是单个 API 调用,在几秒内全局生效。使用基于 DNS 的切换,回滚需要 1-10 分钟,具体取决于 TTL 传播。在你确信新系统稳定之前,永远不要停用旧系统。