目次
ヘッドレスCMSとは
そもそも、CMS(コンテンツ管理システム)は、Webサイトやアプリで提供される様々な情報(文章、画像、動画など)を効率的に管理するための仕組みです。
「ヘッドレスCMS」は、その中でも、コンテンツを表示する部分(フロントエンド)と、コンテンツを管理する部分(バックエンド)が分離された構造を持つCMSを指します。
従来のCMSとの違い
従来のCMSでは、コンテンツ(記事や画像など)の管理と、それらをWebページとして表示する機能がセットになっており、「コンテンツを入れる箱」と「Webページのデザイン」が一体になっているイメージでした。
一方でヘッドレスCMSは、「ヘッド」(見た目の部分)を持たないため、コンテンツの管理と表示を独立して行うことができます。
ヘッドレスCMSのメリット
ヘッドレスCMSは従来のCMSの課題を解決し、様々なメリットをもたらします。具体的に見ていきましょう。
フロントエンドの自由度
従来のCMSではサービス固有のテンプレート(ひな形)を使う必要があるため、デザインの自由度が制限されることが多々あります。
一方で、ヘッドレスCMSはReactやVue.jsなどの最新のJavaScriptフレームワークを用いることが可能であり、開発者がデザインや機能を自由に実装できます。
多様なデバイスへの対応
従来のCMSではWebサイトでの表示を前提としている場合がほとんどであり、スマホアプリなどへの対応が難しいケースも少なくありません。
ヘッドレスCMSならWebサイトだけでなく、モバイルアプリやIoTデバイスなど多様なデバイスに向けた展開が可能です。コンテンツの配信先を限定しない、柔軟な配信システムは、デジタル技術の発展した現代にぴったりです。
表示速度の改善(SEO対策)
スマートフォンからのアクセスが主流となっている現在、Webサイトの表示速度は、ユーザーエクスペリエンスの向上だけでなく、SEO対策においても非常に重要な要素です。
ヘッドレスCMSは、Webページに必要なデータのみをAPI経由で取得するため、従来のCMSよりも高速なWebページ表示を実現できます。
ヘッドレスCMSにおけるフロントエンド技術の選定
現代のWebサイト制作は、目まぐるしいスピードで進化を続けています。かつてはテキストと画像が表示されるだけでしたが、ユーザーの操作に応じてリアルタイムに情報が更新されるものが一般的になっています。
ヘッドレスCMSは、このような現代的なWebサイト制作でよく用いられており、ReactやNext.jsなどのモダンなフロントエンド技術との相性が良いです。
Reactの特徴
Reactは、UI構築に特化したJavaScriptライブラリです。
コンポーネントベースの開発により、効率的なUI構築が可能です。ネイティブアプリのようなリッチなUIを構築でき、スムーズなアニメーションやインタラクションにより、ユーザー体験が向上します。
さらに、Reactは可読性が高く、コードの保守性にも優れており、学習リソースが豊富であるため、比較的初学者にも優しいという特徴があります。
Reactを採用する際の課題
Reactは、UI構築に特化したJavaScriptライブラリである点において、ヘッドレスCMSとの連携に適しています。
しかし、Reactはクライアントサイドレンダリング(CSR)が基本となるため、初期表示速度やSEO対策に課題が残る場合があります。
CSRでは、Webページの描画をブラウザで行うため、JavaScriptの実行が完了するまでコンテンツが表示されず、ユーザーに空白のページが表示される時間が長くなることがあります。検索エンジンのクローラーは、JavaScriptの実行によって生成されたコンテンツを認識しにくい場合があります。
さらに、大規模なアプリケーションでは、パフォーマンスの最適化が重要になります。特に、多数のコンポーネントを使用する場合、レンダリングの最適化を行わないと、ページの動作が遅くなることがあります。
ReactよりもNext.jsがおすすめな理由
Next.jsは、Reactをベースにしたフレームワークであり、サーバーサイドレンダリング(SSR)や静的サイト生成(SSG)などの機能を提供し、ReactアプリケーションのパフォーマンスとSEOを向上させます。
これにより、Reactで課題となりやすい初期表示速度とSEOに対応し、よりパフォーマンスの高いWebサイトを制作できます。
レンダリング手法の選択
レンダリングとは、WebブラウザがHTML、CSS、JavaScriptなどのコードを解釈し、Webページを画面に表示するプロセスのことです。Next.jsでは、レンダリング処理をいつ、どこで行うかによって3つの手法を選択できます。
- CSR(Client-Side Rendering)
クライアント側でページを生成する手法。Reactの基本的なレンダリング手法です。 - SSR(Server-Side Rendering)
サーバー側でページを生成し、クライアントに送信する手法。常に最新の情報を表示するサイトに適しています。 - SSG(Static Site Generation)
ビルド時にページを生成する手法。静的なコンテンツが多いサイトに適しており、高速な表示が可能です。
パフォーマンス向上
Next.jsは、SSRやSSGなどのレンダリング手法により、ページの初期表示速度を大幅に向上させることができます。これにより、ユーザーはWebサイトをストレスなく閲覧できます。
SEOに強いサイトを作りやすい
SSRにより、検索エンジンのクローラーがコンテンツを認識しやすい形でページを生成できます。読み込み速度が速いWebサイトは、検索エンジンの評価も高くなる傾向があり、SEO対策にも貢献します。
比較結果
Webサイト制作におけるフロントエンド技術選定で重要なポイントは、サイトのパフォーマンスやSEOの対策であり、その観点ではNext.jsが優れていると言えます。
そのため、本記事ではReactではなくNext.jsを採用して、Webサイト構築手順を解説します。
サイト公開までの手順
今回は、以下のようなブログトップページと、ブログ詳細ページを作成します。


