WordPress 轉換至 Next.js:完整技術指南
TL;DR
從 WordPress 遷移到 Next.js(2026 年)帶來可衡量的效能提升:平均 TTFB 從 1,200ms 降至 85ms、網頁大小從 3.2 MB 縮至 620 KB、Lighthouse 分數從 42 躍升至 94。遷移流程包括透過 WP REST API 將內容匯出至 Supabase 或 Payload CMS、將媒體遷移至物件儲存、為所有 URL 對應 301 重新導向、使用 Next.js Metadata API 保留 Yoast/RankMath SEO 資料,以及以 Server Actions 替換 Gravity Forms 等外掛。對於 WooCommerce 網站,Stripe 替代整個商務堆疊。對於有 100-500 頁的典型網站,預計需要 4-8 週,最大時間投資在重新導向測試和樣板重建。
這不是一個倉促的想法。我從事 WordPress 開發已超過 12 年。我已交付過代理商網站、會員平台、月收入達六位數的 WooCommerce 商店,以及無數自訂文章類型。我也已將生產網站遷移到 Next.js + Supabase。以下是每一項技術細節 -- 什麼能順利對應、什麼不能,以及需要規劃什麼。
我不會假裝 WordPress 不好。它不是。它驅動了網路的 43%,是有原因的。但對於某些專案 -- 效能是業務指標的網站、安全表面積很重要的網站、你想擁有部署管道的網站 -- Next.js 是更好的工具。但遷移呢?如果你沒有好好規劃,它會是一場地獄。
本指南涵蓋了我使用的確切流程,包含真實代碼、真實陷阱和對你將獲得什麼和失去什麼的誠實評估。
目錄
- 內容遷移:WP REST API 至 Supabase 或 Payload CMS
- 媒體遷移:從 wp-content 至 Supabase 儲存
- URL 結構:對應每個舊 URL
- SEO 遷移:Yoast 和 RankMath 資料至 Next.js Metadata
- 表單:Gravity Forms 至 Server Actions
- WooCommerce 至 Stripe
- 遷移後監控
- 何時保留在 WordPress
- 效能基準:遷移前後
- 常見問題

