a-blog cms をヘッドレスCMSとして Next.js でサイト制作するときのポイント

公開日:

目次

このエントリーは、 a-blog cms Advent Calendar 2023 19日目 の記事です。

本日、個人ブログを、a-blog cms × Next.js の構成でリニューアルしました。

このエントリーでは、a-blog cmsをヘッドレスCMSとして活用し、Next.js でテンプレートを作ってみての良かったこと、難しかったことについてまとめています。

ちなみに、ヘッドレスCMSとしての a-blog cms については、2019年のアドベントカレンダーにてジェノベーゼ寺崎さんが「Headless CMSとしてのa-blog cms」という記事を投稿していただいています。

この記事の投稿から今4年たった今、a-blog cms をヘッドレスCMSとしてどのように活用できるかというところを紹介できたらとおもいます。

ヘッドレスCMSとは

ヘッドレスCMSとは、フロントエンドの表示機能を持たず、APIを通じてコンテンツを提供するCMSです。データをJSON形式で出力し、テンプレートは開発者が自由に選択して実装します。例えば、microCMS, Kuroco, Contentfulなどの製品が日本ではヘッドレスCMSとして有名です。

フロントエンドの表示機能がないため、通常であれば、a-blog cms のテンプレートエンジンを用いてWebサイトのテンプレートを制作しているところを、Web制作者がプログラミングを行うことでテンプレートを制作する必要があります。

Next.js とは

Next.js とは React というJavaScriptライブラリを用いてWebサイトやWebアプリケーションを開発するためのフレームワークです。

ヘッドレスCMSを用いてのWebサイト制作においては、React を用いてテンプレートを制作するために利用されます。

Next.js を利用することにより、以下のメリットを享受することができます。

  • 静的サイトとしてWebサイトを制作できる
  • ルーティング機能が標準で備わっている
  • 画像を自動で最適化できる

特に1つめの、"静的サイトとしてWebサイトを制作できる" というところは、ヘッドレスCMSを活用するにあたって大きな魅力となります。

静的サイトにすることによって、PHPが動作しないことになるため、セキュリティ的に安全なWebサイトを制作することができます。また、静的サイトなので、表示速度はかなり爆速です。

a-blog cms をヘッドレスCMSとして利用するためのポイント

ここからは、a-blog cms をヘッドレスCMSとして利用するために直面した課題や、解決方法を紹介していきます。

Webhook機能でエントリー更新時にビルドする

Next.js を利用して静的サイトを作成する場合、通常ではエントリーを作成・更新してもWebサイトが更新されることはありません。

というのも、静的サイトは、"ビルド" と呼ばれる工程によって CMS からデータを取得し静的サイトを生成しているためです。つまり、a-blog cms でエントリーを作成・更新したときにWebサイトを更新するためには一度ビルドを行う必要があります。

a-blog cms にはそのための機能が標準機能として Ver. 3.0 から Webhook機能として利用できるようになっています。

Webhook機能を利用することで、外部サービスと連携し、エントリーを作成・更新したときに自動でビルドするように設定することができます。

本サイトは Vercel でホスティングをしているため、Deploy Hooks 機能を活用して生成したURLを Webhook URL として設定した Webhook を作成すればOKです。

VercelのDeploy Hooks機能を活用したWebhook機能の管理画面

VercelのDeploy Hooks機能を活用したWebhook機能の管理画面

簡単ですね!このように、a-blog cms はヘッドレスCMSとして利用するために必要不可欠な Webhook 機能を標準機能として搭載しています。

CMSサーバーはメンテナンスモードにすべし

a-blog cms はクラウド型のサービスではないため、a-blog cms をインストールして動作させるサーバーが必要になります。しかし、CMSが動作しているサーバーに一般ユーザーがアクセスした場合にはCMSで管理している情報を表示させないようにしたいですよね。

そういったときに活用できるのがメンテナンスモード機能です。管理画面 > ダッシュボードから「メンテナンスを開始」ボタンをクリックするだけで、ログインしている管理者以外のアクセス時にはメンテナンスページを表示することができます。

これにより、一般ユーザーがCMSが動作しているサーバーにアクセスした場合でも、CMSで管理している情報を非公開にすることができます。

また、HTTPステータスを503に設定することで、検索にインデックスされることがなくなるため、検索からCMSサーバーがバレてしまうといったこともなくなります。

一方で、シークレットブログ機能でも良いのではないかと思う方もいるかも知れません。シークレットブログ機能を用いたときのデメリットとしてカテゴリーやタグなど、エントリー以外の情報が取得できない点が挙げられます。

Category_List モジュールやTag_Cloudモジュールには「ログインしていなくてもシークレットブログ・カテゴリーのデータを表示する」という表示設定がモジュールID設定に存在しないためです。

この仕様による、データが取得できない現状の原因解明にかなりハマりました。。

画像配信専用ドメインを作って、CMSサーバーを隠す

ヘッドレスCMSを利用するメリットの一つとして、"CMSサーバーを隠蔽できる" という点が挙げられます。

ヘッドレスCMSにより、バックエンドであるCMSサーバーとフロントエンドである静的HTMLを配信するサーバーを分離することで、攻撃者からCMSサーバーのドメインを隠すことができるため、悪意のある攻撃自体を未然に防ぐことができます。

CMSで管理しているデータの取得において、Next.jsなどのSSG(静的サイト生成)機能を利用してテンプレートを作成していれば、CMSサーバーへのアクセスはサーバー側で行われるため、CMSサーバーのドメインがバレることはありません。