Next.js のセットアップ
Next.js のインストールは、自動インストールで行うのがおすすめです。手動インストールでも可能ですが、特別な理由がない限り自動で問題ありません。
インストール方法の違いについては、Next.js の公式ドキュメントをご確認ください。
https://nextjs.org/docs/app/getting-started/installation
自動インストール手順
ターミナルで以下のコマンドを実行します。
npx create-next-app@latest
実行すると質問が表示されるので、対話形式で初期設定を進めます。
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Turbopack for `next dev`? No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*
今回は以下の内容で設定しています。ルーティングはNext.js が推奨する App Router を選択しました。
プロジェクト名 - test-headless-cms
TypeScriptの利用 - Yes
ESlintの利用 - Yes
TailWind CSSの利用 - Yes
srcディレクトリを使うか - Yes
App routerを使うか - Yes
インポートエイリアスをカスタマイズするか - No
App Router 設定は重要なポイントになります。のちほど詳しく解説します。
ディレクトリ構成
インストール時にApp Routerを選択したので、ディレクトリ構成は以下の通りになっています。
test-headless-cms
└── src
└── app
├── blog
│ └── [slug]
│ └── page.tsx // ブログ記事詳細ページ
└── page.tsx // ブログトップページ
App Rouer とは
Pages Routerの代替として提供される新しいルーティングシステムです。App Routerは、より柔軟なルーティング、React Server Components(RSC)の採用、そして、大規模なアプリケーションでより優れたパフォーマンスと可観測性を提供します。
ディレクトリ構成は、URLに対応するページ用のコンポーネントを配置する必要があります。
※ ちなみに、 Pages Router を選択した場合は以下のような構成になります。
test-headless-cms
└── pages
├── blog
│ └── [slug].tsx // ブログ記事詳細ページ
└── blog.tsx // ブログトップページ
ヘッドレスCMSのセットアップ
Next.js のセットアップの次は、ヘッドレスCMSのセットアップを進めます。
カードの登録不要ですぐに利用開始でき、無料プランでもサイトを複数作成できるので、今回はNILTOを選択しました。
アカウント登録
NILTO プロダクトサイトにアクセスし、「無料ではじめる」ボタンから進めます。
https://nilto.com/ja

