PowerCMS X ブログ

2021-09-15

やさにちウォッチの舞台裏〜やさしい日本語のオウンドメディア構築事例〜

やさにちウオッチのトップページ

やさしい日本語の情報をやさしい日本語で発信する「やさにちウォッチ」を公開しました。
このオウンドメディアは PowerCMS Xで運営されています。実際にこの PowerCMS Xの製品サイトの CMSと同居していて、一つ専用のスペース(WorkSpace)を作成してそこで運用しています。

この記事では、「やさにちウォッチの舞台裏」と題して、主に CMS構築の観点から、利用している PowerCMS Xの機能をご紹介します。

新バージョンに同梱の標準テーマ「Media」を利用

デザイン自体は非常にシンプルなものですが、Bootstrap 4をベースにした新テーマ(次のバージョンで追加されます)「Media」を使って構築されています。
ある程度のカスタマイズはしていますが、ベースはほぼ標準のテーマに近いものとなっています。

サイトは基本的に静的 HTMLを生成していますが、一部以下の部分で動的機能を利用しています。

  • ランキングの表示(ランキングのためのデータ収集)
  • フォーム
  • ページネーション

尚、ふりがなとわかち書きは静的生成です。

ふりがなとわかち書きの実装

「SimplifiedJapanese」プラグインの機能を利用して、ほとんどのふりがなとわかち書きは自動で設定しています。

やさしい日本語エディタの機能が記事作成画面にありますので、そこで手作業で行っても良いのですが、PowerCMS Xのグローバルモディファイア「furigana」を利用しています。
エディタ上でふりがなを付けるよりも文章の修正などが楽になるメリットがあります。自動でのふりがな付与ですので、当然間違うこともありますが、プレビューで確認しておかしなものは、手動でふりがなを付与するか、あるいは辞書に登録することで修正をしています。

名前 説明 属性値
furigana rubyタグを用いてふりがなを追加します。 '1'を指定するとふりがなを追加 '2'を指定すると分かち書きしたテキストにふりがなを追加 '3'を指定するとふりがなを追加せずに分かち書きのみを適用

今回はわかち書き+ふりがななので、モディファイアの属性値には「2」を指定しています。

<mt:var name="contents" furigana="2">

わかちがきの ON/OFF のために、分かち書きの区切り文字を設定

ふりがなと分かち書きのON/OFFボタン

furiganaモディファイアでわかち書きする文字をプラグイン設定で指定できます。今回は、チェックボックスによってわかち書きとふりがなをそれぞれ ON/OFF できるようにするために、SimplifiedJapaneseプラグインの「分かち書きの区切り文字」に以下のように spanタグに separatorクラスを追加した区切り文字を指定しました。

分かち書きの区切り文字の設定

<span class="separator">&nbsp;&nbsp;</span>

あとは、読者がチェックを ON/OFF するタイミングで cookieに設定を保存、ふりがなとわかち書きの ON/OFF を切り替えています。

キーワード(タグ・アーカイブ)への自動リンク

一覧系ページは、カテゴリアーカイブとタグアーカイブ、年別アーカイブ(いずれも静的)を出力していますが、このうち、タグアーカイブには記事中にあらわれたタグを自動リンクするように設定しています。
Keywordsプラグインのブロックタグ「autokeywords」を使っています。

登録済みのタグと一致する部分を自動リンク

この機能はサイト公開後に思い立って実装したのですが、すでにタグ付けがされているため当初タグを CSVエクスポートしてキーワードにインポートしようと考えました。
ただ、このためだけにすでに「タグ」として登録されているものを別のテーブルにインポートし直すのは無駄だと考えたので、Keywordsプラグインの機能を拡張することにしました。

まず、ブロックタグ「autokeywords」の model属性に、キーワード以外のモデルを指定できるようにしました。今回は「tag」を指定しています。
タグには、キーワードモデルにあるようなリンク先を登録する「url」カラムがありませんが、やさにちウォッチではタグアーカイブを生成しているので、各々のタグのパーマリンクへのリンクとなるようにしました。
また、リンクの前後がわかち書きされてくれないので、生成したリンクの前後に挿入するわかち書きの区切り文字を enclosure 属性に渡すことによって前後をわかち書きできるようにしました。

<mt:setvarblock name="enclosure"><span class="separator">&nbsp;&nbsp;</span></mt:setvarblock>
<mt:autokeywords replace_once="1" model="tag" min_length="2" enclosure="$enclosure">
  <mt:entrybody convert_breaks="auto">
