Contents
構成DBにPostgreSQLが使えるCMSを触りたい
現在、私が使用しているこのCMSはWordpressですけれども、WordpressはリポジトリDBとしてMySQLもしくはMariaDBしか選択肢がありません。
しかし、私が自宅環境でよく利用しているRDBMSはPostgreSQLであり、主にMastodon用のDBとして活用しています。
しかしながら、MastodonのDBにだけわざわざHA化してるデータベースを占有させるのもなんだかもったいないなーと思い、CMSを探しておりましたところ、CraftCMSなるものを見つけました。
WordPressと同様にPHPベースで動作するものですが、Wordpressはある程度テーマなどが構成されている上、体裁を整えるのも比較的容易なのかなと思われるのに対して、CraftCMSはなんとまぁかなり玄人志向というか、この手のものに完全ド素人の私には全く不向きな代物でありまして、これを機に少しCSSって何者だろうか?JSって何者だろうか?と言うところを勉強してみようと言うことにした次第であります。
なお、CraftCMS構築の話ではないのでご容赦を。インストール、構成手順はググってしまうと結構簡単に見つかるので自分が書く必要もないのかなーと思っていたり。取り敢えずPHPベースですので、PHP7.4系の環境が構成できれば大丈夫じゃないかなーと思います。
WordPressと違うポイント
WordPressはわりかしすぐに記事が書ける作りになってるんですが、CraftCMSの場合はページの構成要素を自ら作成する必要があったりします。一番最小の単位は「フィールド」であり、それらをまとめた「セクション」を作ることで、それが1つのページとして表現される単位になります。
今回私が作った体裁はブログに近いものにしたので以下の通りですが、例えば全く異なるレイアウトにすることも可能だったりします。
セクションではそうしたフィールドの配置からタブ構成、実際どういうURIにマッピングさせるのか、テンプレートは何を選ぶのかを選択することが出来ます。