モデル作成
ブログモデルを作成します。追加したフィールドは以下の通りです。
- 記事タイトル(1行テキストフィールド)
- サムネイル(メディアフィールド)
- 記事本文(フレキシブルテキストフィールド)
- スラッグ(1行テキストフィールド)

コンテンツ作成
ブログ記事の元となるコンテンツを登録します。画像のように、必要な情報(記事本文など)を入力します。
記事が完成したら、右上のボタンで公開します。

フロントエンド開発
APIキーの取得
Webサイト側から、NILTOで生成されたAPIキーをリクエストヘッダーに付与してリクエストすることで、コンテンツを取得することができます。
NILTOで公開したコンテンツを表示させたいので、「公開のみ」のAPIキーのトークンをコピーします。

リクエストヘッダーの指定形式については、リンク先の記事を参照ください。
https://nilto.com/ja/docs/topics/tutorial#38def47dedb74ac0f69eabacdfd2bd1d0cb3afae
APIキーの設定
環境変数として、NILTOのAPIキーを.env ファイルに定義します
// .env
NILTO_API_KEY=XXXXXXXXXXXXX
型定義
NILTOで作成したモデルの設定に沿って、型定義ファイルに定義を記述します。
// src/types/contents.ts
export interface ApiResponse {
data: Content[];
}
export interface Content {
_id: number // コンテンツのID
_model: string // モデルのLUID
_status: string // 公開状態
_title: string
slug: string
title: string // コンテンツのタイトル
article: string // コンテンツの本文
_created_at: Date // コンテンツ作成日
_updated_at: Date // コンテンツ更新日
_published_at: Date // コンテンツ公開日
thumbnail: { // 画像
url: string;
alt: string;
};
}
モデルの設定内容と差異があると、表示ができないので注意が必要です。
例)記事本文は、articleというLUIDで作成されている必要があります
モデルのLUIDについての詳細は、チュートリアルのモデル作成の項目をご確認ください。
https://nilto.com/ja/docs/topics/tutorial#90e8e45a02f319f9470de12f8668e15cf5f7a37b
コンテンツの取得
ブログトップページとブログ記事詳細ページ用のページコンポーネントを作成し、コンテンツの取得処理を記述します。
今回はNext.jsプロジェクトのセットアップ時に App Router を選択しているため、SSRに相当する機能を利用したい場合は fetch メソッドのcacheオプションをno-storeで指定する必要があります。
// src/app/page.tsx(ブログトップページ)
import Link from 'next/link';
import type { ApiResponse } from '@/types/contents';
export default async function BlogToplPage() {
const apiKey = process.env.NILTO_API_KEY;
const requestHeader = new Headers();
requestHeader.set('X-Nilto-Api-Key', apiKey ?? '');
requestHeader.set('Content-Type', 'application/json');
const apiUrl = 'https://cms-api.nilto.com/v1/contents?model=blog&order=-_updated_at';
const response = await fetch(apiUrl, {
method: 'GET',
headers: requestHeader,
cache: 'no-store', // 常に最新データを取得
});
const apiResult = await response.json() as ApiResponse;
const contents = apiResult?.data || [];
return (
<div className='top-page'>
<div>
<h1>技術ブログ</h1>
<ul style={{ listStyle: 'none', padding: 0 }}>
{contents.map((content) => {
return (
<li key={content._id || content._title} style={{ marginBottom: '10px' }}>
<Link href={`/blog/${content.slug}`} style={{ textDecoration: 'underline' }}>
{content._title}
</Link>
{<span style={{ marginLeft: '10px', color: '#888' }}>
{new Date(content._published_at).toLocaleDateString('ja-JP', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})}
</span>}
</li>
);
})}
</ul>
</div>
</div>
);
}
// src/app/blog/[slug]/page.tsx(ブログ記事詳細ページ)
import Link from 'next/link';
import Image from 'next/image';
import type { ApiResponse } from '@/types/contents';
type Props = {
params: Promise<{ slug: string }>;
};
export default async function BlogDetailPage({ params }: Props) {
const slug = (await params).slug;
const apiKey = process.env.NILTO_API_KEY;
const requestHeader = new Headers();
requestHeader.set('X-Nilto-Api-Key', apiKey ?? '');
requestHeader.set('Content-Type', 'application/json');
const identifier = decodeURIComponent(slug);
const apiUrl = `https://cms-api.nilto.com/v1/contents?model=blog&slug[eq]=${encodeURIComponent(identifier)}`;
const response = await fetch(apiUrl, {
method: 'GET',
headers: requestHeader,
cache: 'no-store', // 常に最新データを取得
});
const apiResult = await response.json() as ApiResponse;
const content = apiResult.data[0];
return (
<article>
<div>
<h1>{content._title}</h1>
<div className='my-4 relative' style={{ width: '100%', paddingTop: '56.25%' }}>
<Image
src={content.thumbnail.url}
alt={content.thumbnail.alt || content._title || '記事サムネイル'}
fill
style={{ objectFit: 'contain' }}
priority
/>
</div>
<p style={{ color: '#666', fontSize: '0.9em' }}>
公開日: {new Date(content._published_at).toLocaleDateString('ja-JP', { year: 'numeric', month: '2-digit', day: '2-digit' })}
</p>
<div dangerouslySetInnerHTML={{ __html: content.article }} />
</div>
<div style={{ marginBottom: '20px', marginTop: '10px' }}>
<Link
href='/'
style={{ textDecoration: 'underline', color: 'blue' }}
>
戻る
</Link>
</div>
</article>
);
}
コンテンツAPIの詳細については、NILTOのAPIリファレンスをご覧ください。
https://nilto.com/api
サイトの公開
サイトを公開するために、リモート環境(サーバー)にデプロイします。
GitHub
デプロイしたいサイトのプロジェクトのソースコードを置くリポジトリを作成します。
ホスティングサービスの連携
今回は、ホスティングサービスとしてAWS Amplifyを利用します。
AWSのアカウントを作成し、AWS Amplifyのアプリを作成して、GitHubリポジトリと連携します。
GitHubでブランチにソースコードがプッシュされた際に、最新の状態をサイトに反映することが可能です。
1. ソースコードプロバイダーを選択
今回はGitHubを選択し、「次へ」をクリックします。