</mt:autokeywords>

これらの機能追加は、次のバージョンアップで皆様にもご利用いただけるようになります。

とりあえずやってみると、「本」というタグがあり、「日本」の「本」にリンクが付けられてしまいました。そこで、環境変数「keywords_use_mecab」を指定し、タグの判別に MeCabを利用することにしました。
MeCabを利用することで、「東京都」に含まれる「京都」を京都と判定してしまうようなことがなくなります。
ただし、逆に「福岡県」のようなタグを付けている場合「福岡」「県」というように形態素解析の結果がわかれてしまうため、
都道府県についてはキーワードにも別途登録しました(※keywords_use_mecabが有効の時、キーワードに登録された単語は形態素解析の辞書にも登録されるようになっています)。

記事の作成と編集

Webに不慣れなスタッフが記事を作成・編集することも多いため、記事は基本的にリッチテキストエディタを利用しています。
やさしい日本語変換ボタンもありますが、基本的には参考にする程度として、文章作成は基本的に人の手で行っています。

記事作成・編集画面

SNSや検索結果などに使われる「概要」(最近は検索結果というより主に SNSですが)欄へは、入力を忘れるケースが多く見られるので、これも Keywordsプラグインによる概要の自動生成を ONにしています。
ただ、基本的には人がそれを見て、適切なものに修正する、という使い方をしています。自動タグ付けも ONにしていますが、これも適切なものに人の手で編集するようにしています。

前述の通り、ふりがなについては、プレビューで意図しないものになってしまうもののみ手動で追加するか辞書に追加します。

ワークフローについては利用せず、ExternalPreviewプラグインが提供する「外部プレビュー」機能を利用し、Slackチャンネルにプレビュー URLを貼り付ける形でレビューを行っています。

ランキング部分の実装

AccessAnalyticsプラグインによってランキングの算出の元となるアクセス解析結果を取得して蓄積し、ブロックタグ「rankedobjects」を利用して出力しています。

アクセスランキングのような、定期的に自動更新されるようなコンテンツは、cronなどを利用して定期的に再構築を行うような方法もありますが、やさにちウォッチでは、動的生成とし、ブロックタグ「cacheblock」を利用することで、24時間キャッシュされるようにして無駄な負荷がかからないようにしています。そして、ページへの埋め込みは Ajaxによって非同期に読み込んでいます。

ランキングウイジェット

<mt:cacheblock cache_key="__widget_ranking__" workspace_id="$website_id">
  <mt:rankedobjects models="entry" limit="10" include_workspaces="this">
    〜
  </mt:rankedobjects>
</mt:cacheblock>

RSSによる最新記事の配信

(9月15日追記) リクエストをいただいたので、RSSを追加しました。PowerCMS のRSSテンプレートとほぼ同じ形でインデックス・テンプレートを作成するだけです。ついでにRSSをSlackに自動連携するようにして、記事が公開されたことを社内でシェアするようにしました。

<mt:block regex_replace="'/^%s+$/um',''" remove_blank="1"><?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title><mt:websitename remove_html="1" encode_xml="1"></title>
    <link><mt:websiteurl encode_xml="1"></link>
    <description><mt:websitedescription remove_html="1" encode_xml="1"></description>
    <mt:entries lastn="20">
    <item>
      <title><mt:entrytitle remove_html="1" encode_xml="1"></title>
      <link><mt:entrypermalink encode_xml="1"></link>
      <description><mt:entrybody encode_xml="1"></description>
      <pubDate><mt:entrydate format_name="rfc822"></pubDate>
      <mt:entrycategories>
        <category><mt:categorylabel encode_xml="1"></category>
      </mt:entrycategories>
  </item>
  </mt:entries>
  </channel>
</rss>
</mt:block>

Twitterとの自動連携

PostOnTwitterプラグインを利用して、Twitterアカウント( @yasanichiwatch ) 別ウィンドウで開きますと連携し、記事の公開と同時にハッシュタグ付きでTwitterへポストするようにしています。

Twitterとの連携の設定

カテゴリー:サイト制作全般 | テンプレート作成Tips | プラグイン | 技術情報

投稿者:Junnama Noda

2021-07-27

静的サイトジェネレータ(SSG)との再構築 (ファイル生成) 時間比較

