2026年9月11日

2026年9月11日

WordPressのHeadless構成でNuxt 3フロントエンドを構築する方法

WordPress をヘッドレス CMS にして Nuxt 3 でフロントエンドを作るなら、WordPress の REST API を Nuxt のサーバールート経由で取得し、ページごとにキャッシュの方針を routeRules で決める構成が扱いやすいです。ブラウザから WordPress に直接アクセスさせないので、CORS の設定や WordPress の URL を表に出す問題を避けられます。この記事では、プロジェクトの作成から記事一覧・記事詳細ページ、キャッシュ設定、SEO 用のメタ情報、公開前の確認までを順に説明します。

先に確認しておきたいこと:Nuxt 3 と Nuxt 4

Nuxt は 2025年に Nuxt 4 が安定版になり、Nuxt 3 は保守期間の終了が告知されています。これから新規に作るなら Nuxt 4 を選ぶのが自然で、既存の Nuxt 3 プロジェクトも移行を計画しておくのが無難です。サポート期間の詳細は Nuxt の公式ブログで確認してください(2026年10月現在)。

この記事のコードは Nuxt 3 と Nuxt 4 のどちらでも同じ考え方で動きます。大きな違いはディレクトリ構成で、Nuxt 4 ではページやコンポーネントを app/ ディレクトリの下に置くのが標準になりました。server/ ディレクトリはどちらもプロジェクト直下です。以下では pages/ と書きますが、Nuxt 4 では app/pages/ と読み替えてください。

構成の全体像

データの流れは次のようになります。

  1. 編集者は従来どおり WordPress の管理画面で記事を書く
  2. Nuxt のサーバールート(server/api/)が WordPress の REST API から記事を取得し、結果をキャッシュする
  3. Nuxt のページは自分のサーバールートを useFetch で呼び、HTML を生成する

WordPress の REST API を Nuxt のページから直接呼ぶこともできますが、サーバールートを1段挟むと、WordPress の URL や認証情報をブラウザに出さずに済み、取得する項目の絞り込みやキャッシュも1か所で管理できます。

WordPress 側では、パーマリンク設定を「基本」以外にして https://cms.example.com/wp-json/wp/v2/posts が JSON を返すことを確認しておきます。スラッグは英数字とハイフンにそろえておくと、URL エンコードの扱いで迷わずに済みます。

プロジェクトを作成して接続先を設定する

プロジェクトの作成コマンドは時期によって変わっており、最近の公式ドキュメントでは npm create nuxt@latest が案内されています(以前は npx nuxi init でした)。作成したら nuxt.config.ts に接続先を設定します。

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // サーバー側だけで使う値(ブラウザには渡らない)
    wpApiBase: 'https://cms.example.com/wp-json',
  },
  routeRules: {
    '/': { swr: 600 },
    '/posts/**': { swr: 3600 },
  },
})

runtimeConfig の直下に置いた値はサーバー側だけで使われ、public の下に置いた値だけがブラウザにも渡ります。WordPress の URL や認証情報は public に入れないでください。本番環境では、環境変数 NUXT_WP_API_BASE を設定すると、この値を上書きできます。

routeRules の swr は、指定した秒数のあいだ生成済みの HTML をキャッシュから返し、期限が切れたら古い内容を返しつつ裏で作り直す設定です。prerender: true にするとビルド時に静的な HTML を生成し、Vercel や Netlify のようにプラットフォームが対応していれば isr も指定できます。どれが使えるかはデプロイ先によって異なるため、デプロイ先のドキュメントと合わせて確認してください。

サーバールートで WordPress から記事を取得する

記事詳細を返すサーバールートです。defineCachedEventHandler を使うと、WordPress への問い合わせ結果を Nuxt 側でキャッシュできます。

// server/api/posts/[slug].get.ts
export default defineCachedEventHandler(async (event) => {
  const slug = getRouterParam(event, 'slug') ?? ''
  if (!/^[a-z0-9-]+$/.test(slug)) {
    throw createError({ statusCode: 400, statusMessage: 'Invalid slug' })
  }

  const config = useRuntimeConfig(event)
  const posts = await $fetch<any[]>(`${config.wpApiBase}/wp/v2/posts`, {
    query: { slug, _fields: 'id,slug,date,title,excerpt,content' },
  })

  if (!posts.length) {
    throw createError({ statusCode: 404, statusMessage: 'Post not found' })
  }
  return posts[0]
}, {
  maxAge: 600,
  getKey: (event) => getRouterParam(event, 'slug') ?? '',
})

一覧用のサーバールートも同じ形で作れます。