內容遷移:WP REST API 至 Supabase 或 Payload CMS
每次 WordPress 遷移都從這裡開始。你有文章、頁面、自訂文章類型、ACF 欄位、分類法 -- 多年的內容需要安全地放到某個地方。
內容存放地點有兩個很好的選項:
- Supabase -- 如果你想要一個完全控制的資料庫,具備列級安全和開箱即用的 REST/GraphQL API
- Payload CMS -- 如果你的客戶需要一個在 WordPress 之後感覺很熟悉的視覺編輯體驗
對於我們的無頭 CMS 開發專案,我們基於個案評估。當編輯需要自助服務時,Payload 更勝一籌。當開發人員是主要內容管理人員或當你需要資料用途超過網站時,Supabase 更勝一籌。
從 WordPress 遷移到 Next.js 時應該保留哪些內容結構?
在遷移期間保留文章中繼資料、分類法、自訂欄位和 URL slugs。你的 WordPress 文章包含多年的結構化資料:類別、標籤、ACF 欄位、精選圖像和發布日期。透過 WP REST API 和 _embed 參數匯出所有資料,以在單一請求中取得媒體 URL。儲存內容的 HTML 和 Markdown 版本 -- HTML 作為後備、Markdown 用於 MDX 渲染。將自訂文章類型對應至新系統中的等效資料庫表或 CMS 集合。
匯出指令碼
以下是我用來從 WP REST API 提取內容、清理內容並將其插入 Supabase 的 Node.js 指令碼。這處理文章,但你可以為頁面和 CPT 複製此模式(只需變更端點)。
import { createClient } from '@supabase/supabase-js';
import TurndownService from 'turndown';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
);
const turndown = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
});
const WP_API = 'https://yoursite.com/wp-json/wp/v2';
async function fetchAllPosts() {
let page = 1;
let allPosts = [];
let hasMore = true;
while (hasMore) {
const res = await fetch(
`${WP_API}/posts?per_page=100&page=${page}&_embed`
);
if (!res.ok) break;
const posts = await res.json();
allPosts = allPosts.concat(posts);
const totalPages = parseInt(res.headers.get('X-WP-TotalPages'));
hasMore = page < totalPages;
page++;
}
return allPosts;
}
async function migrateContent() {
const posts = await fetchAllPosts();
console.log(`Fetched ${posts.length} posts from WordPress`);
const transformed = posts.map((post) => ({
wp_id: post.id,
title: post.title.rendered,
slug: post.slug,
content_html: post.content.rendered,
content_markdown: turndown.turndown(post.content.rendered),
excerpt: post.excerpt.rendered.replace(/<[^>]*>/g, '').trim(),
published_at: post.date,
status: post.status,
featured_image:
post._embedded?.['wp:featuredmedia']?.[0]?.source_url || null,
categories:
post._embedded?.['wp:term']?.[0]?.map((t) => t.name) || [],
tags:
post._embedded?.['wp:term']?.[1]?.map((t) => t.name) || [],
}));
const { data, error } = await supabase
.from('posts')
.upsert(transformed, { onConflict: 'wp_id' });
if (error) {
console.error('Migration failed:', error);
} else {
console.log(`Migrated ${transformed.length} posts to Supabase`);
}
}
migrateContent();
我學到幾個困難的教訓:
- 總是在 WP REST API 呼叫中使用
_embed。沒有它,你會得到媒體 ID 而不是 URL,這意味著需要 N+1 個請求來解析精選圖像。 - Turndown 將 HTML 轉換為 Markdown -- 如果你計劃稍後使用 MDX 進行渲染,這至關重要。也保留原始 HTML,作為後備。
- 短代碼無法倖免。 WordPress 透過 REST API 渲染某些短代碼,但許多(特別是來自 WPBakery 或 Elementor 等外掛的)會以原始括號文字的形式出現。你需要一個短代碼到元件的對應策略。我保存一個試算表。
- ACF / 自訂欄位:如果你使用 ACF,你需要啟用 ACF to REST API 外掛,然後自訂欄位會出現在每個文章物件的
acf屬性中。
Supabase 表結構
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
wp_id INTEGER UNIQUE,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
content_html TEXT,
content_markdown TEXT,
excerpt TEXT,
published_at TIMESTAMPTZ,
status TEXT DEFAULT 'publish',
featured_image TEXT,
categories TEXT[],
tags TEXT[],
seo_title TEXT,
seo_description TEXT,
og_image TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
注意我包含了 seo_title、seo_description 和 og_image 欄位。你在下面的 SEO 遷移部分需要這些。
媒體遷移:從 wp-content 至 Supabase 儲存
這是大多數指南掩飾的部分,也是花費最長時間的部分。一個 12 年舊的 WordPress 網站可以輕鬆在 wp-content/uploads/ 中擁有 10,000 多個檔案。
方法:
- 下載整個
wp-content/uploads/目錄 - 上傳至 Supabase Storage(或 Cloudflare R2,或 S3)
- 重寫內容中的每個媒體 URL
我如何將 WordPress 媒體檔案遷移至現代物件儲存?
下載整個 wp-content/uploads/ 目錄,上傳至 Supabase Storage 或 Cloudflare R2,然後重寫內容中的所有媒體 URL。使用指令碼從 WordPress 內容中提取每個圖像 URL、上傳至物件儲存同時保留目錄結構(2024/03/image.jpg),然後執行第二步驟以用新儲存 URL 取代舊 URL。為直接連結到舊圖像 URL 的外部網站設定萬用字元重新導向。
下載和上傳指令碼
import { createClient } from '@supabase/supabase-js';
import fs from 'fs';
import path from 'path';
import fetch from 'node-fetch';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
);
const BUCKET = 'media';
async function migrateMedia(posts) {
const urlRegex =
/https?:\/\/yoursite\.com\/wp-content\/uploads\/[^\s"')]+/g;
for (const post of posts) {
const urls = post.content_html.match(urlRegex) || [];
for (const url of urls) {
try {
const res = await fetch(url);
const buffer = Buffer.from(await res.arrayBuffer());
// Preserve directory structure: 2024/03/image.jpg
const storagePath = url.replace(
/https?:\/\/yoursite\.com\/wp-content\/uploads\//,
''
);
const { error } = await supabase.storage
.from(BUCKET)
.upload(storagePath, buffer, {
contentType: res.headers.get('content-type'),
upsert: true,
});
if (error) console.error(`Failed: ${storagePath}`, error);
else console.log(`Uploaded: ${storagePath}`);
} catch (e) {
console.error(`Skipped: ${url}`, e.message);
}
}
}
}
async function rewriteUrls() {
const { data: posts } = await supabase.from('posts').select('*');
const supabaseBase = `${process.env.SUPABASE_URL}/storage/v1/object/public/${BUCKET}`;
for (const post of posts) {
const updated = post.content_html.replace(
/https?:\/\/yoursite\.com\/wp-content\/uploads\//g,
`${supabaseBase}/`
);
await supabase
.from('posts')
.update({
content_html: updated,
content_markdown: turndown.turndown(updated),
})
.eq('id', post.id);
}
}
圖像效能獲益
這是遷移真正發揮作用的地方。WordPress 提供原始上傳 -- 通常是從人們的 DSLR 上傳的 3000×2000px PNG。即使使用 ShortPixel 等外掛,你仍然透過 PHP 提供圖像。
Next.js <Image> 元件搭配 next/image 進行自動格式協商(WebP/AVIF)、回應式大小調整和延遲加載。我們上次遷移的數字:
| 指標 | WordPress | Next.js + Image 元件 |
|---|---|---|
| 平均頁面圖像大小 | 2.1 MB | 380 KB |
| 圖像請求 | 每頁 12 個 | 每頁 6 個(延遲加載) |
| 格式 | JPEG/PNG | WebP(支援的地方為 AVIF) |
| 累積版面配置偏移 | 0.18 | 0.02 |
那不是打字錯誤。平均圖像負載從 2.1 MB 降至 380 KB。 而這是在沒有重新上傳最佳化檔案的情況下 -- 只是讓 next/image 發揮作用。
import Image from 'next/image';
export function PostImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={800}
height={450}
sizes="(max-width: 768px) 100vw, 800px"
quality={80}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..." // generate at build time
/>
);
}
URL 結構:對應每個舊 URL
這是遷移失敗的地方。一個遺漏的重新導向意味著一個頁面出現 404,而那個頁面已被 Google 索引多年。我以對待資料庫遷移一樣的嚴肅性來對待 URL 對應 -- 測試它、驗證它、然後再驗證一次。
在 WordPress 遷移期間處理 URL 重新導向的正確方法是什麼?
從 WordPress 匯出每個已發布的 URL,與 Google Search Console 索引的 URL 交叉參考,然後為所有 URL 實施 301 重新導向。查詢 wp_posts 表以取得所有已發布的 URL,匯出 GSC 的索引 URL,並建立重新導向對應。對於少於 50 個 URL,使用 next.config.js 重新導向;對於 50-1,024 個 URL,使用 JSON 檔案;對於超過 Vercel 的 1,024 個重新導向限制的網站,使用中介軟體。包含類別頁面、分頁和 wp-content/uploads 路徑的萬用字元重新導向。
對應流程
首先,從 WordPress 匯出每個 URL。我直接從資料庫提取:
SELECT
CONCAT('/', post_name, '/') AS old_url,
post_type,
post_status
FROM wp_posts
WHERE post_status = 'publish'
AND post_type IN ('post', 'page', 'product')
ORDER BY post_type, post_name;
然後與 Google Search Console 的索引 URL 交叉參考。GSC 通常會顯示資料庫中不再存在的 URL -- 舊類別頁面、分頁 URL、附件頁面。你需要所有 URL 的重新導向。
next.config.js 重新導向
對於少於 50 個重新導向的網站,內聯它們:
// next.config.js
module.exports = {
async redirects() {
return [
{
source: '/2019/03/old-post-slug/',
destination: '/blog/old-post-slug',
permanent: true,
},
{
source: '/category/:slug',
destination: '/blog/category/:slug',
permanent: true,
},
{
source: '/product/:slug',
destination: '/shop/:slug',
permanent: true,
},
];
},
};
對於 200 多個重新導向:使用 JSON 檔案
一旦超過幾百個,維護內聯重新導向就變得令人沮喪。我使用 JSON 檔案:
// redirects.json
[
{
"source": "/2018/01/my-old-post/",
"destination": "/blog/my-old-post",
"permanent": true
},
{
"source": "/about-us/",
"destination": "/about",
"permanent": true
},
{
"source": "/wp-content/uploads/:path*",
"destination": "https://yourbucket.supabase.co/storage/v1/object/public/media/:path*",
"permanent": true
}
]
// next.config.js
const redirectsList = require('./redirects.json');
module.exports = {
async redirects() {
return redirectsList;
},
};
wp-content/uploads 的萬用字元重新導向至關重要。會有外部網站直接連結到你的圖像。不要失去那些反向連結。
重要:Vercel 在 next.config.js 中有 1,024 個重新導向的限制。對於超過 1,024 個重新導向的網站,使用中介軟體:
// middleware.ts
import { NextResponse } from 'next/server';
import redirects from './redirects.json';
const redirectMap = new Map(
redirects.map((r) => [r.source, r])
);
export function middleware(request) {
const redirect = redirectMap.get(request.nextUrl.pathname);
if (redirect) {
return NextResponse.redirect(
new URL(redirect.destination, request.url),
redirect.permanent ? 308 : 307
);
}
}

