TL;DR

WordPressからNext.jsへの2026年の移行は、測定可能なパフォーマンス向上をもたらします。平均TTFBは1,200msから85msに低下し、ページウェイトは3.2MBから620KBに縮小し、Lighthouseスコアは42から94に上昇します。このプロセスには、WP REST APIを介したコンテンツのエクスポート(SupabaseまたはPayload CMSへ)、メディアのオブジェクトストレージへの移行、すべてのURLの301リダイレクトへのマッピング、YoastまたはRankMathのSEOデータをNext.js Metadata APIで保持、Gravity FormなどのプラグインをServer Actionsで置き換えることが含まれます。WooCommerceサイトの場合、Stripeがコマース全体を置き換えます。100~500ページの典型的なサイトには4~8週間を想定してください。最大の時間投資はリダイレクトテストとテンプレート再構築にあります。

これは一般的な意見ではありません。私は12年以上WordPressで開発してきました。エージェンシーサイト、メンバーシッププラットフォーム、月間6桁の収益を上げる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フィールド、タクソノミーがあります。年分のコンテンツを安全な場所に配置する必要があります。

コンテンツが行く先として、2つの堅実なオプションがあります。

  • Supabase -- 完全にコントロールできるデータベースが必要な場合。行レベルセキュリティと既製のREST/GraphQL APIがあります
  • Payload CMS -- クライアントがWordPressの後で馴染みのあるビジュアル編集体験を必要とする場合

ヘッドレスCMS開発プロジェクトでは、これをクライアントごとに評価します。編集者がセルフサービスを必要とする場合、Payloadが勝ちます。開発者がコンテンツの主要な管理者である場合、またはデータをWebサイト以上に使用する必要がある場合、Supabaseが勝ちます。

WordPressからNext.jsへの移行時に、どのコンテンツ構造を保持すべきですか?

移行中に投稿メタデータ、タクソノミー、カスタムフィールド、URLスラッグを保持します。WordPressの投稿には、年分の構造化データが含まれています。カテゴリ、タグ、ACFフィールド、アイキャッチ画像、公開日です。_embedパラメータを使用してWP REST API経由ですべてをエクスポートして、1つのリクエストでメディア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();

厳しい方法で学んだいくつかのこと:

  • 常に_embedをWP REST APIコールで使用してください。それなしに、メディア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_description、およびog_image列を含めていることに注意してください。下記のSEO移行セクションでそれらが必要になります。

メディア移行:wp-contentからSupabase Storageへ

これは、ほとんどのガイドが軽視する部分であり、最も時間がかかる部分です。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に置き換えるために2番目のパスを実行します。古い画像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());

        // ディレクトリ構造を保持: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 Component
平均ページ画像ウェイト 2.1 MB 380 KB
画像リクエスト ページあたり12 ページあたり6(遅延読み込み)
フォーマット JPEG/PNG WebP(サポートされている場合はAVIF)
累積レイアウトシフト 0.18 0.02

これはタイプミスではありません。平均画像ペイロードは2.1MBから380KBに低下しました。 これは、最適化されたファイルを再アップロードすることなく、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,..." // ビルド時に生成
    />
  );
}

URL構造:すべての古いURLのマッピング

ここが移行が死ぬ場所です。1つのリダイレクトを見逃すことは、Google によって長年インデックスされたページに対して404を意味します。私はURL マッピングをデータベース移行と同じ真摯さで扱います。テストしてください。検証してください。もう一度検証してください。

WordPressの移行中にURLリダイレクトを正しく処理する方法は何ですか?

WordPressから公開されているすべてのURLをエクスポートし、Google Search Consoleの索引付けURLと相互参照し、すべてのリダイレクトの301を実装します。wp_postsテーブルをクエリしてすべての公開URLを取得し、GSCの索引付けURLをエクスポートして、リダイレクトマップを作成します。50以下のURL には next.config.js リダイレクトを使用し、50~1,024のURLにはJSONファイルを使用し、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、添付ファイルページ。すべてのリダイレクトが必要です。

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,000以上のリダイレクトがあるサイトの場合、ミドルウェアを使用します:

// 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データがあります。それを失わないようにしてください。

WordPressから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列にインポートします。App RouterでNext.jsの 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

このデータを、前に定義したSEO列のSupabaseのpostsテーブルにインポートします。

Next.js Metadata API

App Routerを使用すると、メタデータはファーストクラスの市民です:

// 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) }}
    />
  );
}

ダイナミックサイトマップ

// 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プラグインの1つです。また、Eliteライセンスで年間259ドル、各フォームは200KB以上のJavaScriptを読み込みます。

ここに置き換えがあります。フォームあたり約20行のコードです。

既存のエントリをエクスポート

まず、WordPressアドミンからGravity FormsエントリをCSVとしてエクスポートします。必要に応じて、履歴レコードのためにそれらを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(),
    };

    // 検証
    if (!entry.name || !entry.email || !entry.message) {
      throw new Error('All fields required');
    }

    await supabase.from('form_submissions').insert(entry);

    // オプション: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で一致する製品を作成します。500以下の製品を持つサイトの場合、Stripe APIを直接使用してプッシュします。名前、説明、画像を持つ製品を作成してから、その製品にリンクされた価格オブジェクトを作成します。参照用にStripeメタデータにWooCommerce製品IDを保存します。決済処理にはStripe Checkout Sessionsを使用し、Webhooksを使用してデータベース内の注文を追跡します。

製品の移行

WooCommerceからCSVまたはREST APIを使用して製品をエクスポートします。目的地として2つのオプションがあります:

アプローチ 最適な使用場合 トレードオフ
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 は試用期間、按分配分、そしてダニング処理を処理します。

Webhooks経由の注文追跡

// 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ドルです。これを、ホスティング(月額30~100ドル)、Subscriptionsプラグイン(年間239ドル)、決済ゲートウェイプラグイン、そしておそらく他のいくつかのアドオンを支払っているWordPressと比較してください。数学はすぐに機能します。

複雑なコマース移行の場合、これをNext.js開発サービスの一部として提供しています。これは私たちが最も多く受け取るリクエストの1つです。

移行後のモニタリング

起動は終わりではありません。移行後の最初の2週間が重要です。

Google Search Console

  • 新しいサイトマップをすぐに送信します
  • URLインスペクションツールを使用して、上位20ページのインデックス作成を要求します
  • 最初の週、カバレッジレポートを毎日監視します。404sのスパイクを見てください
  • 「ページインデックス」レポートで、「検出されたものの、現在インデックスが作成されていない」状態で立ち往生しているページをチェックしてください

分析の比較

週ごとの比較ダッシュボードを設定します:

  • 総セッション数
  • 特に有機検索トラフィック
  • ページ別のバウンス率
  • コンバージョン率(フォーム送信、購入)

1週目の小さなトラフィック低下は正常です。3週間で回復していない場合、リダイレクトまたはインデックス作成で何か間違っています。

Lighthouse監査

すべての主要なテンプレート(ホームページ、ブログ投稿、製品ページ、連絡先ページ)でLighthouseを実行します。対象:

  • パフォーマンス:90以上
  • アクセシビリティ:95以上
  • ベストプラクティス:95以上
  • SEO:100

最後の移行(400ページのコンテンツサイト)で、WordPressの平均Lighthouseパフォーマンススコアは38から、VercelにデプロイされたNext.jsで96になりました。これはチェリーピックされていません。これは平均です。

WordPressに留まるべき場合

ここが私があなたの何人かを失う部分ですが、このガイドの最も重要なセクションです。

移行しないでください:

  • 20ページ未満のシンプルなブログまたはブロシュアサイトがある場合
  • あなたのチームが非技術的であり、日常の更新のためにWordPressアドミンに依存している場合
  • Lighthouseスコアが既に70以上で、パフォーマンスが重要なビジネスニーズがない場合
  • セキュリティの問題がなく、ホスティングが安定している場合
  • プラグインの総費用が年間200ドル未満の場合
  • 開発者がいない(またはNext.jsサイトを維持するための予算がない)場合

