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 是更好的工具。但遷移呢?如果你沒有好好規劃,它會是一場地獄。

本指南涵蓋了我使用的確切流程,包含真實代碼、真實陷阱和對你將獲得什麼和失去什麼的誠實評估。

目錄

WordPress to Next.js Migration: A Complete Technical Guide

內容遷移: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_titleseo_descriptionog_image 欄位。你在下面的 SEO 遷移部分需要這些。

媒體遷移:從 wp-content 至 Supabase 儲存

這是大多數指南掩飾的部分,也是花費最長時間的部分。一個 12 年舊的 WordPress 網站可以輕鬆在 wp-content/uploads/ 中擁有 10,000 多個檔案。

方法:

  1. 下載整個 wp-content/uploads/ 目錄
  2. 上傳至 Supabase Storage(或 Cloudflare R2,或 S3)
  3. 重寫內容中的每個媒體 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
    );
  }
}

WordPress to Next.js Migration: A Complete Technical Guide - architecture

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_titlerank_math_descriptionrank_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 託管,用於同等流量等級。