鬼門であるテンプレート
さて、ここで大抵はささっと準備できるテンプレートとかどっかしらから提供されるもんですが、なんとCraftCMSはテンプレートがはじめの状態からは備わっていません(´;ω;`)ブワッ
理由は恐らく「ハンドル」にあるんだと思われます。フィールドなどの構成要素を配置する際、このハンドルを指定する必要があるんですが、このハンドルの名称はウェブサイトを作る人それぞれで定義の仕方があり、そこに順応させるのが難しいからかなぁ?とか思いましたが、実際はどうなのでしょうかね・・
今回、分からないなりにテンプレートを作ってみることにしました。
参考にしたのはCraftCMSの以下のドキュメントです。
ここのBuild Your Front Endでその作り方が記載されています。英語ドキュメントですが、Deeplのような翻訳サービスを使ってじっくり読むと、おぼろげながらイメージは出来るのではないかなと思われます。
・・・とか思ってたら、どうやらスターターキットなるものがあったようです。ぬぬっ、まさかそんな便利なものがあったとは・・
ファイル配置
以下のような構成を取っています。

_layout.twig
全体のレイアウトを決めるファイルが_layout.twigです。
スタイルシートってどう定義するの?ってのが当初良く分かってなかったので、チュートリアルのドキュメントに加え、今回以下のものを使用しています。
これは元々初心者向けCSS/JSとして提供されているBootstrapを日本語環境に最適化したもののようで、ここにあるクラスを使用することにより最低限の体裁は取れるようになりました。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>dic.bluecore.net</title> <link rel="stylesheet" type="text/css" href="https://dic.bluecore.net/css/bootstrap.css"> <link href="https://fonts.googleapis.com/css2?family=M+PLUS+1p&display=swap" rel="stylesheet"> <style type="text/css"> .bs-component + .bs-component { margin-top: 1rem; } @media (min-width: 768px) { .bs-docs-section { margin-top: 8em; font-family: 'M PLUS 1p', sans-serif; } .bs-component { position: relative; } .bs-component .modal { position: relative; top: auto; right: auto; bottom: auto; left: auto; z-index: 1; display: block; } .bs-component .modal-dialog { width: 90%; } .bs-component .popover { position: relative; display: inline-block; width: 220px; margin: 20px; } .nav-tabs { margin-bottom: 15px; } .progress { margin-bottom: 10px; } .container { font-family: 'M PLUS 1p', sans-serif; } .jumbotron { background: url(https://dic.bluecore.net/assets/blog/background-title-dic-20201110a.png) center no-repeat; background-size: cover; opacity: 0.9; padding-top: 30px; padding-bottom: 30px; margin-bottom: 30px; color: #ffffff; background-color: #000000; } .jumbotron.container { color:#FFF;} .jumbotron h1, .jumbotron .h1 { color: inherit; } .jumbotron p { margin-bottom: 15px; font-size: 24px; font-weight: 200; } .jumbotron > hr { border-top-color: #cfd9db; } .container .jumbotron, .container-fluid .jumbotron { border-radius: 4px; } .jumbotron .container { max-width: 100%; } @media screen and (min-width: 768px) { .jumbotron { padding-top: 48px; padding-bottom: 48px; } .container .jumbotron, .container-fluid .jumbotron { padding-left: 60px; padding-right: 60px; } .jumbotron h1, .jumbotron .h1 { font-size: 72px; } } h1 { border-bottom: 3px solid #000; } h2 { border-bottom: 3px dotted #000; } } </style> </head> <body> {% include "_includes/nav" %} <div class="container"> <div class="page-header" id="banner"> <div class="row my-4"> <div class="col-12"> {% block content %} {% endblock %} </div> <footer class="container mx-auto mt-8 p-4 text-sm opacity-50"> {{ siteInformation.siteDescription|markdown }} <p>© {{ now | date('Y') }}, <a class="text-blue-600" href="https://www.bluecore.net">BLUECORE.NET</a>, built with <a class="text-blue-600" href="https://craftcms.com">Craft CMS</a></p> </footer> </div> </div> </div> <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> <script src="https://dic.bluecore.net/js/bootstrap.min.js"></script> <script type="text/javascript"> $('.bs-component [data-toggle="popover"]').popover(); $('.bs-component [data-toggle="tooltip"]').tooltip(); </script> </body> </html>
_includes/listing.twig
_includeディレクトリは色々なページテンプレートで使い回されるモジュールを格納しています。
ブログ一覧を表示させるモジュールになります。DefaultCardと言うものを使い、四角い枠毎にタイトル、featureImageを表示するようにしています。
記事が増えるにつれてページ切り替えが必要になってくるので、そのあたりはCraftCMSに備わるPaginate機能をうまく使いこなせるようになれば・・と言うことで現在勉強中です。

<div class="row"> {% for post in posts %} <div class="col-xs-6 col-lg-4"> <a href="{{ post.url }}"> {% if post.featureImage|length %} {% set image = post.featureImage.one() %} {% set cate = post.postCategories.one() %} <div class="card mb-3 style="max-width: 20rem;"> <div class="card-header">{{ cate.title }}</div> <div class="card-body"> <h4 class="card-title">{{ post.title }}</h4> <p class="card-text" align="center"> <img class="block" src="{{ image.getUrl({ width: 150, height: 150}) }}" alt="{{ image.title }}"> </p> </div> </div> {% endif %} </a> </div> {% endfor %} </div>
レイアウト上部に表示させるナビゲーションバーの定義です。ページ上端の青い部分になっています。

<header> <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <div class="container"> <a class="navbar-brand" href="./"> <strong>dic.bluecore.net</strong> </a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbar"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <b><a class="nav-link" href="{{ siteUrl }}">Top <span class="sr-only">(current)</span></a></b> </li> <li class="nav-item"> <b><a class="nav-link" href="{{ url('blog') }}">Blogs</a></b> </li> <li class="nav-item"> <b><a class="nav-link" href="https://interact.bluecore.net">Social Networking Service</a></b> </li> <li class="nav-item"> <b><a class="nav-link" href="https://www.bluecore.net">Primary Blog site</a></b> </li> </ul> </div> </div> </nav> </header>
_includes/post-blocks.twig
記事部分そのものを表示する箇所になります。
<div class="my-8"> {% for block in blocks %} <div class="my-4"> {% if block.type == 'richText' %} {{ block.richText|raw }} {% elseif block.type == 'image' %} {% for image in block.image.all() %} <img src="{{ image.url }}" alt="{{ image.title }}" /> {% endfor %} {% endif %} </div> {% endfor %} </div>
blog/index.twig
記事一覧の全体を表示する部分であり、URLで「https://<fqdn>/blog 」を指定したときに表示される箇所にもなります。

{% extends "_layout" %} {% set posts = craft.entries.section('blog').all() %} {% block content %} <h1 class="text-4xl text-black font-display my-4">Blog Posts</h1> {% include "_includes/listing" with { posts: posts } only %} {% endblock %}
blog/_category.twig
カテゴリ毎の記事一覧を表示する部分になります。

{% extends "_layout" %} {% set posts = craft.entries.section('blog').relatedTo(category).all() %} {% block content %} <h1 class="text-4xl text-black font-display my-4"> Blog Posts in “{{ category.title }}” </h1> {% include "_includes/listing" with { posts: posts } only %} {% endblock %}
blog/_entry.twig
各記事全体のフォーマットになります。

{% extends "_layout" %} {# create settings for image transform #} {% set featureImage = { mode: 'crop', width: 1024, height: 200, quality: 80 } %} {% block content %} <div class="row> <div class="col-lg-4"> <div class="bs-component"> <h1 class="">{{ entry.title }}</h1> <P> <time class="text-sm block pb-4" datetime="{{ entry.postDate | date('Y-m-d') }}">{{ entry.postDate | date('d M Y') }}</time> <P> {# output transformed feature image(s) #} {% if entry.featureImage | length %} {% for image in entry.featureImage.all() %} <img class="img-fluid" width="100%" height="150" src="{{ image.getUrl(featureImage) }}" alt="{{ image.title }}" /> {% endfor %} {% endif %} <P> {# render Matrix blocks for the “Post Content” field, passed as `blocks` #} {% include "_includes/post-blocks" with { blocks:entry.postContent.all } only %} {# display post categories #} {% if entry.postCategories|length %} <div class="border-t py-2 mb-6"> {% for category in entry.postCategories.all() %} <a href="{{ category.url }}" class="inline-block border rounded px-2 py-1 text-sm"> {{- category.title -}} </a> {% endfor %} </div> {% endif %} </div> </div> </div> {% endblock %}
_singles/about.twig
ページトップはシングルページとして構成しています。ブログ記事は「チャネル」という形式で構成されており、複数の投稿を一つのまとまりにグルーピングできるわけですが、シングルページだとそれこそ独立したページとして構成されます。Wordpressで言う「固定ページ」に該当するのかなと思います。

ここで使ってるもので特徴的なのはjumbotronってクラスで、最近のウェブサイトでよく使われていたりします。タイトル部分「BLUECORE’s Dictionary」の所がそれです。
ただ、スマホで参照したときにどうしても文字サイズを制御できて居らず、ここを何とかうまいこと修正したいものです・・。
{% extends "_layout" %} {% block content %} <div class="flex -mx-4"> <div class="bs-component"> <div class="jumbotron"> <h1 class="display-3"><b>BLUECORE's Dictionary</b></h1> <p class="lead">ITインフラエンジニアの思考記録:やって覚えて、見て覚えて、調べて覚える。</p> <hr class="my-4"> <p>以下のボタンを押すことでブログ投稿一覧を参照することが出来ます。</p> <p class="lead"> <a class="btn btn-primary btn-lg" href="https://dic.bluecore.net/blog">ブログポストを見る</a> </p> </div> </div> <div class="px-6 text-left"> {% include "_includes/post-blocks" with { blocks: entry.postContent.all() } only %} </div> </div> {% endblock %}
苦手なものでも遊びならおもろい
私には基本的にこうした絵心というか、見た目をカスタマイズする事に対するセンスに欠けてるのかなーと思っていたりします。それでも、技術観点から構文の構成を読み込んでみたり、見よう見まねだけど何かを作っていくのは面白いなぁ・・と思っています。
意外とこうして得た浅い知識でも仕事で周辺技術として覚えておくと何かと役に立つことも多く、今後も少しずつ見識を広げられたら良いなぁとか思っています。