しかし、画像などのアセット類においてはバックエンドのドメインがついたURLで取得することになるため、CMSサーバーのドメインを隠すためには一工夫する必要があります。

例えば、以下のようなURLです。

https://backend.example.com/media/001/202309/sample.png

対策方法としては、色々あるかと思いますが、a-blog cms をヘッドレスCMSとして利用する場合に最も簡単なのは、画像配信専用ドメインを作成する方法だと思います。

今回、a-blog cms は Xserver に設置していたため、assets.example.com のような画像配信専用のサブドメインを作成し、そちらから画像などのアセット類を配信するようにしています。

public_html
├ assets
│ ├ archives
│ ├ media
│ └ storage
└ backend
  ├ archives
  ├ media
  ├ storage
  └ index.php

assets ディレクトリ内の archives, media, storage ディレクトリは、CMSを動作させている backend ディレクトリからシンボリックリンクさせています。

このような工夫をすることで、画像などのアセット類においてもバックエンドのドメインを利用せず配信することができます。

最後に a-blog cms を設置するサブドメインを複雑な文字列にすれば、CMSを動作させているサーバーをできる限り隠蔽することができます。

組み込みJSを利用する

Next.js で1からテンプレートを作成するとなると、a-blog cms の便利機能である組み込みJSが使えません。

しかし、SmartPhoto や ScrollHint など、一部の組み込みJSは利用したかったため、npm でインストールして利用しています。以下のように、develop テーマを参考に必要な組み込みJSだけを起動する React コンポーネントを作成してすべてのページで読み込んでいます。

// app/components/BuildInJs/BuildInJs.tsx
'use client';

import {
  usePathname,
  useSearchParams,
} from 'next/navigation';
import {
  documentOutliner,
  externalLinks,
  openStreetMap,
  scrollHint,
  smartPhoto,
} from '@/app/lib/buildIn';
import { Suspense, useEffect } from 'react';

function BuildInJs() {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    if (typeof window !== 'undefined') {
      ((context: Document | Element) => {
        externalLinks(context);
        smartPhoto(context);
        scrollHint(context);
        openStreetMap(context);
        documentOutliner(context);
      })(document);
    }
  }, [pathname, searchParams]);
  return null;
}

export default function BuildInJsSuspense() {
  return (
    <Suspense fallback={null}>
      <BuildInJs />
    </Suspense>
  );
}

このようにすることで、組み込みJSのライブラリが React に対応していない場合でも、組み込みJSの機能をサイト全体で利用することができます。この方法であれば、他にもGoogleMap や modalVideo などの組み込みJSも手軽に利用することが可能です。

ユニットグループやユニットの配置機能は利用しない

本サイトでは、ユニットグループやユニットの配置機能は利用しておりません。

というのも、ユニットグループやユニットの配置機能に必要なCSSを1から書くのが大変だったためです。これらのCSSは float を活用する必要があります。本サイトのスタイルは Flowbite を利用しており、Flowbite のスタイルとユニットグループやユニットの配置機能に必要なCSSを共存させることが難しそうでした。

個人ブログなので凝ったレイアウトは必要ないという判断で、ユニットグループやユニットの配置機能は利用しないことにしました。ただ、acms.css をコピペしたCSSを読み込めばできそうな気もするので、今後試してみたいと思います。

ユニット周りのテンプレートを1から React で実装するのにかなり時間がかかったため、今回はユニットグループやユニットの配置機能まで実装することができませんでした。

タグの複数かけ合わせ検索一覧ページにページネーションがつけられない

a-blog cms でタグを複数かけ合わせて検索する一覧ページのURLは以下のようになります。

https://example.com/tag/りんご/ぶどう/ばなな

これにページネーションを組み合わせると以下のようになります。

https://example.com/tag/りんご/ぶどう/ばなな/page/2

Next.jsでも同じURL構造でタグの一覧ページをページネーション機能付き表示しようと実装しようとしたところ、Next.js ではこのようなURLでルーティングすることはできませんでした。

Next.js のルーティング機能で上記のURLを表現すると以下のようになりますが、Catch-all Segment によるルーティングはURLの末尾である必要があるらしくページネーション機能付きタグの一覧ページにて、タグを複数かけ合わせたページの実装を断念しました。(タグ1つであれば可能です。)

https://example.com/tag/[...tag]/page/[page]

page を前に持ってくれば実装は可能でしたが、URLが少し気持ち悪い感じがしたのでやめました。

まとめ

今回、本来ヘッドレスCMS用途でないa-blog cmsを無理やりヘッドレスCMSとして活用しブログサイトを制作しました。

a-blog cms がテーマとして用意してくれているテンプレートを1から制作する必要があるのは結構たいへんでした。(特にユニット周り)

プレビュー機能の実装など、今後改善したい課題もありますので少しずつ改善して聞きたいと思います。

この本サイトのソースコードは GitHub にて公開していますので、詳しい実装を知りたい方は GitHub の方をご覧ください。

明日の a-blog cms Advent Calendar 2023 20日目 は、tamshow_さんの記事です。是非お楽しみに!

おまけ

本サイトを開発した際のの副産物として、a-blog cms の API機能を簡単に扱うためのライブラリができそうです。

const acmsClient = createClient({
   baseUrl: "YOUR_BASE_URL",
   apiKey: "YOUR_API_KEY",
});

// YOUR_BASE_URL/blog/api/summary_index へのHTTPリクエスト結果を取得できる
const { data } = await acmsClient.get(
    { blog: 'blog', api: 'summary_index' },
);

お正月休みで完成できたらいいな。。