Grav CMS の Flex Objects でマイクロブログを作る
前回の記事 では、 Grav の開発環境を作ってみました。
今回は、その開発環境で、 Flex Objects というデータ型を作ってみます。
Flex Objects とは
Grav 1.7 以降で使える、大量のデータを管理する仕組みです。
各データには、データ型の制約がつけられます。
WordPress でいうところのカスタムフィールド、 Astro でいうところの Content collection、 Hugo でいうところの Data source に近い仕組みです。
Flex Objects の利点のひとつとして、 blueprint という YAML 設定ファイルを書くだけで、管理パネルから CRUD 操作できるようになります。
セットアップ
セットアップ方法は、以前の記事で紹介したとおりです。
環境によって、多少違うかもしれませんが、おおむね、以下のようになるはずです:
cd ~/your/develop/directory/
wget -O mygrav.zip https://getgrav.org/download/core/grav/latest
unzip mygrav.zip
rm mygrav.zip
mv grav mydevelop
cd ./mydevelop/
php bin/gpm version # 2.0 以上を確認
php bin/grav server # 開発サーバーが動くことを確認
mkdir -p ./user/env/localhost/config/
touch ./user/env/localhost/config/system.yaml
vi ./user/env/localhost/config/system.yaml # 開発用 system.yaml 設定
php bin/gpm install admin2
php bin/plugin login new-user # スーパーユーザー作成
php bin/gpm install devtools
flex-objects プラグインのインストール
さらに今回は、 Flex Objects を Admin2 プラグインで動かせるようにする flex-objects プラグインをインストールします。
このプラグインは、本番環境でも必要になるプラグインです。
php bin/gpm install flex-objects
データ型を設計する
ここまでで、開発環境の準備は整ったので、データ型を考えていきます。
データベースでいうところの、スキーマを設計するような段階です。
今回は、マイクロブログ(つぶやきブログ)を作ってみたいと思います。
そこで、以下のようなデータ型を考えました。
- 投稿本文(
body) 必須項目、テキストエリアで書きたい - 投稿日時(
time) 必須項目 - 添付画像(
images) 任意項目、4枚くらいアップロードしたい
つまり、この時点では、以下のような json で管理したいなと想像しています。
(後ほど実際のデータをお見せしますが、実際には Flex Objects の仕様で管理されることになり、以下の形式とは異なります。しかし、スキーマ設計の段階では、これくらいの見通しがあれば十分です)
{
"body": "つぶやき本文 ... 。",
"time": "2026-07-21 12:34:56",
"images": [
"/path/to/image1.jpg",
"/path/to/image2.jpg",
"/path/to/image3.jpg",
"/path/to/image4.jpg"
]
}
カスタム Flex Objects プラグインを作成する
上記で考えたスキーマを実現するためのプラグインを作成しましょう:
php bin/plugin devtools new-plugin
すると、この記事の執筆時点(2026年7月)では、以下のような内容を聞かれます:
- プラグイン名(
microblogにしました) - プラグインの説明
- 開発者の名前
- GitHub の ID
- 開発者の Eメール
- 対応する Grav のバージョン(
2.0を選択) - プラグインテンプレートの種類(
flexを選択) - Flex Object の名前(これも
microblogにしました) - Flex Objects のストレージの種類(
folderにしました)
その後、 composer install するか聞かれるので、 yes を回答します。
もし忘れた場合は、そのディレクトリまで移動し、 composer install を実行します。
cd ./user/plugins/microblog
composer install
cd ../../../
Blueprint を編集する
先ほど考えたデータ型を、 blueprint に反映させます。
デフォルトで使える入力欄の設定方法は、公式ドキュメントにリファレンスがあります。
先ほど考えた設計を、フォームの設定にすると、おおよそ以下のようになります。
form:
validation: loose
fields:
body:
type: textarea
label: '投稿本文'
validate:
required: true
time:
type: datetime
label: '投稿日時'
validate:
required: true
images:
type: file
label: '添付画像'
multiple: true
destination: 'self@'
limit: 4
accept:
- image/*
これを、先ほど作成したプラグインの flex objects の blueprint に反映させます。
合わせて、諸設定もしておきます。
管理パネルのサイドバーに表示されるアイコンを編集:
config:
admin:
menu:
list:
icon: fa-comment
パーミッションを設定:
config:
admin:
permissions:
api.microblog:
type: crudl
リスト表示での表示内容を設定:
config:
admin:
list:
title: name
fields:
body:
time:
管理パネルでの検索に投稿本文を追加:
config:
data:
search:
fields:
- body
先ほど考えたデータ型を定義:
form:
validation: loose
fields:
body:
type: textarea
label: '投稿本文'
validate:
required: true
time:
type: datetime
label: '投稿日時'
validate:
required: true
images:
type: file
label: '添付画像'
multiple: true
destination: 'self@'
limit: 4
accept:
- image/*
ここまでで、最低限の Flex Objects ができています。
もし、ユーザーのパーミッションのスーパーユーザーを false にしていた場合は、下記のように、アカウントにパーミッションを与えてください。
今回は、開発環境なので、スーパーユーザーのままでも全く問題ありません。
将来的に、本番環境では、下記のパーミッション設定が必要になります。
access:
api:
super: false
access: true
microblog:
list: true
read: true
create: true
update: true
delete: true
開発サーバーを立ち上げて、 https://127.0.0.1:8000/admin/ にアクセスし、ログインしてください。
php bin/grav server
右側に、 Microblog が現れ、入力画面が表示されます。

+ Add ボタンで、データが作成できます。
とりあえず、ひとつ、つぶやいてみます。
すると、たとえば、以下のようなファイルが作成されるはずです。
{"body":"\u30c6\u30b9\u30c8\u672c\u6587\u3002\n\u30c6\u30b9\u30c8\u672c\u6587\u3002","time":"21-07-2026 12:34","images":{"image1.jpg":{"name":"image1.jpg","type":"image\/jpeg","size":72707,"path":"image1.jpg"}}}
ここまでで、Flex Object の CRUD 操作(フォルダの作成・編集・削除、及び一覧表示)ができるようになっているはずです。
入力フィールドの改善
とりあえず、管理パネルでの CRUD 操作ができるようになりました。
しかし、デフォルトのままでは、使いにくいところもあります。
少し、改修してみましょう。
日付をフォーマットしたい
初期設定では、 datetime のフォーマットが、 mm-dd-yyyy HH:MM です。しかし、これは日本ではあまり馴染みのないフォーマットですし、1分間に2つ以上つぶやくことがあるかもしれないので、 yyyy-mm-ddTHH:MM:SS に変更したいです。
Grav 2.0 で、管理パネルでの入力フォームが作りやすくなりました。
JavaScript のカスタムエレメントを作るだけです。
具体的には、以下のようなファイルを作り、その中に js ファイルを作成します:
mkdir -p ./user/plugins/microblog/admin-next/fields/
touch ./user/plugins/microblog/admin-next/fields/tweetdatetime.js
const TAG = window.__GRAV_FIELD_TAG;
customElements.define(TAG, class extends HTMLElement {
set field(f) { this._field = f; this._render(); }
set value(v) { this._value = v; this._render(); }
get value() { return this._value; }
#dateInput = document.createElement('input');
#timeInput = document.createElement('input');
#nowButton = document.createElement('button');
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.#dateInput.type = 'date';
this.#timeInput.type = 'time';
this.#timeInput.step = '1';
this.#nowButton.type = 'button';
this.#nowButton.textContent = 'Now';
this.shadowRoot.appendChild(this.#dateInput);
this.shadowRoot.appendChild(this.#timeInput);
this.shadowRoot.appendChild(this.#nowButton);
this._render();
this.#dateInput.addEventListener('change', ()=>{
this.value = `${this.#dateInput.value}T${this.#timeInput.value}`;
this._emitChange(this.value);
});
this.#timeInput.addEventListener('change', ()=>{
this.value = `${this.#dateInput.value}T${this.#timeInput.value}`;
this._emitChange(this.value);
});
this.#nowButton.addEventListener('click', ()=>{
this.value = this.#formatDate(new Date());
this._emitChange(this.value);
});
}
#formatDate(dateObj) {
const year = dateObj.getFullYear();
const month = String(dateObj.getMonth()+1).padStart(2, '0');
const date = String(dateObj.getDate()).padStart(2, '0');
const hour = String(dateObj.getHours()).padStart(2, '0');
const minute = String(dateObj.getMinutes()).padStart(2, '0');
const second = String(dateObj.getSeconds()).padStart(2, '0');
return `${year}-${month}-${date}T${hour}:${minute}:${second}`;
}
#validDate(dateString) {
const exp = /[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}/;
return exp.test(dateString);
}
#splitDate(dateString) {
const empty = ['', ''];
if(typeof dateString !== 'string') return empty;
const split = dateString.split('T');
return split.length === 2 ? split : empty;
}
_render() {
if (!this.shadowRoot) return;
const [dateString, timeString] = this.#splitDate(this.value);
this.#dateInput.value = dateString;
this.#timeInput.value = timeString;
}
_emitChange(newValue) {
if(!this.#validDate(newValue)) {
newValue = '';
}
this.dispatchEvent(new CustomEvent('change', {
detail: newValue,
bubbles: true
}));
}
});
具体的な書き方は、 公式ドキュメント を参照してください。
処理だけ AI に書いてもらって、 style を自分で当てるなどでも十分動作すると思います。
作成したフォーム入力欄を、管理画面に対応させるには、 Flex Objects の blueprint を編集します:
form:
fields:
time:
type: tweetdatetime
この状態で、開発サーバーから管理パネルを見ると、フォーマットが変わっているはずです。
さらに、保存してみると、期待したフォーマットで保存されていることが分かります:
{"body":"\u30c6\u30b9\u30c8\u672c\u6587\u3002","time":"2026-07-21T12:34:56"}
作成したデータをフロントエンドに表示する
管理パネル側の操作ができたので、次に、フロントエンド(サイトの公開側)を作っていきます。
ページを作成
ルーティングの起点になるページを作成します:
mkdir ./user/pages/03.microblog/
touch ./user/pages/03.microblog/microblog.md
---
title: 'つぶやきブログ'
flex:
directory: microblog
---
マイクロブログ用のテンプレートを作成
各テンプレートを作っていきます。
mkdir -p ./user/plugins/microblog/templates/
touch ./user/plugins/microblog/templates/microblog.html.twig
mkdir -p ./user/plugins/microblog/templates/flex/microblog/collection/
touch ./user/plugins/microblog/templates/flex/microblog/collection/default.html.twig
mkdir -p ./user/plugins/microblog/templates/flex/microblog/object/
touch ./user/plugins/microblog/templates/flex/microblog/object/default.html.twig
<!DOCTYPE html>
<html lang="ja">
<head><title>{{ page.title }}</title></head>
<body>
<h1>{{ page.title }}</h1>
{% render tweets layout: 'default' %}
{% if tweets_meta.hasPrev %}
<a href="?p={{ tweets_meta.prevPage }}">前のページ</a>
{% endif %}
{% if tweets_meta.hasNext %}
<a href="?p={{ tweets_meta.nextPage }}">次のページ</a>
{% endif %}
</body>
</html>
<div>
{% for object in collection %}
<div>
{% render object layout: default %}
</div>
{% endfor %}
</div>
<p style="white-space: pre-wrap;">{{ object.body }}</p>
<p><time>{{ object.time }}</time></p>
{% if object.images %}
{% set folder = object.getMediaFolder() %}
{% for image in object.images %}
{% set url = '/' ~ folder ~ '/' ~ image.name %}
<a href="{{ url }}">
<img style="width:150px;height:150px;object-fit:cover;object-position:center"
src="{{ url }}" alt="{{ image.name }}"/>
</a>
{% endfor %}
{% endif %}
これらのテンプレートを読み込ませるために、 php ファイルでは、以下のように登録します:
public function onPluginsInitialized(): void
{
$this->enable([
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
]);
}
public function onTwigTemplatePaths(): void
{
$twig = $this->grav['twig'];
$twig->twig_paths[] = __DIR__ . '/templates';
}
PHP で処理をして Twig 変数に代入
Twig 変数への代入などは、以前紹介した onTwigSiteVariables イベントで制御できます。
イベントを登録したうえで、たとえば、以下のように書けます:
public function onTwigSiteVariables(): void
{
$page = $this->grav['page'];
if($page->template() !== 'microblog') {
return;
}
// flex collection を取得
$tweets = $this->grav->get('flex')->getCollection('microblog');
$tweets = $tweets->sort(['time' => 'DESC']);
// 各サービスを取得
$twig = $this->grav['twig'];
$uri = $this->grav['uri'];
// ページネーション
$limit = 3;
$max_count = count($tweets);
$max_page = intdiv($max_count + $limit - 1, $limit);
$current_page = filter_var($uri->query('p'), FILTER_VALIDATE_INT);
if($current_page === false || $current_page < 1 || $max_page < $current_page) {
$current_page = 1;
}
$start = ($current_page - 1) * $limit;
$tweets = $tweets->limit($start, $limit);
$twig->twig_vars['tweets'] = $tweets;
$twig->twig_vars['tweets_meta'] = [
'hasPrev' => 1 < $current_page,
'prevPage' => $current_page - 1,
'hasNext' => $current_page < $max_page,
'nextPage' => $current_page + 1,
];
}
開発サーバーを立ち上げて、 /microblog?p=2 などを表示すると、ページネーション付きのマイクロブログが出てくるはずです。
まとめ
今回は、 Grav の Flex Objects 機能を使って、データ操作(CRUD)からフロントエンド表示までを一気に紹介しました。
CRUD 操作が blueprint の設定だけでできるところも便利ですし、 PHP 部分を追記していけば、検索やハッシュタグなどの追加実装も、簡単にできるはずです。
とても便利な機能だと思うので、ぜひ使ってみてください。
Discussion