2. リポジトリとブランチを追加
mainブランチを選択し、「次へ」をクリックします。

3. アプリケーションの設定
アプリケーションの名前を入力します。
フレームワークはリポジトリ内のソースコードから自動で検出されるので、ビルドコマンド等は記入する必要はありません。

4. 確認
最後に設定内容が表示されます。内容が正しいか確認し、「保存してデプロイ」をクリックします。

5. 完成
アプリが作成されたら、完了です。

環境変数の設定
Next.jsのサーバーコンポーネントでキャッシュせずにリクエストごとに最新データを取得したい場合は、
Amplifyの環境変数の設定画面ではなく、ビルド時にechoコマンド等で変数を割り当てる必要があります。
例)build の commands 以下に echo 'NILTO_API_KEY=$NILTO_API_KEY' >> .env
を追記します。
version: 1
frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands:
- echo 'NILTO_API_KEY=$NILTO_API_KEY' >> .env
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- .next/cache/**/*
- .npm/**/*
詳細はAWS公式ドキュメントをご確認ください。
https://docs.aws.amazon.com/en_us/amplify/latest/userguide/ssr-environment-variables.html
まとめ
本記事では、ヘッドレスCMSとNext.jsを組み合わせたウェブサイト構築の手順について解説しました。
この構成は、コンテンツ管理の柔軟性と、フロントエンドにおけるパフォーマンスおよびSEOの最適化を目指す場合に有効な選択肢です。
より高速で、柔軟なウェブサイト開発手法として、ぜひヘッドレスCMSとNext.jsを活用したウェブサイト構築に挑戦してみてください。