
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、最近特にTwitter
から情報を得ていると最新情報が豊富なので「時代に取り残されにくくなる」ような気がしています。
しかも、優秀な方々のツイートはお金を払ってでもほしいと思えるようなものがあったりもします。(それにしても、ああいう超優秀な方々はホント何者なんでしょうか…)
そして、そんな情報収集の中で気になったのが、
ヘッドレス CMS
「ヘッドレス CMS」とは、簡単にいうと「コンテンツ部分だけ管理する別サイト」で、例えば「ユーザーへのお知らせ部分だけ管理できるサイト」をイメージすると分かりやすいでしょうか。
そして、そんな「ヘッドレス CMS」はすでに日本版サービスが出てるぐらい浸透してきているのですが、そのときあることがひらめきました。
「これ、WordPressで作れるんじゃない??」
と。
そこで
今回は、WordPress
+ Laravel
でシンプルな「ヘッドレス CMS」をつくってみます。
ぜひ何かの参考になりましたら嬉しいです。
「シャウエッセン with
ハインツのケッチャップ
がうまくて仕方がない…」
開発環境: WordPress 5.9.2、Laravel 9.x
目次 [非表示]
やりたいこと
今回実装する内容は次のとおりです。
- 通常のブログ表示は無効にする
- 他のサイトからはアクセスできないよう認証をつける
- 別サイトの「お知らせ」を管理する
つまり、流れとしてはこうなります。
- WordPress でお知らせ記事を書く
- Laravel 側からデータを取得
- 「お知らせ」として表示
では、今回も楽しくやっていきましょう
専用テーマをつくる
まず、「通常のブログ表示は無効にする」部分からつくっていきます。
今回はブログ記事は表示しませんので、独自にWordPress
のテーマをつくり、さらに、もしアクセスされても、Rest API
のトップページへ強制リダイレクトするようにします。
まずは、wp-content/themes
の中へ「headless_cms」というフォルダをつくり、さらにその中へ「index.php」と「style.css」をつくります。
そして、各ファイルの中身を変更してください。
wp-content/themes/headless_cms/style.css
/*
Theme Name: Headless CMS
*/
wp-content/themes/headless_cms/index.php
<?php
$url = get_rest_url(null, 'wp/v2/');
wp_redirect($url); // Rest API へリダイレクト
die();
これで独自テーマ作成は完了です。
では、実際にテーマを「Headless CMS」へ変更してみましょう
WordPress管理ページのページ左側に表示されているメニューの中から、「外観 > テーマ」をクリック。
すると、テーマ一覧が表示されるので、その中から「Headless CMS」を有効にしてください。
これで、WordPress
トップページへアクセスしてもRest API
のトップページへ自動的に転送されることになります。
Rest API をログイン必須にする
続いて、「他のサイトからはアクセスできないようにする」部分です。
まずは、先ほどつくった独自テーマの中に以下のファイルを追加してください。
wp-content/themes/headless_cms/functions.php
<?php
// 直接アクセス禁止(セキュリティ対策)
if(!defined('ABSPATH')) {
exit;
}
// Rest API をログイン必須にする
add_filter('rest_authentication_errors', function($result) {
if(!is_user_logged_in()) {
return new WP_Error(
'rest_not_logged_in',
'You are not currently logged in.',
['status' => 401]
);
}
return $result;
});
これでログイン認証されていない場合はアクセスができなくなりますが、ではどうやって認証をするかというと「Application Password」を使います。
Application Password
は、簡単に言うと「一部の機能だけに使えるログイン・パスワード」です。(つまり、WordPress
の管理ページにログインすることはできません)
では、ページ左側メニューから「ユーザー > プロフィール」へ移動してください。
すると、ページ最下部に「アプリケーションパスワード」という項目があるので、パスワード名に「Headless CMS」と入力し、「新しいアプリケーションパスワードを追加」ボタンをクリックします。
クリックすると、アプリケーション・パスワードが表示されますので、大切に保管しておいてください。(二度と表示されません)
ちなみに、このApplication Password
は、WordPress
がHTTPS
で接続されていないと有効にできませんので注意してください。
では、これでWordPress
側の設定は完了です。
次はLaravel
へ行ってみましょう
Laravel から Rest API へアクセスしデータ取得する
続いて、Laravel
から、WordPress
のRest API
へアクセスし、「お知らせ」データを取得できるようにしてみます。
※ なお、JavaScript
からでも直接取得はできますが、そうなるとアプリケーション・パスワードがダダ漏れになりますので、今回のケースでは実装しないように注意してください。
コントローラーをつくる
では、まずはコントローラーです。
以下のコマンドを実行してください。
php artisan make:controller HeadlessCmsController
すると、ファイルが作成されるので、中身を次のように変更します。
app/Http/Controllers/HeadlessCmsController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class HeadlessCmsController extends Controller
{
public function index(Request $request)
{
$page = intval($request->input('page', 1));
$per_page = intval($request->input('per_page', 10));
$username = 'root'; // Application Password をつくったときのログインユーザー名
$application_password = 'ioOe kYj5 scp6 qzTj jYQM pT3h'; // 本来は .env に書くべきです
$authorization = 'Basic ' . base64_encode($username . ':' . $application_password);
$url = 'https://cms.wp59.test/wp-json/wp/v2/posts';
$should_verify = ! app()->environment('local');
$category_ids = [3]; // 取得したいサイトのカテゴリ ID(適宜変更してください)
$response = Http::withHeaders(['Authorization' => $authorization])
->withOptions(['verify' => $should_verify])
->get($url, [
'_fields' => 'id,title,content',
'categories' => implode(',', $category_ids),
'page' => $page,
'per_page' => $per_page,
'orderby' => 'date',
'order' => 'desc',
]);
if($response->ok()) {
return [
'data' => $response->json(),
'pagination' => [
'total' => $response->header('X-WP-Total'),
'total_pages' => $response->header('X-WP-TotalPages'),
],
];
}
abort(500, 'Failed to get posts.');
}
}
この中では、Laravel
のHTTP Client
(guzzle
)を使ってアクセスしているだけですので、シンプルなコードになっているかと思います。
ただ、一点気をつけていただきたいのが、以下の部分です。
->withOptions(['verify' => $should_verify])
これは「サーバー証明書の検証」を実行する or しないを決める部分なのですが、ローカル環境では検証してもうまくいかない(ことのほうが多い)と思いますので、以下の条件で切り替えています。
- ローカル: サーバー証明書の検証はしない
- 本番: 検証する
【追記:2025.02.15】
どうやら、パーマリンク設定の影響で上記のコードだとリダイレクトしてしまう場合があるようです。その場合は以下のように変更して試してみてください!
// 1:URLをトップへ変更
$url = 'https://cms.wp59.test/wp-json/wp/v2/posts';
↓↓↓
$url = 'https://cms.wp59.test/';
// 2:パラメータにrest_routeを追加
->get($url, [
'rest_route' => '/wp/v2/posts', // ←ここをパラメータに追加
'_fields' => 'id,title,content',
'categories' => implode(',', $category_ids),
'page' => $page,
'per_page' => $per_page,
'orderby' => 'date',
'order' => 'desc',
]);
ルートをつくる
次に、ルートです。
以下のように変更してください。
routes/web.php
// 省略
use App\Http\Controllers\HeadlessCmsController;
// 省略
Route::controller(HeadlessCmsController::class)->group(function(){
Route::get('headless_cms', 'index')->name('headless_cms.index');
});
Rest API から取得したデータを表示する
では、最後にRest API
から取得したデータをVue
で表示する部分です。
resources/views/announcement.blade.php
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="p-5 w-50">
<div v-if="isStatusIndex">
<ul v-for="announcement in announcements">
<li>
<a href="#" v-text="announcement.title.rendered" @click.prevent="showDetail(announcement.id)"></a>
</li>
</ul>
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item" :class="{ disabled: !hasPrevPage }" @click.prevent="movePage('prev')"><a class="page-link" href="#">前へ</a></li>
<li class="page-item" :class="{ disabled: !hasNextPage }" @click.prevent="movePage('next')"><a class="page-link" href="#">次へ</a></li>
</ul>
</nav>
</div>
<div v-if="isStatusShow">
<div class="card mb-3">
<div class="card-header" v-text="currentAnnouncement.title.rendered"></div>
<div class="card-body" v-html="currentAnnouncement.content.rendered"></div>
</div>
<button class="btn btn-secondary" @click="setStatus('index')">戻る</button>
</div>
</div>
<script src="https://unpkg.com/vue@3.2.31/dist/vue.global.prod.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.26.1/axios.min.js"></script>
<script>
const { createApp, ref, computed, onMounted } = Vue;
createApp({
setup() {
// 表示ステータス
let status = ref('index');
const isStatusIndex = computed(() => status.value === 'index');
const isStatusShow = computed(() => status.value === 'show');
const setStatus = newStatus => {
status.value = newStatus;
};
// お知らせ一覧
let announcements = ref([]);
let pagination = ref({});
let page = ref(1);
const perPage = 2;
const getPosts = () => {
const url = '{{ route('headless_cms.index') }}';
const params = {
params: {
page: page.value,
per_page: perPage
}
}
axios.get(url, params)
.then(response => {
const responseData = response.data;
announcements.value = responseData.data;
pagination.value = responseData.pagination;
});
};
const movePage = direction => {
if (direction === 'prev' && hasPrevPage.value) {
page.value--;
} else if (direction === 'next' && hasNextPage.value) {
page.value++;
}
getPosts();
};
const hasPrevPage = computed(() => {
return page.value > 1;
});
const hasNextPage = computed(() => {
return pagination.value.total_pages > page.value;
});
onMounted(() => {
getPosts();
});
// お知らせ詳細
let currentAnnouncementId = ref(-1);
const currentAnnouncement = computed(() => {
return announcements.value.find(announcement => {
return Number(announcement.id) === Number(currentAnnouncementId.value)
});
});
const showDetail = (announcementId) => {
currentAnnouncementId.value = announcementId;
status.value = 'show';
};
return {
isStatusIndex,
isStatusShow,
setStatus,
announcements,
movePage,
hasPrevPage,
hasNextPage,
currentAnnouncement,
showDetail
}
}
})
.mount('#app');
</script>
</body>
</html>
今回もComposition API
形式でコードを書いています(.value
が邪魔くさいかもですね…)
ちなみに:カテゴリ分けして複数サイト対応する
当初は複数サイト対応を考えていたのですが、そうなると記事として複雑になりすぎてしまうと思ったので、ここで「おまけ的」なカンジでご紹介することにします。
なお、この形にしておくと「WordPress ひとつあれば、いくつでもサイトののお知らせ管理ができる」のでとても便利だと思います。
では、まずカテゴリをつくります。
例えば、「カレー大好き!」というサイトがあったとしたら、以下のように「カレー大好き!のお知らせ」というカテゴリをつくります。
カテゴリを作成したら、カテゴリ ID
を取得します。
ID
は編集ページのURL
に含まれています。
※ Rest API
を使えばslug
で指定することもできますが、これまた複雑になってしまうので、ID取得する形で紹介します。
後は、Rest API
にアクセスする部分にパラメータを追加してあげるだけでOKです。
app/Http/Controllers/HeadlessCmsController.php
// 省略
$response = Http::withHeaders(['Authorization' => $authorization])
->withOptions(['verify' => $should_verify])
->get($url, [
'_fields' => 'id,title,content',
'page' => $page,
'per_page' => $per_page,
'orderby' => 'date',
'order' => 'desc',
'categories' => '3,4' //
ここ(複数の場合はカンマ区切り)
]);
同様に他のサイトにもカテゴリをつくってID
をセットすれば複数サイトへ対応ができますよ
テストしてみる
では、実際にテストしてみましょう
まずはいくつかテストのお知らせを投稿しておきます。
続いて、Laravel
側のURL
「https://******/announcement」にアクセスしてください。
すると・・・・・・
はい
今回はページ数を2件にしているのでちょっと少ないですが、ページ移動リンクも表示されています。
では、一番上のリンクをクリックしてみましょう。
どうなるでしょうか・・・・・・??
はい
うまく中身が表示されました。
では、「戻る」ボタンで戻って「次へ」ボタンをクリックしてみましょう。
はい
リンクの内容が変わり、さらに「前へ」リンクも有効になりました。
成功です
では、おそらく画像やYouTube
動画を使いたい場合もあると思うので、うまく表示できるかをチェックしてみます。
うまくいくでしょうか・・・・・・??
はい
画像も動画もうまく表示されました。
すべて成功です
実際の使用例
私が個人的に運営している「街角コレクション」の「お知らせ」は今回の技術で作成しています。
興味のある方はぜひご覧ください
企業様へのご提案
もちろん、実際にウェブサイトで公開されているサービスと比べるとできることは少ないかもしれませんが、シンプルに「タイトル」と「本文」だけでOKな場合は、十分対応できると思います。
また、「ヘッドレス CMS」サービスに必要な固定費をなくすことができますし、すでにご利用中のサーバーにインストールすることもできます。
そして、複数サイトへ対応できますし、なによりWordPress
が使い慣れている場合、新しい勉強をせずに使いはじめることができます。
ぜひ今回の機能をご希望でしたら、お問い合わせからご依頼ください。
お待ちしております。
WordPress をヘッドレス CMS 化するプラグイン
せっかくなので、今回のコードを元にしてシンプルなプラグイン「ヘッドレス CMSize」をつくってみました。
こんなカンジです。
↓↓↓
以下からダウンロードできます。
WordPress をヘッドレス CMS 化するプラグイン概要
- 無料で使えます(もし良かったら代わりに Twitter フォローお願いします
)
- WordPress 5.9(PHP 7.4) で開発しました
- ライセンスは WordPress に合わせて GPLv2 (or later) です
インストール方法
ダウンロード&展開し、wp-content/plugins/
にセットし(※)、プラグインページから「ヘッドレス CMSize」を有効にするだけです。
※ wp-content/plugins/headless-cmsize/headless-cmsize.php
が存在するように設置してください。
おわりに
ということで、今回は「Laravel + WordPress」で「ヘッドレスCMS」をつくってみました。
実装するには、「WordPress」「Laravel」「Vue」の知識が必要になるので、少し学習するエリアは広いですが、それほど難しい内容ではないと思いますので、ぜひ試してみてくださいね。
ではでは〜
「また当初のボリュームを
大きく超えてしまいました
グッタリ…(笑)」