WordPress + Laravel で「ヘッドレス CMS」をつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、最近特にTwitterから情報を得ていると最新情報が豊富なので「時代に取り残されにくくなる」ような気がしています。

しかも、優秀な方々のツイートはお金を払ってでもほしいと思えるようなものがあったりもします。(それにしても、ああいう超優秀な方々はホント何者なんでしょうか…😂)

そして、そんな情報収集の中で気になったのが、

ヘッドレス CMS

ヘッドレス CMS」とは、簡単にいうと「コンテンツ部分だけ管理する別サイト」で、例えば「ユーザーへのお知らせ部分だけ管理できるサイト」をイメージすると分かりやすいでしょうか。

そして、そんな「ヘッドレス CMS」はすでに日本版サービスが出てるぐらい浸透してきているのですが、そのときあることがひらめきました。

「これ、WordPressで作れるんじゃない??

と。

そこで❗

今回は、WordPress + Laravel でシンプルな「ヘッドレス CMS」をつくってみます。

ぜひ何かの参考になりましたら嬉しいです。😄✨

「シャウエッセン with
ハインツのケッチャップ
がうまくて仕方がない…」

開発環境: WordPress 5.9.2、Laravel 9.x

やりたいこと

今回実装する内容は次のとおりです。

  • 通常のブログ表示は無効にする
  • 他のサイトからはアクセスできないよう認証をつける
  • 別サイトの「お知らせ」を管理する

つまり、流れとしてはこうなります。

  1. WordPress でお知らせ記事を書く
  2. Laravel 側からデータを取得
  3. 「お知らせ」として表示

では、今回も楽しくやっていきましょう❗

専用テーマをつくる

まず、「通常のブログ表示は無効にする」部分からつくっていきます。

今回はブログ記事は表示しませんので、独自に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は、WordPressHTTPSで接続されていないと有効にできませんので注意してください。

では、これでWordPress側の設定は完了です。
次はLaravelへ行ってみましょう❗

Laravel から Rest API へアクセスしデータ取得する

続いて、Laravelから、WordPressRest 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.');
    }

}

この中では、LaravelHTTP Clientguzzle)を使ってアクセスしているだけですので、シンプルなコードになっているかと思います。

ただ、一点気をつけていただきたいのが、以下の部分です。

->withOptions(['verify' => $should_verify])

これは「サーバー証明書の検証」を実行する or しないを決める部分なのですが、ローカル環境では検証してもうまくいかない(ことのほうが多い)と思いますので、以下の条件で切り替えています。

  • ローカル: サーバー証明書の検証はしない
  • 本番: 検証する

ルートをつくる

次に、ルートです。
以下のように変更してください。

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側のURLhttps://******/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が存在するように設置してください。

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回は「Laravel + WordPress」で「ヘッドレスCMS」をつくってみました。

実装するには、「WordPress」「Laravel」「Vue」の知識が必要になるので、少し学習するエリアは広いですが、それほど難しい内容ではないと思いますので、ぜひ試してみてくださいね。

ではでは〜❗

「また当初のボリュームを
大きく超えてしまいました
グッタリ…(笑)」

このエントリーをはてなブックマークに追加       follow us in feedly