SSG (Static Site Generator) や Jamstack あるいはサーバーレスといった言葉を聞くことが増えてきました。SSGと対比してよく引き合いに出されるのが WordPressに代表されるCMSです。管理画面からコンテンツが更新でき、コンテンツはデータベースに保存、コマンド操作なども不要である CMSはやはり便利ですが、静的ファイルの高速配信、セキュリティ面での管理のしやすさなどから Webにおける静的配信が流行っているのも理解できます。

PowerCMS Xは静的ファイルを高速に出力できるCMSです (動的生成も可能なハイブリッド型CMS)。では、Static Site Generatorと比較してどの程度のパフォーマンスが出せるのでしょうか。静的htmlを出力するSSG / CMSを同一環境にインストールし、出力時間を計測しました。

計測した SSG / CMS は以下にあげる6種類です。

  • Jekyll (4.2.0)
  • Hexo (hexo-cli 4.2.0)
  • Pelican (4.6.0)
  • Hugo (v0.85.0+extended)
  • MTOS (5.2.11)
  • PowerCMS X (2.5820)

計測は同一マシンで行いました。1記事 / 10記事 / 100記事 / 1000記事で各5回、SSGについてはコマンドから、PowerCMS X / MTOS については管理画面からの再構築で計測しました。

  • ハードウェア : AWS EC2 (t3.medium)
  • OS : Amazon Linux release 2 (Karoo)
  • Python 3.7.10
  • Ruby 2.7.4p191
  • Node.js v14.17.2
  • PHP 8.0.8
  • Perl v5.16.3
  • MySQL 8.0.25

計測結果

静的ファイルジェネレーター/CMSによる再構築計測結果のグラフ

SSG/CMS 1記事 10記事 100記事 1000記事
Jekyll 0.837 0.909 1.204 4.248
Hexo 1.51 1.516 2.217 6.506
Pelican 0.61 0.527 0.786 3.233
Hugo 0.197 0.197 0.357 1.852
PowerCMS X 0.152 0.164 0.29 2.61
MTOS 1.4 1.8 9.8 90.8

単位 : 秒

1000記事分のデータを投入した状態では、Hugo がもっともビルド時間が短かい結果となりました(PowerCMS Xではper_rebuild, async_max_procなどの環境変数の最適化でさらに速度を向上させることができますが、本計測では rebuld_async有効の他はデフォルト状態で計測しました)。次いで高速な順に Power CMS X, Pelican, Jekyll、Hexo, MTOS となり、PowerCMS X は静的ファイルジェネレーターと比較しても遜色のないファイル生成パフォーマンスを叩き出せることがおわかりいただけると思います。

AWSの S3などを利用した静的ファイルホスティングや、CloudFrontなどと組み合わせて高速なコンテンツ配信を実現するも簡単です。次回以降の記事では、 AWSの S3/CloudFront と PowerCMS Xを連携させるソリューションについてご紹介します。

カテゴリー:技術情報

投稿者:Junnama Noda

2021-07-05

様々な条件でSELECT文を生成する (PADOBaseModel の loadメソッド詳説)

PADOBaseModelクラスの loadメソッドとは PowerCMS Xでデータベースに対して SELECT文を発行するしくみで、PowerCMS Xのデータベース操作の肝となるメソッドです。

PADOBaseModelクラスのオブジェクト生成

PowerCMS X (Prorotypeクラス) のオブジェクトが $app の時、$app->db が PADOクラス、さらに、以下のようにしてオブジェクトを生成します。

$app = Prototype::get_instance();
$entry = $app->db->model( 'entry' ); // PADOBaseModel (記事モデル)

PowerCMS Xのデータベースのスキーマと命名規則

テーブル名は prefix(mt_)+モデル名となります。「記事」モデルであれば「mt_entry」、「ページ」モデルであれば「mt_page」となります。
カラム名は、モデル名_+カラム名となります。記事のタイトルは「entry_title」、カテゴリのラベルは「category_label」となります。
すべてのテーブルには「モデル名_id」という、INTEGER PRIMARY KEY, AUTOINCREMENT が設定されたカラムが作成されます。記事モデルの場合は「entry_id」です。
リレーション型のカラムは「疑似カラム」で、テーブルにカラムは作成されません。
リレーション型のカラムに値をセットすると「mt_relation」テーブルに relation_from_obj, relation_from_id, relation_to_obj, relation_to_id, relation_order というレコードを生成します。

loadメソッドや PowerCMS Xのデータベース操作においては、prefix(mt_)やカラム名の先頭の「モデル名_」は省略して指定します。