// server/api/posts/index.get.ts
export default defineCachedEventHandler(async (event) => {
  const config = useRuntimeConfig(event)
  return await $fetch<any[]>(`${config.wpApiBase}/wp/v2/posts`, {
    query: { per_page: 20, _fields: 'id,slug,date,title,excerpt' },
  })
}, { maxAge: 300 })

ここでのポイントは3つです。1つ目は、受け取ったスラッグを形式チェックしてから WordPress に渡していることです。2つ目は、_fields で必要な項目だけを取得していることで、REST API の応答は既定では項目が多く、絞るだけで転送量と処理時間が減ります。3つ目は、記事が見つからないときに 404 を返していることで、空のページを 200 で返すと検索エンジンに不要なページとして扱われる原因になります。

ページを作る

記事詳細ページです。自分のサーバールートを useFetch で呼びます。

<!-- pages/posts/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
const slug = String(route.params.slug)

const { data: post, error } = await useFetch(`/api/posts/${slug}`)

if (error.value || !post.value) {
  throw createError({ statusCode: 404, statusMessage: 'Not Found', fatal: true })
}

useSeoMeta({
  title: () => post.value?.title?.rendered ?? '',
  description: () => (post.value?.excerpt?.rendered ?? '').replace(/<[^>]*>/g, '').trim(),
})
</script>

<template>
  <article v-if="post">
    <h1 v-html="post.title.rendered" />
    <div v-html="post.content.rendered" />
  </article>
</template>

useFetch はサーバー側での描画時にデータを取得し、その結果をページに埋め込んでブラウザに渡すので、ブラウザ側で同じ取得が二重に走りません。一覧ページ(pages/index.vue)も /api/posts を呼び、NuxtLink で /posts/スラッグ へリンクすれば形になります。

注意したいのは v-html です。WordPress の本文は HTML なのでこの方法で表示しますが、v-html は中身をそのまま挿入するため、信頼できない HTML が混ざると XSS の原因になります。投稿できるのが信頼できる編集者だけかを確認し、外部の寄稿者が多い場合はサニタイズを検討してください。

もう1つは文字参照です。WordPress のタイトルは &#8217; のような文字参照を含むことがあり、useSeoMeta の title にそのまま渡すとブラウザのタブにそのまま表示される場合があります。表示を確認し、必要に応じてデコード処理を挟んでください。

WordPress 側で整えておくこと

ヘッドレス構成にすると、WordPress のテーマが出力していたものが表示されなくなります。事前に洗い出しておくと移行後の手戻りが減ります。

  • ブロックのスタイル:本文の HTML にはブロックエディターのクラス名が付いているが、そのスタイルシートは Nuxt 側で読み込む必要がある
  • プラグインの表示機能:目次、問い合わせフォーム、関連記事など、テーマ上で動いていた機能は Nuxt 側で作り直すか、別の手段に置き換える
  • SEO プラグインのメタ情報:タイトルや説明文を SEO プラグインで管理している場合、REST API で取得できる形になっているかを確認する
  • 旧 URL:WordPress のテーマで表示していた URL と Nuxt の URL が変わるなら、301 リダイレクトを用意する

WordPress 本体は管理画面と API だけを使う状態になるため、フロント側の URL に WordPress のテーマのページが残っていないか、検索エンジンにどちらの URL を登録させるかも整理しておきます。

公開前の確認

公開前には、開発サーバーではなく本番と同じ形でビルドして確認します。nuxt build で作ったものを node .output/server/index.mjs で起動するか、デプロイ先のプレビュー環境を使います。

  • 記事詳細を開き、HTML のソースに本文が含まれているか(サーバー側で描画されているか)
  • 存在しないスラッグで 404 が返るか
  • WordPress で記事を更新し、swr や maxAge に指定した時間のあとに反映されるか

更新をすぐに反映したい場合は、キャッシュ時間を短くするか、デプロイ先の仕組みでキャッシュを消す方法を検討します。nuxt generate による完全な静的書き出しを選んだ場合は、サーバールートは本番で動かず、記事を更新するたびに再ビルドが必要になる点に注意してください。

まとめ

  • 新規なら Nuxt 4 も検討する。Nuxt 3 と考え方は同じで、主な違いは app/ ディレクトリ
  • WordPress の REST API はサーバールート経由で取得し、URL や認証情報は runtimeConfig の非公開側に置く
  • defineCachedEventHandler と routeRules の swr で、取得結果と HTML のキャッシュ方針を決める
  • 記事が見つからなければ 404 を返し、v-html で表示する本文の出どころに注意する
  • ブロックのスタイル、プラグインの表示機能、旧 URL のリダイレクトは事前に洗い出しておく

お気軽にご相談ください

お見積りへ お問い合わせへ
▲