優れたホスト(Cloudways、Kinsta)、堅実なテーマ、最小限のプラグインを使用したWordPressは問題ありません。実は、それ以上です。それは戦闘でテストされ、十分に文書化されており、何百万人の開発者に理解されています。

移行は次の場合に意味があります:

  • パフォーマンスが直接売上に関連している(e-コマース、SaaS マーケティングサイト)
  • 管理ホスティングセキュリティプラグインに月額500ドル以上を費やしている場合
  • 開発チームが既にReactを書いている場合
  • プレビュービルド、ステージング環境、ロールバックを備えたデプロイメントパイプラインが必要な場合
  • セキュリティ表面積が本当の懸念事項である場合(政府、医療、金融)

これを言う理由は、信頼が売却より重要だからです。移行が価値があるかどうかわからない場合は、お問い合わせください。正直な評価をさせてください。

パフォーマンスベンチマーク:移行前後

2024~2025年の最後の5つの移行から:

メトリック 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メタタイトルと説明の移行、サイトマップ構造の保持、Google Search Consoleへの新しいサイトマップの即座の送信です。最後の移行で、サイトが移行前のトラフィックに1~2週間以内に回復しました。3ヶ月後には、Core Web Vitalsの改善により有機的な成長が大幅に増加しました。

ヘッドレスCMSとしてWordPressをNext.jsで使用できますか?

はい。人気のあるアプローチです。WordPressをコンテンツバックエンド(WP REST APIまたはWPGraphQL経由)として保持し、Next.jsをフロントエンドとして使用します。これにより、WordPressの編集体験が馴染みのあるまま保持されながら、Next.jsパフォーマンスが得られます。欠点は、セキュリティと更新のオーバーヘッドがある状態でWordPressインストールを保持していることです。新規プロジェクトではPayload CMSまたはSanityをお勧めします。チームがWordPressワークフローに深く投資されている場合は除きます。

WordPressからNext.jsへの移行のコストはいくらですか?

開発者の時間でDIY:ツールは無料ですが、開発時間に80~200時間を予算してください。エージェンシーコスト:サイトの複雑さ、ページ数、e-コマース機能、カスタム機能によって、通常は10,000ドル~50,000ドルです。詳細については、価格ページを確認してください。ROIは通常、ホスティングコストの削減(月額50~100ドルの節約)、廃止されたプラグインライセンス料、改善されたパフォーマンスからのコンバージョン率向上から出ます。

移行後、WordPressプラグインはどうなりますか?

各プラグインはNext.js等価物を必要とします。Contact Form 7またはGravity Formsはサーバーアクションになります。Yoast SEOはNext.js Metadata APIになります。WooCommerceはStripeになります。Google Analyticsは同じままです(トラッキングスニペットを移動するだけ)。Wordfenceなどのプラグインは、攻撃するWordPressがないため不要になります。移行を開始する前に、プラグインの完全なインベントリを作成します。明確な置き換え戦略なしのプラグインはリスクです。

WooCommerceのサブスクリプションをStripeに移行できますか?

はい。ただし、アクティブなサブスクライバーの慎重な処理が必要です。Stripeで顧客とサブスクリプションを作成してから、請求変更をお客様に通知する必要があります。Stripe Billingは試用期間、按分配分、失敗した支払い再試行ロジック、キャンセルフロー処理を処理します。移行自体は1回限りのスクリプトですが、実際のサブスクリプションシナリオに対してテストするのに時間がかかります。100以上のアクティブなサブスクライバーがいる場合は、この予算に余分な時間を追加してください。

WordPressから移行した後のNext.jsの最適なホスティングは何ですか?

Vercelはデフォルトの選択肢です。これはNext.jsを作るチームによって構築されており、無料層はほとんどのマーケティングサイトを処理します。Vercel Proは月額20ドルです。代替案には、Netlify、Cloudflare Pages(エッジパフォーマンスに優れています)、および完全な制御が必要な場合のVPSでのセルフホスティング(Docker)が含まれます。これらはすべてマネージドWordPressホスティングよりもはるかに安いです。