loadメソッドのベーシックなサンプル

$app->db->model( 'entry' )->load( ['title' => 'PowerCMS X'] );

発行されるSQL文は以下となります。

SELECT * FROM mt_entry WHERE entry_title='PowerCMS X';

※ 実際には(これ以下のSQL文も) 疑問符 placeholder が使われます。

SELECT * FROM mt_entry WHERE entry_title=?;

loadメソッドの詳説

SELECT文を発行してオブジェクトを返します。

$app->db->model('モデル名')->load( [ $terms, $args, $cols, $extra, $ex_vars ] );

パラメタ

  1. mixed $terms : 数値(ID) または、カラム名と値の配列、またはSQL文
  2. array $args : ソート順やlimit/offset等のオプションの配列、または第一引数にSQL文を指定した時は placeholderの配列
  3. mixed $cols : SELECT対象のカラム(配列またはカンマ区切りの文字列)または'*' (すべて)
  4. string $extra : SELECT文の WHERE句に追加する SQL(インジェクション対策はコード中で行う必要があることに注意)
  5. array $ex_vars : 第4引数に指定した placeholder に対する値の配列

戻り値

  • mixed $objects(s) : 数値(ID)を渡した時は単一のオブジェクト、それ以外の時はオブジェクトの配列

1. ID指定で単一のレコードを取得

IDが1の記事を取得する

$entry = $app->db->model( 'entry' )->load( 1 );
$entry = $app->db->model( 'entry' )->load( '1' );
// 発行されるSQL文
// SELECT * FROM mt_entry WHERE entry_id=1;

数値指定の場合、戻り値は単一の PADOBaseModelオブジェクトとなります。指定したIDが存在しない場合は NULLが返ります。

2. 配列(ハッシュ) $terms 指定での load

第1引数が配列 $terms の場合、戻り値は常にオブジェクトの配列(該当するものがない場合は空の配列)となります。

$app->db->model( 'entry' )->load( ['title' => 'PowerCMS X', 'basename' => 'powercmsx'] );
// 発行されるSQL文
// SELECT * FROM mt_entry WHERE entry_title='PowerCMS X' AND entry_basename='powercmsx';

値に配列を指定することもできます。

