Headless CMSと言えば、クラウドならContentfulやmicroCMS、オンプレミスならGhostやStrapiなどを想像する人が多いでしょう。

VCbornの公式サイトも以前はGhostを使っていたのですが、カスタマイズ性があまり高くないことや日本語入力が時折おかしくなるなどの問題があり、他のものを探すことにしました。

目次
    1. 選定
      1. Strapi
      2. Tina
      3. Payload CMS
    2. Directus
      1. 良いところ
      2. 悪いところ
    3. 導入
      1. セットアップ
      2. コレクションの作成
      3. アイテムの作成
      4. 権限設定
    4. Next.jsで取得
    5. まとめ

選定

Check out this showcase of some of the best, open source headless CMSes. This is…
jamstack.org

Headless CMSの選定にはJamstackのページを参考にしました。

Strapi

Ghostを導入する前に一度試しました。悪くはないのですが、localとproductionで一々編集してアップしてをするのは面倒だったので除外しました。

Tina

基本ローカルでもできるそうですが、production環境ではTina Cloudを使用しないといけないそうです。

Payload CMS

UIが超絶シンプルだったので候補になりましたが、対応DBがmongoDBのみでした。

Directus

A powerful CMS, BaaS, and more. Power any project with Directus – a composable d…
directus.io

APIベースのデータ管理プラットフォームです。

クラウド版のDirectus Cloudもありますが、GPL-3.0で提供されているセルフホスト版もあります。

良いところ

  • セルフホストできる
  • MySQL(MariaDB)で使える
  • UIがシンプルでカッコいい
  • リポジトリの更新頻度が高い
  • 詳細な権限設定が可能

悪いところ

  • ドキュメントが分かりづらい
  • 日本語記事がほぼない
  • クラウド版の一部機能が使えない

導入

セットアップ

LTSのNode.jsが入っている環境で行ってください。

まずdirectusのプロジェクトを作成します。

npm init directus-project example-project

MySQL/MariaDBが入っている環境の場合はアドレスやポート番号、ユーザー名・パスワード、データベース名を聞いてくるので、それらを入力します。

管理者ユーザーは適当なユーザーにしましょう。

Create your first admin user:
? Email: [email protected]
? Password: ********

一通り終わるとプロジェクトが生成されるので、npx directus startでサーバーを開始します。

デフォルトのポートは8055です。

コレクションの作成

初期状態ではまだ何も作成されていません。

Settings > Data Modelから追加を選択し、欲しい形のモデルを作成します。

例えば、VCbornのニュースだとこのようになっています。

各項目の値はこのようにしています。

id記事のパーマリンク
title記事のタイトル
thumbnail記事のサムネイル
date_created投稿日
date_updated更新日
user_created作成したユーザー
draft下書きかどうか
content内容

各フィールドは様々な物から選ぶことができます。

アイテムの作成

コレクションを作成し終えたら、それを元にアイテムを作ります。

権限設定

このままでは認証していないクライアントで画像を表示することができないので、権限をいじる必要があります。

Settings > Roles & Permissionsを開き、PublicのSystem CollectionsからDirectus Filesの読み取りのみ許可してやります。

Next.jsで取得

データの作成はできたので、今度は実際にデータを取得して表示してみましょう。

公式のNext.js用Exampleがあるのでそれを参考にして作っていきます。

Integration Examples with Directus. Contribute to directus-labs/examples develop…
github.com

Directus SDKを既存のプロジェクトに入れておきます。

npm i @directus/sdk

認証用にトークンも生成しておきます。

まずは認証済みのDirectusクライアントを作成します。

import { Directus } from "@directus/sdk"
import getConfig from "next/config"
const { publicRuntimeConfig, serverRuntimeConfig } = getConfig();
const { url } = publicRuntimeConfig;
const { email, password, token } = serverRuntimeConfig;
type News = {
  id: string
  title: string
  thumbnail: string
  date_created: Date
  date_updated: Date
  user_created: string
  draft: Boolean
  content: string
}
type Collections = {
  news: News
};
const directus = new Directus<Collections>(url);
export async function getDirectusClient() {
  if (email && password) {
    await directus.auth.login({ email, password });
  } else if (token) {
    await directus.auth.static(token);
  }
  return directus;
}

next.config.jsに環境変数の設定を追加します。

const moduleExports = {
  ...
  publicRuntimeConfig: {
    url: process.env.DIRECTUS_URL,
  },
  serverRuntimeConfig: {
    token: process.env.DIRECTUS_STATIC_TOKEN,
  },
}
module.exports = moduleExports

.envにも環境変数を追加しておきます。

DIRECTUS_URL=http://localhost:8055
DIRECTUS_STATIC_TOKEN=<生成したトークン>

最後にgetDirectusClientでクライアントを呼び出し、queryすれば完成!

import { format } from 'date-fns'
import Image from 'next/image'
import Link from 'next/link'
import { getDirectusClient } from '@/lib/directus'
const Home = ({news}) => {
  return (
    <div>
      {news.map((item, index) => {
            return (
                <article key={item.id}>
                  <Link
                    className='mx-auto flex flex-col gap-8 md:flex-row md:items-center md:mx-0 duration-200 hover:bg-gray-100'
                    href={`/news/${item.id}`}
                  >
                    <div>
                      <Image
                        alt={item.id}
                        src={`http://localhost:8055/assets/${item.thumbnail}`}
                        width={360}
                        height={180}
                      />
                    </div>
                    <div>
                      <time className='font-semibold text-xl'>
                        {format(new Date(item.date_created), 'yyyy.MM.dd')}
                      </time>
                      <h3 className='font-semibold text-3xl'>{item.title}</h3>
                    </div>
                  </Link>
                </article>
            )
          })}
    </div>
  )
}
export const getServerSideProps: GetServerSideProps = async (context) => {
  const directus = await getDirectusClient()
  const res = await directus.items('news').readByQuery({
    sort: ['-date_created'],
    filter: {
      draft: false,
    },
  })
  const news = res.data
  return {
    props: { news },
  }
}

まとめ

大分気に入ったのでしばらくDirectusを使っていきます。

ただ公式のSDKのドキュメントが大雑把で少しわかりづらかったです。