SEO 遷移:Yoast 和 RankMath 資料至 Next.js Metadata
如果你一直在使用 Yoast 或 RankMath,你在 wp_postmeta 表中儲存了多年的自訂中繼標題、描述和 Open Graph 資料。不要遺失它。
遷移到 Next.js 時我如何保留 Yoast SEO 資料?
從 wp_postmeta 匯出 Yoast 中繼標題、描述和 Open Graph 圖像,將其儲存在新資料庫中,然後使用 Next.js Metadata API 進行渲染。查詢 wp_postmeta 以取得 _yoast_wpseo_title、_yoast_wpseo_metadesc 和 _yoast_wpseo_opengraph-image 欄位。將此資料匯入至文章表中的專用 SEO 欄位。在 Next.js App Router 中使用 generateMetadata 以將此資料渲染為適當的中繼標籤和 Open Graph 標記。
匯出 SEO 資料
SELECT
p.post_name AS slug,
MAX(CASE WHEN pm.meta_key = '_yoast_wpseo_title' THEN pm.meta_value END) AS seo_title,
MAX(CASE WHEN pm.meta_key = '_yoast_wpseo_metadesc' THEN pm.meta_value END) AS seo_description,
MAX(CASE WHEN pm.meta_key = '_yoast_wpseo_opengraph-image' THEN pm.meta_value END) AS og_image
FROM wp_posts p
JOIN wp_postmeta pm ON p.ID = pm.post_id
WHERE p.post_status = 'publish'
GROUP BY p.ID, p.post_name;
對於 RankMath,交換中繼鍵:rank_math_title、rank_math_description、rank_math_facebook_image。
將此資料匯入至 Supabase posts 表中我們之前定義的 SEO 欄位。
Next.js Metadata API
使用 App Router,metadata 是一級公民:
// app/blog/[slug]/page.tsx
import { supabase } from '@/lib/supabase';
import { Metadata } from 'next';
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const { data: post } = await supabase
.from('posts')
.select('title, seo_title, seo_description, og_image')
.eq('slug', params.slug)
.single();
return {
title: post.seo_title || post.title,
description: post.seo_description,
openGraph: {
title: post.seo_title || post.title,
description: post.seo_description,
images: post.og_image ? [{ url: post.og_image }] : [],
},
};
}
作為 JSON-LD 伺服器元件的結構化標記
WordPress 外掛會自動產生結構化標記。在 Next.js 中,你自己建立它 -- 這實際上給了你更多的控制:
// components/ArticleSchema.tsx
export function ArticleSchema({ post }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
datePublished: post.published_at,
dateModified: post.updated_at || post.published_at,
author: {
'@type': 'Organization',
name: 'Your Company',
},
image: post.og_image,
description: post.seo_description,
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
動態 Sitemap
// app/sitemap.ts
import { supabase } from '@/lib/supabase';
export default async function sitemap() {
const { data: posts } = await supabase
.from('posts')
.select('slug, published_at')
.eq('status', 'publish');
return posts.map((post) => ({
url: `https://yoursite.com/blog/${post.slug}`,
lastModified: post.published_at,
changeFrequency: 'monthly',
priority: 0.8,
}));
}
這在構建時對靜態網站產生,或按需對動態網站產生。沒有外掛、沒有 XML 樣板檔案、沒有快取問題。
表單:Gravity Forms 至 Server Actions
Gravity Forms 是有史以來最好的 WordPress 外掛之一。它每年也要 $259 用於 Elite 授權,而每個表單加載超過 200KB 的 JavaScript。
以下是替代方案。每個表單約 20 行代碼。
匯出現有條目
首先,從 WordPress 管理員以 CSV 形式匯出 Gravity Forms 條目。如果需要,將其儲存在 Supabase 中以保存歷史記錄。
Server Action 聯繫表單
// app/contact/page.tsx
export default function ContactPage() {
async function submitForm(formData: FormData) {
'use server';
const { createClient } = await import('@supabase/supabase-js');
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!
);
const entry = {
name: formData.get('name') as string,
email: formData.get('email') as string,
message: formData.get('message') as string,
submitted_at: new Date().toISOString(),
};
// Validate
if (!entry.name || !entry.email || !entry.message) {
throw new Error('All fields required');
}
await supabase.from('form_submissions').insert(entry);
// Optional: send notification email via Resend
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'forms@yoursite.com',
to: 'team@yoursite.com',
subject: `New contact: ${entry.name}`,
html: `<p>${entry.message}</p><p>From: ${entry.email}</p>`,
}),
});
}
return (
<form action={submitForm}>
<input name="name" type="text" required placeholder="Name" />
<input name="email" type="email" required placeholder="Email" />
<textarea name="message" required placeholder="Message" />
<button type="submit">Send</button>
</form>
);
}
沒有外掛。表單本身沒有 JavaScript 負載(它是一個帶有伺服器動作的原生 HTML 表單)。漸進式增強 -- 它在沒有啟用 JavaScript 的情況下也能工作。
對於更複雜的表單(多步驟、檔案上傳、條件欄位),我們在客戶端使用 React Hook Form 搭配相同的伺服器動作模式。關鍵見解:當你有資料庫和 API 時,你不需要表單外掛。
WooCommerce 至 Stripe
這是任何 WordPress 遷移中最困難的部分。WooCommerce 不僅僅是外掛 -- 它是一個商務平台,具有產品、變體、庫存、訂單、訂閱、優惠券、稅務和運輸規則。你不是在遷移一項功能。你是在替換一個平台。
我如何將 WooCommerce 產品遷移到 Stripe?
透過 WooCommerce REST API 或 CSV 匯出產品,然後使用 Stripe Products API 在 Stripe 中建立匹配的產品及價格。對於少於 500 個產品的網站,使用其 API 直接推送至 Stripe:建立具有名稱、描述和圖像的產品,然後建立連結至該產品的價格物件。在 Stripe 中繼資料中儲存 WooCommerce 產品 ID 以供參考。使用 Stripe Checkout Sessions 進行支付處理,使用 Webhook 在資料庫中追蹤訂單。
產品遷移
從 WooCommerce 透過 CSV 或 REST API 匯出產品。你有兩個目的地選項:
| 方法 | 最適合 | 折衷 |
|---|---|---|
| Supabase 產品表 | 自訂店面、複雜篩選 | 你管理庫存邏輯 |
| Stripe Products API | 簡單目錄、訂閱業務 | Stripe 管理定價、你管理顯示 |
對於大多數少於 500 個產品的網站,我直接推送至 Stripe Products:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
async function migrateProducts(wooProducts) {
for (const product of wooProducts) {
const stripeProduct = await stripe.products.create({
name: product.name,
description: product.short_description,
images: [product.images[0]?.src].filter(Boolean),
metadata: {
woo_id: String(product.id),
slug: product.slug,
sku: product.sku,
},
});
await stripe.prices.create({
product: stripeProduct.id,
unit_amount: Math.round(parseFloat(product.price) * 100),
currency: 'usd',
});
console.log(`Created: ${product.name} → ${stripeProduct.id}`);
}
}
搭配 Stripe Checkout Sessions 的結帳
// app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId, quantity = 1 } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity }],
success_url: `${process.env.NEXT_PUBLIC_URL}/order/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/shop`,
});
return NextResponse.json({ url: session.url });
}
訂閱
如果你在 WooCommerce Subscriptions($239/年)上,轉換至 Stripe Billing。將 mode: 'payment' 變更為 mode: 'subscription' 並確保你的價格已設定 recurring。就這樣。Stripe 處理試用期、按比例分配和催款。
透過 Webhook 的訂單追蹤
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import Stripe from 'stripe';
import { supabase } from '@/lib/supabase';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const body = await request.text();
const sig = headers().get('stripe-signature')!;
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session;
await supabase.from('orders').insert({
stripe_session_id: session.id,
customer_email: session.customer_details?.email,
amount_total: session.amount_total,
status: 'completed',
});
}
return new Response('OK', { status: 200 });
}
Stripe 的交易費用是 2.9% + 每筆交易 $0.30。將其與 WordPress 比較,你需要支付託管費用($30-100/月用於託管 WP)、Subscriptions 外掛($239/年)、支付閘道外掛和可能的其他幾個附加元件。數學計算會很快產生作用。
對於複雜的商務遷移,我們以我們的Next.js 開發服務的一部分提供此功能 -- 這是我們最常收到的請求之一。
遷移後監控
啟動不是結束。遷移後的前兩週至關重要。
Google Search Console
- 立即提交新 sitemap
- 使用 URL 檢查工具要求索引你的前 20 個頁面
- 在第一週每天監控 Coverage 報告 -- 留意 404 激增
- 檢查「頁面索引」報告中卡在「已發現 -- 目前未索引」的任何頁面
分析比較
設定一個儀表板來比較每週的:
- 總工作階段數
- 特別是有機搜尋流量
- 按頁面的跳出率
- 轉換率(表單提交、購買)
第一週的小流量下跌是正常的。如果到第三週還沒有恢復,出了問題,可能是重新導向或索引的問題。
Lighthouse 審計
對每個主要樣板(首頁、部落格文章、產品頁面、聯繫頁面)執行 Lighthouse。目標:
- 效能:90+
- 無障礙:95+
- 最佳實踐:95+
- SEO:100
在我們上次遷移 -- 一個 400 頁內容網站 -- 上,我們將平均 Lighthouse 效能分數從 WordPress 上的 38 提高到 Vercel 上的 96。這不是精心挑選的。這是平均值。
何時保留在 WordPress
這是我失去部分讀者的部分,但它是本指南中最重要的部分。
不要遷移如果:
- 你有一個簡單的部落格或宣傳冊式網站,少於 20 頁
- 你的團隊是非技術人員,依賴 WordPress 管理員進行日常更新
- 你的 Lighthouse 分數已經是 70+,而你沒有效能關鍵業務需求
- 你沒有安全問題,你的託管是穩定的
- 你的外掛總成本低於 $200/年
- 你沒有開發人員(或預算)來維護 Next.js 網站
WordPress 搭配良好的主機(Cloudways、Kinsta)、穩實的主題和最少的外掛是可以的。實際上,它不止可以 -- 它經過戰鬥測試、文件齊全並被數百萬開發人員理解。
遷移在以下情況下是有意義的:
- 效能直接與收入掛鉤(電子商務、SaaS 行銷網站)
- 你在託管和安全外掛上花費 $500+/月
- 你的開發團隊已經在編寫 React
- 你需要一個具有預覽構建、中試環境和回滾的部署管道
- 安全表面積是一個真實的問題(政府、醫療保健、財務)
我這樣說是因為信任比銷售更重要。如果你不確定遷移是否值得,與我們聯繫,我們會給你一個誠實的評估。
效能基準:遷移前後
來自我們 2024-2025 年最後五次遷移的數據:
| 指標 | WordPress(平均) | Next.js(平均) | 變化 |
|---|---|---|---|
| TTFB | 1,200ms | 85ms | 快 14 倍 |
| LCP | 3.8s | 0.9s | 快 4.2 倍 |
| 總頁面大小 | 3.2 MB | 620 KB | 輕 5 倍 |
| 每頁請求數 | 47 | 11 | 減少 77% |
| Lighthouse 效能 | 42 | 94 | +52 分 |
| 月託管成本 | $75 | $20(Vercel Pro) | 節省 73% |
| Core Web Vitals 通過率 | 31% 的頁面 | 100% 的頁面 | ✓ |
這些是生產網站的真實數字。WordPress 網站運行在託管託管(WP Engine 和 Kinsta)、最佳化快取和圖像最佳化外掛上。它們沒有被忽視 -- 它們是維護的網站,只是達到了 WordPress 能提供的上限。
如果你有興趣了解現代框架的可能性,請檢查我們的 Astro 開發能力 -- 對於內容繁重但互動性最小的網站,Astro 甚至可以提供比 Next.js 更小的負載。
常見問題
WordPress 到 Next.js 的遷移需要多長時間?
對於有 100-500 頁的典型網站,預計 4-8 週的開發時間。簡單的宣傳冊網站可以在 2-3 週內完成。具有數千種產品的複雜 WooCommerce 商店可能需要 10-12 週。內容遷移本身很快 -- 重建前端樣板和測試每個重新導向花費時間。
從 WordPress 遷移到 Next.js 時會失去 SEO 排名嗎?
如果你正確處理重新導向和中繼資料,就不會。關鍵部分是:為每個舊 URL 設定 301 重新導向、遷移所有 Yoast/RankMath 中繼標題和描述、保留 sitemap 結構,以及立即向 Google Search Console 提交新 sitemap。我們看過網站在 1-2 週內恢復到遷移前的流量,由於改進的 Core Web Vitals,到第三個月會看到顯著的有機增長。
我能使用 WordPress 作為 Next.js 的無頭 CMS 嗎?
能,這很流行。你保留 WordPress 作為內容後端,使用 WP REST API 或 WPGraphQL,以及 Next.js 作為前端。這保留了熟悉的編輯體驗,同時獲得 Next.js 效能。缺點是你仍然在維護一個具有安全和更新開銷的 WordPress 安裝。我們通常對新專案推薦 Payload CMS 或 Sanity,除非團隊對 WordPress 工作流程深度投入。
從 WordPress 遷移到 Next.js 需要多少成本?
DIY 開發人員時間:工具中免費,但預算 80-200 小時開發時間。代理成本:通常 $10,000-$50,000,取決於網站複雜性、頁面數量、電子商務功能和自訂功能。檢查我們的定價頁面以了解我們套件的具體信息。ROI 通常來自減少的託管成本(每月節省 $50-100)、消除的外掛授權費和提高轉換率,因為更好的效能。
遷移後我的 WordPress 外掛會發生什麼?
每個外掛都需要 Next.js 等效物。Contact Form 7 或 Gravity Forms 變成 Server Action。Yoast SEO 變成 Next.js Metadata API。WooCommerce 變成 Stripe。Google Analytics 保持不變(只需移動追蹤程式碼片段)。某些外掛(如 Wordfence)變得不必要,因為沒有 WordPress 可以攻擊。在開始前創建完整的外掛清單 -- 任何沒有明確替換策略的外掛都是風險。
我應該從 WordPress 遷移到 Next.js 或 Astro?
取決於你的互動需求。Next.js 更適合具有動態功能的網站 -- 使用者驗證、電子商務、儀表板、即時資料。Astro 更適合內容繁重的網站,主要是靜態的 -- 部落格、文件、行銷網站。Astro 預設情況下不運送任何 JavaScript,這意味著更小的頁面大小。我們同時使用兩者 -- 請參閱我們的 Astro 開發和 Next.js 開發頁面以了解詳情。
我能將 WooCommerce 訂閱遷移到 Stripe 嗎?
能,但需要謹慎處理活躍訂閱者。你需要在 Stripe 中建立客戶和訂閱,然後將帳單變更傳達給客戶。Stripe Billing 處理試用期、按比例分配、失敗的支付重試邏輯和取消流程。遷移本身是一次性指令碼,但針對真實訂閱情況測試它是花費時間的地方。如果你有超過 100 個活躍訂閱者,請預算額外時間。
遷移到 Next.js 後,最佳託管是什麼?
Vercel 是預設選擇 -- 它由製作 Next.js 的團隊建立,免費等級處理大多數行銷網站。Vercel Pro 是 $20/月用於團隊。替代選項包括 Netlify、Cloudflare Pages(邊緣效能優異)和在 VPS 上使用 Docker 自託管(如果你想完全控制)。所有這些的成本都明顯低於託管 WordPress 託管,用於同等流量等級。