$app->db->model( 'entry' )->load( ['title' => ['like' => '%PowerCMS X%'], 'status' => ['IN' => [1, 2, 3] ] ] );
// 発行されるSQL文
// SELECT * FROM mt_entry WHERE entry_title LIKE '%PowerCMS X%' AND entry_status IN (1,2,3);
// ※ $app->db->escape_like( 'PowerCMS X', true, true ); で、'%PowerCMS X%'  が取得できます
// (%や_をエスケープして、第2, 第3引数指定で前後に「%」を付けます)。
$entries = $app->db->model( 'entry' )->load( ['keywords' => ['IS NULL' => 1] ] );
// 発行されるSQL文
// SELECT * FROM mt_entry WHERE entry_keywords IS NULL;
$entries = $app->db->model( 'entry' )->load( ['published_on' => ['BETWEEN' => ['2021-07-01 00:00:00', '2021-07-31 23:59:59'] ] );
// 発行されるSQL文
// SELECT * FROM mt_entry WHERE entry_published_on BETWEEN '2021-07-01 00:00:00' AND '2021-07-31 23:59:59';

3. 第2引数 $args の指定

第1引数 $terms に配列を指定した場合、第2引数にLIMIT, OFFSET, ORDER BY, JOIN 句を指定できます。

$app->db->model( 'entry' )->load( ['title' => 'PowerCMS X'], ['sort' => 'id', 'direction' => 'descend', 'limit' => 10, 'offset' => 5] );
// 発行されるSQL文
// SELECT * FROM mt_entry WHERE entry_title='PowerCMS X' LIMIT 10 OFFSET 5 ORDER BY entry_id DESC

※ limit指定が「1」であっても、返り値は常に配列となることに注意してください。

JOIN句を組み立てるには以下のようにします。以下は id=1のassetオブジェクトのサムネイル(meta)とURLオブジェクトをあわせて取得する例です。

$args = ['join' => ['urlinfo', 'meta_id'], 'distinct' => 1];
$thumbnails = $app->db->model( 'meta' )->load( ['object_id' => 1, 'model' => 'asset', 'kind' => 'thumbnail'], $args );
// 発行されるSQL文
// SELECT DISTINCT mt_meta.*,mt_urlinfo.* FROM mt_meta JOIN mt_urlinfo ON mt_meta.meta_id=mt_urlinfo.urlinfo_meta_id WHERE ( meta_object_id = 1  AND  meta_model = 'asset'  AND  meta_kind = 'thumbnail' );

4. 第3引数 $cols の指定

第1引数 $terms に配列もしくは数値を指定した場合、第3引数 $cols に SELECT対象のカラムを指定できます。指定できるのはカンマ区切りのテキストもしくは配列です。

$entry = $app->db->model( 'entry' )->load( 1, null, 'id,title' );
// 発行されるSQL文
// SELECT entry_id,entry_title FROM mt_entry WHERE entry_id=1;
$app->db->model( 'entry' )->load( ['title' => 'PowerCMS X', null, ['id', 'title'] );
// 発行されるSQL文
// SELECT entry_id,entry_title FROM mt_entry WHERE entry_title='PowerCMS X';

5. 第4引数 $extra の指定

第1引数 $terms に配列もしくは数値を指定した場合、第4引数にWHERE句に追加するSQL文を指定できます。

$extra = 'AND entry_workspace_id IN (1, 2, 3)';
$app->db->model( 'entry' )->load( ['title' => 'PowerCMS X', null, null, $extra );
// 発行されるSQL文
// SELECT * FROM mt_entry WHERE entry_title='PowerCMS X' AND entry_workspace_id IN (1, 2, 3);

6. 第5引数 $ex_vars の指定

第4引数にSQL文を指定する時、第5引数に第4引数に対する placeholderを指定できます(配列)。

$extra = 'AND entry_keywords=? AND entry_status=?';
$ex_vars = ['PowerCMS X', 4];
$app->db->model( 'entry' )->load( ['title' => 'PowerCMS X', null, null, $extra, $ex_vars );
// 発行されるSQL文
// SELECT * FROM mt_entry WHERE entry_title='PowerCMS X' AND entry_keywords='PowerCMS X' AND entry_status=4;

7. 第1引数にSQL文を指定する

数値や $terms配列の代わりに、第1引数にSQL文を指定することができます。当然ですが、ユーザーから受け取った任意の値をそのままSQL文に含めてはいけません。第1引数にSQL文を指定する場合は第2引数に placeholder (配列)を渡すことができます。

$sql = 'SELECT * FROM mt_entry WHERE entry_title='PowerCMS X';
// 発行されるSQL文
// $app->db->model( 'entry' )->load( $sql );

8. 第1引数にSQL文を指定した時は、第2引数に placeholderに対応する値の配列を指定

$sql = 'SELECT * FROM mt_entry WHERE entry_title=?';
$vars = ['PowerCMS X'];
$app->db->model( 'entry' )->load( $sql, $vars );
// 発行されるSQL文
// SELECT * FROM mt_entry WHERE entry_title=?;
// ↓
// SELECT * FROM mt_entry WHERE entry_title='PowerCMS X';

loadメソッドとよく似たメソッド

loadメソッドとよく似たメソッドに以下のメソッドがあります。

load_iter メソッド

戻り値が PDOStatement であることを除き、loadメソッドとの違いはありません。

$sth = $app->db->model('モデル名')->load_iter( [ $terms, $args, $cols, $extra, $ex_vars ] );
$objects = $sth->fethAll();

get_by_key メソッド

loadメソッドを呼び出し、該当するものがある時、最初にマッチした単一のオブジェクトを返します(配列ではなく、単一のオブジェクトが返ります)。
該当するものがない時は、$termsが配列指定の時、その条件でオブジェクトを新規に生成し(第2引数以下の引数は無視されます)、単一のオブジェクトを返します。

$obj = $app->db->model('モデル名')->get_by_key( [ $terms, $args, $cols, $extra, $ex_vars ] );
if (! $obj->id ) {
    // オブジェクトがマッチしなかった時に生成された新規オブジェクト
    $obj->save();
}

countメソッド

loadメソッドと同じ条件でオブジェクトをカウントし、マッチした数値を返します。

$count = $app->db->model( 'entry' )->count( ['title' => 'Welcome!'] );
// 発行されるSQL文
// SELECT COUNT(entry_id) FROM mt_entry WHERE ( entry_title = 'Welcome!' );

カテゴリー:技術情報 | プラグイン

投稿者:Junnama Noda

ブログ内検索

アーカイブ