Next.js + ヘッドレスWordPressの理想と現実。ACFのデータ取得が複雑すぎませんか?
Next.jsによる高速なフロントエンドと、WordPressの柔軟なコンテンツ管理(CMS)。この組み合わせは、現代のWeb制作における強力なソリューションの一つです。しかし、実際に構築を進めると、多くの方が「ある壁」に直面します。それは、Advanced Custom Fields (ACF) で作成した複雑なフィールドのデータを、Next.js側でどう扱うかという問題です。
特にBtoBサイトのオウンドメディアでは、記事に関連情報やCTAを紐付けるために、ACFのリピーターフィールドやグループフィールドが多用されます。
「connect_insights_summary」のようなカスタムフィールドの扱いに潜む罠
例えば、「この記事に関連するインサイト」として、複数の「タイトル」と「リンク先」、「概要文」をセットで管理したいとします。ACFで「connect_insights_summary」という名前のリピーターフィールドを作成するのは簡単です。
しかし問題は、このネストされた配列データを、フロントエンド(Next.js)でいかに効率的かつ安全に取得し、表示するかです。
REST APIや基本的なGraphQLクエリではなぜ不十分なのか
WordPressの標準REST APIは、ACFのデータを取得するために追加のカスタマイズや複数回のリクエストが必要になることが多く、パフォーマンスのボトルネックになりがちです。
一方、WPGraphQLは強力ですが、ACFのフィールド構造をそのままGraphQLのスキーマに反映させるには、「WPGraphQL for ACF」プラグインの利用が実質的に必須となります。これなしでは、ACFデータは単なるシリアライズされた文字列として返されることもあり、フロントエンドでの扱いは困難を極めます。
その実装、保守性が低いかも? 複雑なデータ構造が引き起こす「負債」
「とりあえず動く」実装は可能かもしれません。しかし、そのコードは将来的に技術的負債となる可能性があります。
コンポーネントがデータ構造に密結合してしまう
データ取得ロジックと表示ロジックがコンポーネント内で混在し、ACFのフィールド構造(例: data.post.acfFields.connectInsightsSummary[0].summaryTitle のような深いネスト)に強く依存したコードは、非常に脆くなります。
WordPress側でフィールド名や構造を少し変更しただけで、フロントエンドの表示が壊れてしまうのです。
クエリが肥大化し、パフォーマンスに影響が出る懸念
必要なデータだけを的確に取得するGraphQLの利点を活かせず、不要なデータまで取得(オーバーフェッチ)してしまったり、逆に必要なデータを取得するためにクエリが極端に複雑化・肥大化したりするケースも見られます。
「ACFのデータをNext.jsで使うのは面倒だ」と感じているなら、それは実装アプローチを見直すサインかもしれません。
鍵は「WPGraphQL for ACF」と「適切なコンポーネント設計」
この問題を解決し、開発者体験(DX)と保守性を向上させるための具体的なアプローチは、以下の3つのステップに集約されます。
解決策1: 「WPGraphQL for ACF」でスキーマをリッチにする
まず、WordPress側に「WPGraphQL for ACF」プラグインを導入します。これにより、ACFで作成したフィールドグループがWPGraphQLのスキーマに自動的に追加され、型定義が明確になります。GraphiQL(GraphQLのIDE)で、どのようなクエリが発行可能かを確認できるようになることが、最初の大きな一歩です。
解決策2: Next.js側でデータ取得ロジックを分離する
Next.js(特にApp Router)では、データ取得ロジックを専用の関数やファイル(例: lib/api.js や app/blog/[slug]/page.jsx 内の generateStaticParams / page 関数)にカプセル化します。
コンポーネントは「何を表示するか」に集中し、「どうデータを取るか」は関知しない設計を目指します。
解決策3: 受け取ったデータを扱うコンポーネントを分割する
取得したデータをそのまま巨大なページコンポーネントでレンダリングするのではなく、特定のデータ構造(例: connect_insights_summary)を表示するためだけの、小さく再利用可能なコンポーネントを作成します。
【実装例】ACF「connect_insights_summary」をNext.jsで表示する全手順
ここでは、最も具体的なシナリオとして、ACFのリピーターフィールド「connect_insights_summary」をNext.jsのApp Router(RSC: React Server Components)で表示する例を見ていきましょう。
ステップ1: ACFフィールド(リピーター)の定義(前提)
まず、WordPress側で以下のようなACFリピーターフィールドが定義されていると仮定します。
フィールド名 (Field Name):
connect_insights_summary
サブフィールド1 (テキスト):
summary_title (例: 記事のタイトル)
サブフィールド2 (URL):
summary_link (例: 記事への内部リンク)
サブフィールド3 (テキストエリア):
summary_excerpt (例: 記事の概要)
(※「WPGraphQL for ACF」が有効化され、このフィールドグループがGraphQLスキーマに公開設定されていることも前提とします。)
ステップ2: WPGraphQLのスキーマ確認とクエリ構築
GraphiQLなどのツールを使い、対象の投稿タイプ(例: post)で、ACFフィールドがどのように公開されているかを確認します。恐らく post.acfFields.connectInsightsSummary のような形でアクセスできるはずです。
以下は、特定の記事(ID指定)からこのACFデータを取得するためのGraphQLクエリ例です。
query GetPostAcfData($id: ID!) {
post(id: $id, idType: DATABASE_ID) {
id
title
# ACFフィールドグループにアクセス
acfFields {
# リピーターフィールド名
connectInsightsSummary {
# サブフィールド名を指定
summaryTitle
summaryLink
summaryExcerpt
}
}
}
}
ステップ3: Next.js(App Router)でのデータ取得関数の実装
Next.jsプロジェクトのデータ取得ロジック(例: lib/graphql-client.js や app/blog/[slug]/page.jsx)で、このクエリを実行します。fetch を使ったRSCでのシンプルな例を示します。
// lib/api.js (仮)
async function fetchGraphQL(query, variables = {}) {
const headers = { 'Content-Type': 'application/json' };
// WPGraphQLのエンドポイントURL
const response = await fetch(process.env.WP_GRAPHQL_URL, {
method: 'POST',
headers,
body: JSON.stringify({ query, variables }),
// Next.js 14+ のキャッシュ戦略(例: 60秒キャッシュ)
next: { revalidate: 60 },
});
const json = await response.json();
if (json.errors) {
console.error(JSON.stringify(json.errors, null, 2));
throw new Error('Failed to fetch API');
}
return json.data;
}
// 記事ページ (page.jsx) で使用するデータ取得関数
export async function getPostData(id) {
const query = `
query GetPostAcfData($id: ID!) {
post(id: $id, idType: DATABASE_ID) {
id
title
acfFields {
connectInsightsSummary {
summaryTitle
summaryLink
summaryExcerpt
}
}
}
}
`;
const data = await fetchGraphQL(query, { id });
return data.post;
}
// ---
// app/blog/[slug]/page.jsx (抜粋)
// import { getPostData } from '@/lib/api';
// import { InsightsSummary } from '@/components/InsightsSummary';
// export default async function PostPage({ params }) {
// // params.slug から記事IDを取得するロジック (省略)
// const postId = 123; // 仮のID
// const postData = await getPostData(postId);
// const insights = postData?.acfFields?.connectInsightsSummary;
// return (
// <article>
// <h1>{postData.title}</h1>
// {/* ... 記事本文 ... */}
// {/* ACFデータをコンポーネントに渡す */}
// {insights && insights.length > 0 && (
// <InsightsSummary summaries={insights} />
// )}
// </article>
// );
// }
ステップ4: Reactコンポーネント(JSX)でのレンダリング
ステップ3で分離した InsightsSummary コンポーネントの実装例です。これにより、ページコンポーネントはデータ構造の詳細を知る必要がなくなります。
import Link from 'next/link';
// 型定義(TypeScriptの場合)
// type SummaryItem = {
// summaryTitle: string;
// summaryLink: string;
// summaryExcerpt: string;
// };
// type InsightsSummaryProps = {
// summaries: SummaryItem[];
// };
export function InsightsSummary({ summaries }) {
// summaries が null や undefined、または空配列の場合のガード
if (!summaries || summaries.length === 0) {
return null;
}
return (
<div className="insights-summary-container">
<h3>関連インサイト</h3>
<ul className="insights-list">
{summaries.map((item, index) => (
// リピーターフィールドでは安定したIDがない場合、indexをkeyに使う
// (もし __typename や一意のIDが取得できるならそちらを推奨)
<li key={index} className="insight-item">
<h4>
<Link href={item.summaryLink || '#'} legacyBehavior>
<a>{item.summaryTitle || 'タイトルなし'}</a>
</Link>
</h4>
{item.summaryExcerpt && (
<p>{item.summaryExcerpt}</p>
)}
</li>
))}
</ul>
</div>
);
}
この設計により、connect_insights_summary の表示スタイルを変更したい場合は InsightsSummary.jsx を修正するだけで済み、page.jsx はデータの取得と受け渡しに集中できます。
次世代のWeb制作へ。今日から始めるACFとNext.jsの最適化
Next.jsとヘッドレスWordPress(ACF)の連携は、正しく設計すれば非常に強力で、開発者にとってもコンテンツ管理者にとっても快適な環境を構築できます。
まずはGraphiQLでクエリを試してみましょう
もし「WPGraphQL for ACF」をまだ試していないなら、まずはローカル環境やステージング環境でプラグインを導入し、WordPressダッシュボードからGraphiQLを開いてみてください。ACFで作成したフィールドがスキーマにどのように反映されるかを見るだけでも、大きなヒントが得られるはずです。
ヘッドレスCMS構築のパートナーをお探しなら
私たちは、Next.jsとヘッドレスWordPress(ACF)を活用した、高パフォーマンスでスケーラブルなWebサイト構築を専門としています。
「既存サイトのJamstack化を進めたい」「ACFのデータ連携がうまくいかない」といった技術的な課題から、「オウンドメディアのパフォーマンスを改善したい」といったビジネス課題まで、幅広くサポートいたします。

