Movable Typeの最近のブログ記事

Data::ObjectDriver::BaseObject::begin_work、rollback、commitを使う。

MTのコードにはこのあたりを考慮していないものが多すぎる。権限(Association)を割り当ててからユーザーのパーミッション(Permission)を更新する処理など、あちこちに投げっぱなしのコードがある。数万件のレコードを管理画面操作で更新する等。

複数のレコードを纏めてINSERT、UPDATEするなどの処理をループで回さなければならなかったり、速度面の問題であったり、あの忌まわしいMT::Meta::Proxy::save 問題であったり、そういうところにもっと目を向けて真摯に取り組まないと未来はないよ。


    my @entries = MT->model( 'entry' )->load( undef, { sort => 'id', direction => 'descend' } );
    my $driver = MT::Object->driver;
    $driver->begin_work;
    eval {
        for my $obj ( @entries ) {
            print $obj->id . "\n";
            # if ( $obj->id < 800 ) {
            #     die; # ここでdieした時、すべてのレコードは更新されない
            # }
            $obj->excerpt( 'Hello World.' ); # 概要欄に値をセット
            $obj->save or die $obj->errstr;
        }
    };
    if( $@ ) {
        $driver->rollback;
        die $@;
    }
    $driver->commit;

この記事は Movable Type Advent Calendar 2016 の最終日の記事です。

再構築ウィンドウ

Movable Type Advent Calendar は2012年から参加していて毎年最終日を担当させていただいています。過去の記事はこちら。

2016年のネタ。2017年、MTはもっと高速になる!

今年は何を取り上げようかと思っていましたが、丁寧に記事を書くモチベーションと時間が足りず、とはいえ負けるのも(誰に?)悔しいので、以前から気になっているMTの箇所にパッチを当てて再構築のスピードを速める実験をしてみました。

静的ファイルジェネレーターとしての Movable Type

少しだけもったいつけて前振りを。静的サイトジェネレーター Advent Calendarで Movable Typeの中の人である長内さんが記事を書かれていますね。そう、MTは静的ファイルを吐き出すことのできるCMSです(ダイナミックパブリッシングも可能、部分的ファイナミックパブリッシングも可能、もちろんダイナミック処理ではキャッシュの仕組みを備えています)。

今年は第2回CMSプロレス 多言語サイトタイトルマッチ - CMS SUNDAYにお呼ばれして、チームMT(一人だけど)として参加、見事に勝利してきました。Movable Type以外の参加CMSはDrupal、concrete5、TYPO3、WordPress。Movable Typeだけが異質だと思いませんか?

  • MT以外のCMSはすべてダイナミック前提
  • MT以外のCMSはすべてPHP(MTは基本Perl、ダイナミックはPHP)

PHP、Perlの違いはまぁ置いておいて、ダイナミックとスタティック(静的サイト)だと、文化が違うというか話がかみ合わないんです。同じく中の人の澤さんに「勘所がわからん!」とか言ってますね。

Facebookにて、ピンチを迎える私!

そう、動的、ダイナミックなCMSが「高速ですよ」っていったらみんなキャッシュの話静的ファイルジェネレーターでキャッシュって、あるとしたらCDNやwebサーバーチューニングとかの話です。もちろんダイナミックも持ってるからキャッシュの話ついていけるけど、URLリライト系の話も盛り上がるみたいなんですよねあっちの文化では。てことで勘所あんまりわかりませんでした前半は。

静的ファイルジェネレーターで「高速」ってのは一般にはいわゆるパブリッシュ(再構築)の話ですよね。Movable Typeの再構築はしばしば待ち時間が嫌だとかそういう話になります。ただ、エンタープライズCMSの世界では、そこあんまり話題にならないんですよね。CMS操作の重さより、公開サイトの遅さや負荷とかそっちを気にされます。ま、そういうのも含め(使われているサイトの規模や用途も含め)て文化の違いとしか表現しようがありません。ま、なんとか勝ててよかったです。

MTの再構築はまだ速くなる余地がある

今年、ある案件で公開側のサイトのダイナミック処理の速度が思ったように出ずに調査していた時に、テンプレートのコンパイルキャッシュが非効率な処理になっているケースを発見しました。以下の記事にその顛末をまとめています。

その時に「あれ? 静的ファイルの再構築処理の際の MTタグ(MTML)のコンパイルってどうなってんだっけ? って見たんです。どうもキャッシュされている気配がない。

で、少し手を入れてみたんですがうまく行かず、途中のファイルをFogBugzに投げてそこで止まってました。

MT::Builder の compile に手を入れる

MT::Builder の compile てのは、乱暴にひと言で言えば、テンプレートのMTタグをパースしてコンパイルして{tokens}を組み立てているところです。そのあと、テンプレートはコンテキストに沿ってビルドされます。MTはSQLの実行結果やビルドしたブロックやモジュールなどはマメにキャッシュしていますが、このコンパイル結果をキャッシュしていません。以下、見てもらえますか!? そう、MTタグを正規表現でパースしているんですよ、ここ。

    while ( $text
        =~ m!(<\$?(MT:?)((?:<[^>]+?>|"(?:<[^>]+?>|.)*?"|'(?:<[^>]+?>|.)*?'|.)+?)([-]?)[\$/]?>)!gis
        )
    {

ご存知の通り(知らない人も多いでしょうけど)、Movable Typeでは、再構築処理を行う際に1つのCGIへのリクエストあたり40記事ファイルずつ再構築を行い、リダイレクトを繰り返してすべてのファイルを再構築します。この40という数字は環境変数で変えることができます。

つまり、1リクエストでの再構築処理で同じテンプレートが少なくとも40回パースされるわけです。こいつを一回で済ませてしまえば。

mt/lib/MT/Builder.pm

12a13
> use Data::Dumper;
32a34
>     my $is_tmpl;
34a37,39
>         if ( $ctx->id ) {
>             $is_tmpl = $ctx->id;
>         }
73a79,88
>     
>     my $cache_key;
>     my $chached_state;
>     if ( $is_tmpl && $text ) {
>         $cache_key = 'compiled-cache-' . MT::Util::perl_sha1_digest_hex( $text );
>         if ( $opt ) {
>             $cache_key .= '.' . MT::Util::perl_sha1_digest_hex( Dumper $opt );
>         }
>         $chached_state = MT->request( $cache_key );
>     }
82a98,101
>     
>     if (! $chached_state )
>     {
>     
291a311,316
> 
>     } else {
>         $state = $chached_state->{ state };
>         $depth = $chached_state->{ depth };
>         $errors = $chached_state->{ errors };
>     }
306c331,338
< 
---
>     if ( $cache_key ) {
>         if (! $chached_state ) {
>             $chached_state = { depth => $depth,
>                                state => $state,
>                                errors => $errors };
>             MT->request( $cache_key, $chached_state );
>         }
>     }

ソースファイル(Builder.pm)

マシンスペックの高い環境では、EntriesPerRebuildの値を大きくするほどその効果が顕著になり処理も速くなります。

計測結果

このブログをエクスポートして、最新版のMT6.32にテーマ Rainier 1.22 を入れた環境を用意して計測しました。「デフォルト、すべてのファイルの再構築計測結果(ケース1)」と、「記事アーカイブのみ、EntriesPerRebuildに1000を指定した場合(ケース2)」各5回の計測結果はこちら。

計測ケースケース1ケース2
Builder.pmオリジナルBuilder.pm改良Builder.pmオリジナルBuilder.pm改良Builder.pm
1回目331243216155
2回目302254270170
3回目285234211180
4回目301255189155
5回目281245170155
合計(秒)150012311056815
結果約18%高速約22%高速

2017年、もっとMTが速くなるための宿題

あまり時間もなかったので、今日のところはこれまでです。引き続き以下のような部分を検討して実装できれば2017年、MTの再構築はもっと速くなることでしょう!

  • まずは、このコードの影響度など、問題ないかの確認・検証
  • 現状template_id 指定がある場合のみキャッシュしているが、これ以外にキャッシュできるケースがないかの検討
  • リクエストをまたがって memcachedやMTのキャッシュ(MT::Session)などへのキャッシュ

それでは、メリークリスマス、そして、良いお年をお迎えください!

すいませんすいません、前の夜にちょっと頭にくることがあって明け方まで飲んでたらそのまま寝落ちしてしまい、翌日社員に電話で起こされたので十分な準備もできずお話ししてしまったので海より深く反省しつつ。

セッションの写真

伝えたかった、プレゼンに勝つための4要素

  1. ターゲットによってアピールポイントを変える
  2. CMS製品と同じだけ「会社」「プレゼンター」をアピールする
  3. 言い訳を用意してあげる
  4. 背中を押してあげる

誰にプレゼンするのか。部門ごとの注意点。

CMSの導入をどの部門が主導で行うかって実は結構バラバラです。代表的なものは次のような部門となります。

  1. 企画・広報・総務部門
  2. 情報システム部門
  3. マーケティング部門
  4. 営業部門や製造部門などのwebと直接関係のない部門
  5. 経営者自身

その他にメディア系企業では編集部とかネット専業企業の場合はそのものweb部門がある企業なんかもあります。

経営者へのプレゼンを行うか、否か

上記の5つの顧客ポジションのうち、最後の「経営者自身に対するプレゼン」ですが、これを行うかどうかは「どの市場を相手にビジネスを展開するか」という戦略と密接に絡んできます。ちなみにアルファサードでは経営者自身にプレゼンを行うことはほぼ皆無、実質ありません。これは、ライセンス料の値付けが関係しています。

  • PowerCMS Standard 5ユーザー 30万円
  • PowerCMS Professional 無制限ユーザー 60万円
  • PowerCMS Enterprise 無制限ユーザー 90万円
  • PowerCMS Advanced 無制限ユーザー無制限サーバー 180万円

当社のCMSは上記の価格です。これにオプションや初期構築などのSI、ケースに応じてインフラなどの構築コストが追加されます。

CMSのライセンス料金は、全体コストの20%が上限

CMSの場合、CMSを導入すればすぐにサイトの更新ができるようになるわけではありません。例外としては、CMSにあらかじめ用意されたテーマを使ってサクッとサイトを立ち上げるケースでしょう。WordPressなどはこのあたりに強みがあります。一方でPowerCMSの場合、用意されたテーマでサイトをそのまま運用し始めるようなケースは稀で、オリジナルのデザインのサイトをベースにガッツリつくり込むタイプのサイトが多いです。有償の商用CMSですから、このライセンスコストと初期構築のバランスをクライアントは意識されます。この、コスト感覚からいえば、私の経験値から「CMSのライセンス料金は、全体コストの20%が上限」という数字が感覚としては合っているように思います。つまり、Professional版なら初期構築の総予算300万円、Enterpriseなら初期構築の総予算450万円、Advancedなら900万円。

この規模の予算を用意する(できる)クライアントのweb担当者は、まずもって経営者ではありません。地方のサイト制作では経営者が直接ハンドリングする場合もあるでしょうが、そのケースで予算300万円を超えるケースは少ないと思います。よって、PowerCMSはライセンス金額の設定によって経営者が直接決定に関与する規模の案件、クライアントではなく、経営者以外の誰かが決定者である規模以上のクライアントをターゲットにビジネスしているということになります。必然的に東京が主戦場になります。

一方で、当社のようなメーカーの場合、直接エンドユーザーにプレゼンしない場合もあります。いわゆるCMSを提案する立場の以下のようなお客様んい対するプレゼンが必要なケースなどです。

  1. web制作会社
  2. 広告代理店(web部門、子会社など)
  3. SIer(子会社など)

テーマが広範囲に渡りすぎるので、このケースは一旦別の機会にゆずることにします。ここで検討するのは下記の4部門です。

  1. 企画・広報・総務部門
  2. 情報システム部門
  3. マーケティング部門
  4. 営業部門や製造部門などのwebと直接関係のない部門

企画・広報・総務部門はワークフロー重視、現場に負担をかけずに運用を良くする提案が響く

そこそこの規模の会社でサラリーマン経験がある方はおわかりだと思うのですが、いわゆる総務企画、広報といったバックオフィス担当、直接お金やモノを生み出さない部門の人は、一般に社内ではあまり力が強くありません。普通に考えればわかると思いますが、例えばトップセールス、お客様に支持されるために営業に接待に駆けずり回ってしかも成果を上げている部門の人やヒット商品を作るために商品を作っている人からすると「直接収益に貢献している自分たちがあって会社が成り立ってんだ」という思いがどうしてもあります。お客様の都合で予定が変わることなんかもざらですし、数字が悪ければ容赦なく社内の突き上げや上司からの叱責が飛んでくるわけです。そんな毎日の忙しさの中で総務部門の人がですよ?「すいません、今度CMSというのを導入して皆様にもほーむぺーじを活用してですねぇ、webでの情報発信をお手伝いいただくことになりました」なんて気軽に言えるわけないじゃないですか。どう言えたらいいと思います? それは、以下のようなセリフですよね。

「すいません、今度CMSというのを導入して皆様にもほーむぺーじを活用してですねぇ、webでの情報発信をお手伝いいただくことになったのですが、皆様にほーむぺーじのチェックいただいていたこれまでの手順は何も変わりませんし、Wordで社内報の原稿お願いするときあるじゃないですか、あんな感じで時々お願いするだけですので。あとは我々が頑張ってやることになりましたので」

くらいに留めたいわけです。ただでさえ気を遣うんですよ。営業部の山田さん(仮)や製造部の田中さん(仮)にお願いするのって。

なので、いわゆる間接部門、バックオフィス担当者へのプレゼンでは、圧倒的に「ワークフロー重視、現場に負担をかけずに運用を良くする提案が響く」わけです。

「みなさんの画面はこんな感じで多機能ですけど、コンテンツ投入担当者の画面はこんな風になります。シンプルですよね。権限を設定するとできない機能が画面から消えちゃうので、こんなシンプルな画面になるんですね。」

くらいは見せてあげる必要はあるでしょう。

また、承認フローだけじゃなくて、サイトの確認フェイズで直接部門の皆さんに負担をかけない工夫も考えてあげなければなりません。

  • 制作サーバー >> 確認用サーバー(ここのアドレスをメールして担当者にチェックもらう >> 公開サーバー

という運用をしていたとします。CMSになっても、これが変わらないのであれば、社内でイチイチ説明する必要すらありません。これ、すごく喜ばれます。

情報システム部門は「多機能」アピールが効く。でも意外に採用ポイントは社内ネットワークやセキュリティポリシーだったりする件

情報システムの方はそれはもう諸々詳しいです。他の製品のことも良く知ってる。多機能をそのままアピールしても情報処理能力が高いから消化できる。日々色々な社内のネットワークやシステムの課題に対処していくのが仕事ですからね。多機能のアピールがそのままできます。正直楽しいですよね、プレゼンしてても。

ところが、意外や意外、採用ポイントは社内ネットワークやセキュリティポリシーだったりすることが意外と多いわけですよ。DMZがどうとかVPNがどうとか、ネットワークなどのセキュリティに引っ張られたりする。なので、「スタティックにファイルを吐き出せる」Movable Type や PowerCMS はそれだけで有利だったりするんですよね。だから CMSプロレスとかで競合だったPHP+動的前提のCMSに対してはそれだけで優位性が出るときも多いわけです。ここ、実はすごく大きな優位性だったりするんですわね。

PowerCMS の Copy2Public プラグインや PowerSyncがなんで売れるのか。これ、顧客のネットワーク&サーバー構成を変える必要がないからなのですね。合わせられる強みです。ここは、企画広報部門に対する強みにも繋がります。運用ルールを変えなくていいからですね。

PowerSyncウェブサイト

また、個人情報の取り扱いに責任を負わされるのもこの部門だったりします。フォームなどのコンバージョンに関連する機能を提供するのもCMSには求められますから、このあたりに対する答えも用意しておく必要があります。Salesforceと連携するソリューションなどは、選択肢を示すという点でも有用なアピールポイントになります。

PowerCMS for Salesforceウェブサイト

マーケティング部門にはアクセス解析やMA、デジタルマーケティングの機能が響く。でも、拡張性の高さが実は一番響いたりする

旬ですね。今この分野がいろいろ熱いですが、例えばこのあたりの市場に対して自社だけででソリューションを提供するのは中々に辛いものがあります。例えば PowerCMS でも簡単なアクセス解析の機能を提供していますが、 Google Analytics などの高機能で普及しているソリューションと同等の機能を自社で開発するのは実際に割に合いません。であれば、そのあたりの競争力の高い別のソリューションと「つなぎ込める」懐の広さ、拡張性の高さがアピールポイントになります。PowerCMSでは、Google Analytics、Salesforce、FacebookやTwitterなどと連携するソリューションがありますし、REST APIなどを通じて他のソリューションと連携する機能も豊富に用意されています。開発も簡単にできる。ここをアピールすることが響きます。

営業部門や製造部門などのwebと直接関係のない部門はとにかくシンプルで「多機能」の逆を行くこと

情シス部門や、企画広報部門、マーケティング部門と真逆で、ここへのアピールはとにかく「シンプルで負担が増えないこと」です。管理画面のボタンは少なければ少ない方がいい。多機能なソフトウェアが導入されて学習コストを負担するなんて、実際ごめんです。CMSプロレスで示したのはまさにこれです。多機能をアピールすると、学習コストへの抵抗と仕事が増えることに対する警戒心が生まれますし、そもそも消化できないです。「理系の情報処理能力」「文系の感覚的・直感的な好き嫌い」とかいうと反論きそうではありますが、情報が消化しきれなことを意識しなければなりません。

他言語サイトタイトルマッチでのカスタマイズ済みダッシュボード

CMSプロレスで工夫したあたりは以下の記事に纏めていますので、こちらも参考にしてください。

書ききれなかったので、後日続きを書きます。

先の土曜日、ハロウィンで賑わう渋谷で開催された 第2回CMSプロレス 多言語サイトタイトルマッチ に チーム Movable Type (一人だけど) 参加してきました。

結果的にいくつかのCMSと同点だったのですが、最終的には審査員の評価に重点を置いていただき、無事勝利を収めることができました! (前半に行われたクイズの点を加味すると同点だったのですが、クイズの結果相当酷かったというか、私はCMSマニアではないので他のCMSのことあんまり興味なくて知らんのです...)

CMSプロレスチャンピオンの記念品

さて、勝利したから書くわけではないのですが、今回、自分のデモンストレーションも他のCMSのデモンストレーションも見ていて、どこが勝敗のポイントになったのか自分なりに分析した結果が興味深かったので書いておこうと思います。

短時間のデモで「多機能」を伝える困難さを理解しておくこと

多言語サイトに対する機能がデフォルトで充実しているのは間違いなく concrete5、Typo3、Drupal の3つだったと思います。管理画面にデフォルトでその機能があり、対応言語数もとても多い。3者ともその部分をデモに取り入れて画面を見せていました。ここで考えておかなければならないことは「多言語に関する設定を行うのは管理者(Administrator)であり、運用者は目にすることのない画面」であることです。多機能であり且つ管理者向け機能であるがゆえに、サイト運用者には難しすぎます。そもそもサイト運用者が目にする必要のない画面なのですが、これを見せてしまった。

ちなみに、審査員の2/3は非エンジニアです。エンジニアにターゲットを絞ったデモを行うよりも、非エンジニア目線のほうが有利に働く場だったのです。

各競技のポイントは、以下の審査員によって採点されます。

株式会社ロフトワーク

WordPressの場合は、複数のプラグインソリューションがあるという紹介だったと思います。プラグインの多さは言わずもがなWordPressの強みであることに間違いはないのですが、どこをアピールするのかが今ひとつはっきりと絞り込めていなかったように思います。

一方で、私がとった戦略は「多機能」アピールをしないことでした。PowerCMSでなく、PowerCMSの入っていない純粋? なMTで、しかもダッシュボードを極端にシンプルにして臨みました。運用者の目に飛び込んでくる大きなボタンは何と2つだけです。多機能の真逆、Less is Moreですね。

極限にシンプルなダッシュボード

URLの設定についての文化の違いとアピールポイント

今回のCMSのうち、Movable Type(PowerCMS) だけが静的ファイルパブリッシュを基本としたCMSです。「基本」と書いたのは、MTはダイナミック・パブリッシングもサポートしていて、動的生成もできるのですが、DynamicMTMLというその中間(ファイルの一部を動的処理する)形式もサポートしていて、動的サイトではこちらを使うことが(当社では)多いため、基本ファイルを生成する運用がほぼ100%。こうなると、CMSの設定でURLの見せ方を変えるような文化がそもそもありません。パブリッシュ後静的生成されたファイルを別のサーバーに転送(同期)して運用するようなケースも多く、国ごとに配信するサーバーを分けて別のサブドメインで運用するなんてことも普通にできます。ファイルを吐き出した後のURLの設定はwebサーバー(Apache、IISやnginx)の仕事になるため、CMSでできることは限られています。もちろんファイルを吐き出すパスについては自由に設定でき、いわゆるMTのアーカイブマッピングはなかなかに柔軟で強力なのですが、正直このテーマで盛り上がる他のCMSの感覚が理解できず、ここは何か逆に不利なシチュエーションだな、と思っていました。

なので、結局は文化の違いとファイルを吐けるメリットのみ強調して、特に機能のアピールはせず。まぁ、これも運用者にはわりとどうでも良い話なのかもしれません。

ただ、大事なことなので繰り返し言いますが、この設定画面も「管理者(Administrator)であり、運用者は目にすることのない画面」なのですね。この画面を見せても少なくとも運用者アピールにはなりません。

ディテールにこだわること

今回、要件を満たすために、当日の朝ギリギリまで作り込んでいました。当日、時間の関係で(変に盛り上がってしまった?関係で)、サイト運用者による実際のページ作成・更新が行えなかったのは残念ですが、すべての要件通りにきちんと動作するように作り込みつつ、細かい部分を画面だけでなく、プラグインによって要件通りに動作させる部分について相当作り込みました。標準機能ではPowerCMSも入っておらず「多機能」の逆ではありましたが、反対にシンプルな画面に必要な機能をコンパクトに凝縮させました。

ウェブページ編集画面

ウェブページ編集画面のカスタマイズポイントは主にこの5つ。

  1. 各言語のトップページを確認するための画面右上の国旗アイコン
  2. 入力順と表示順を同じにしたサブタイトル入力欄
  3. 言語タブの実装(タブの追加とラベルのカスタマイズ)
  4. Translator API - Microsoft Translatorを利用した翻訳テキストの挿入機能
  5. 言語ごとに公開制御できる機能

5の部分については、mt_entry_status_en/mt_entry_status_tw というカラムを追加して、値の有無でファイル書き出しの制御を行いつつ、各言語の一覧表示を制御するテンプレート・タグ(フィルタ)を作成しました。ただ、このあたりの裏側の仕組み(テンプレート・タグ)は、これも「管理者(Administrator、もしくはDesigner)であり、運用者は目にすることのない画面」ですから、あまりアピールせず、管理画面のチェックボックス実装のみさらっとアピールすることに。

<MTPages status_filter="$lang">
    各言語の公開フラグ付きページの一覧
</MTPages>

実際に動かしてみることの説得力

デモで見せようと思っていたのは2点。Microsoft Translator API を使ったリアルタイム機械翻訳と、翻訳データCSVファイルからの翻訳フレーズ一括登録>パブリッシュ>サイトの多言語化完了(5秒でサイトが多言語に変わる)の2点です。

翻訳CSVからの一括データ登録

Microsoft Translator API を使ったリアルタイム機械翻訳については、concrete5が数日前にプレスリリースを出していたのを見ていて、敢えて準備していきました。ここだけは、ま、狙ったところですね。CSVの件も機械翻訳の件も Drupal、Typo3、concrete5あたりからお話はありましたが、実際に動かしてみせたのは Movable Typeチームのみ。実際に動くことの説得力が効いたのは間違いないと思います。

その他、MT Studio(開発ツール)やPowerCMSを使ったカスタム・マスタ(テーブル)を作成するデモも行いましたが、何を実際に動作させてみせるかについては事前にある程度絞りこんでシナリオを作成しておきました。

もう一点(追記)、アイテム挿入の際の altテキストを各言語のものにするところも見せました。細かい所ですが、画像の代替テキストも多言語対応、という部分で敢えて他の陣営が触れなかった部分も作り込んでおいたあたりも細かなアピールに繋がったのかもしれません。

画像の挿入ダイアログの多言語対応

自信をもって演じきる、可能性を感じさせる

途中の質疑応答で最終で見せようと思っていたMT Studio(開発ツール)やPowerCMSを使ったカスタム・マスタ(テーブル)を作成するデモを見せてしまったので、最後はmacOSのデスクトップアプリからブログを実際に更新するデモ、CMSに登録済のアイテム(画像)をデスクトップアプリでリアルタイム編集するデモを行いました。ここだけアドリブですね。他は計画どおりです。最後は MTDataAPIDebuggerも紹介してデバッグツールもあること、1週間程度あれば投稿用アプリも作成できる可能性があることを自信持って言い切りました。

おまけ - 真面目に馬鹿なことをやってみせる

ひとつだけ、アドリブでネタを仕込みました。concrete5 のデモの際に、ドロップダウンで表示される対応言語数の多さ(スワヒリ語とか余計なこと言ったのはお詫びします :-p )にヒントを得て、いつぞやのエイプリルフールに作成したプラグインをインストールしてテンプレートにタグを一つ追加、アルファサードの経営理念を大阪弁に変換するデモを入れました。実はこれが一番ウケたのだったりして...

アルファサードの経営理念を大阪弁化してみた。アルファサードはテクノロジー(web技術)の会社や。主役はお客様であり、パートナー様であり、うちらの提供する製品やサービスを用いて作られ運営されるwebサイトでありアプリケーションや。うちらは主役ではなく裏方や。うちらは、うちらとともに何ぞを生み出し、育てたいと思ってくださるお客様とパートナー様に寄り添いまんねんわ。 また、お客様とパートナー様が提供するwebサイトやアプリケーションが成功することでお客様の利益に貢献し、お客様とパートナー様とともにうちらが成長すること。また、設立以来一貫して取り組んできたwebアクセシビリティ技術を通じて「どなたはんもが状況やデバイスや障害の有無を問わず情報にアクセスできる社会の実現に寄与する」こと。これがうちらの存在意義や。

これは流石にジョークプラグインではありますが、こんなことも実現できるんだよってアピールには繋がったのかもしれません。あとは会場の空気を変える効果も。ホンマになんでもやりよんな! こいつ と思わせることに繋げられたことは間違いないでしょう。

最後に、ベテラン、慣れてるから(野田さんだから)できる、への反論

ひとことでいうと、これを自分でなくてもできるように、という発想から作ったのが PowerCMS なのです。確かにコストはかかりますが、コストをかけることで、ノウハウと時間を買えるということを理解していただけたらと思います。

※これ、今回MTの実装としてはよくできたと思うので、コミュニティの勉強会の材料にしたいと思っています。どなたか企画してくれませんかね(チラッ

第2回CMSプロレス 多言語サイトタイトルマッチ - CMS SUNDAY | Doorkeeper

CMSプロレス

参加中です。WordPressさんのお隣です。なんか前半は文化の違いで空気に馴染めず難しかったですけど。

CMSプロレスとは?

  • 同じ仕様のサイトを複数のCMSで構築して競うエンターテイメント性の高いイベント
  • 第一線の開発者達がどのように実装したのかを解説つきで観戦
  • CMSの体育祭!

よくわからないという方はこちらをご覧ください!

 昨年のCMSプロレスの開催レポートはこちら
 ※昨年は、各CMSの代表チームが同じ仕様のウェブサイトを制限時間内に構築するライブコーディングイベントと銘打っていましたが、今回は、事前に構築したウェブサイトをもとにしたプレゼンバトルです

先日、Movable Type6.3がリリースされました。

一番の変更点はダイナミックパブリッシングのPHP7サポートですが、内部的には Smartyのバージョンが2系から3系に変更になっています(PowerCMS/DynamicMTMLの対応は少々お待ち下さい)。

PHP 7 をサポート

PHP 7 を正式にサポートしました。PHP 7 は、PHP 5系と比べてもパフォーマンスの向上が期待できます。

Smarty 3.1 へアップデート

PHP 7 への対応に伴い、Smarty 2 から Smarty 3 へのアップデートされました。

プラグイン開発者へ

ダイナミック・パブリッシングに対応したプラグインについて、直接 Smarty を操作していないかぎりにおいて変更の必要は無いものと想定していますが、動作確認等よろしくお願いいたします。

※(再度) PowerCMS/DynamicMTMLの対応は少々お待ち下さい

MTViewerとは、$ctx であり Smartyである

何のことか分かる人にしかわからないアレゲな書き方ですが、つまるところMTにおけるダイナミック・パブリッシングとはMTのテンプレートをSmartyテンプレートにコンバートしてビルドするという仕組みになっています。実体は mt/php/lib/MTViewer.php です。

MTViewer.phpのコード

この MTViewerクラスの fetchメソッドがテンプレートをビルドするコードなのですが、MTタグをPHPの中で動的にビルドする方法について備忘録がてらメモしておきます。ちなみに、元がSmartyですので、これらはSmartyのテンプレートを渡しても動作します。MTテンプレートを指定した場合、mt/php/lib/prefilter.mt_to_smarty.php がMTのテンプレートをSmartyのテンプレートに変換してからビルドされます。というわけでMTViewerクラスの fetchメソッドにはMTタグをそのまま渡せるのです。

MTテンプレートをPHPコードの中でビルドする

念のためですが、$ctx の初期化については下記の記事を参照ください。

template_id 指定でビルド

$mt = MT::get_instance();
$ctx =& $mt->context();
$str = $ctx->fetch("mt:123");

テンプレート名(template_name) 指定でビルド

$mt = MT::get_instance();
$ctx =& $mt->context();
$str = $ctx->fetch("mt:テンプレート名");

テンプレート指定のビルドについては mt/php/lib/resource.mt.php で定義されています(Smartyのカスタムリソース)。 Smartyのカスタムリソースの作り方は以下のページに。カスタムリソースを作成すれば、例えば S3 に置いたMTテンプレートを指定してビルド、とかまぁ色々できるわけですね。

MTテンプレート文字列指定でビルド

$mt = MT::get_instance();
$ctx =& $mt->context();
$str = $ctx->fetch("eval:<mt:Date>");

ファイルパス指定でビルド

$mt = MT::get_instance();
$ctx =& $mt->context();
$str = $ctx->fetch("file:/path/to/tmpl.tpl");

テンプレート変数をセットしてビルド

$mt = MT::get_instance();
$ctx =& $mt->context();
$ctx->__stash[ 'vars' ][ 'foo' ] = 'bar';
$str = $ctx->fetch('eval:<mt:Var name="foo">');

記事コンテキストをセットしてビルド

$mt = MT::get_instance();
$ctx =& $mt->context();
$entry = $mt->db()->fetch_entry( 1 );
$ctx->stash( 'entry', $entry );
$str = $ctx->fetch('eval:<mt:EntryTitle>');

この辺りを理解しておくと、PHPでサイトを拡張していく際にMTテンプレートが活用できるようになります。例えばPHPで作成したフォームメールのテンプレートにMTタグを使ったり。ということでMT6.3の Tips でした。

既にPowerCMSでやっているものなのですが、再整理の意味で、メモ(てか、ブログサボってたのでリハビリがてらw)。

config.yaml

permissions:
    system.access_cms:
        label: Can Access CMS
        order: 2000
    system.administer:
        inherit_from:
            - system.access_cms

これで、ユーザーの編集画面にシステム権限「Can Access CMS(CMSへのアクセス)」が追加して表示されます。system.administer... 以下を追加しているので、「システム管理者」にチェックを入れると「Can Access CMS(CMSへのアクセス)」にもチェックが入ります。

管理画面のシステム権限設定エリア

ただ、このまま「保存」しても「MT::Authorに can_access_cms メソッドがない」というエラーが出ます。社内で聞いてみたところ「アクセサを定義する必要があります。」とのこと。

config.yaml

callbacks:
    init_app: $myplugin::MyPlugin::Plugin::_cb_init_app

MyPlugin::Plugin::_cb_init_app

sub _cb_init_app {
    require MT::Author;
    *MT::Author::can_access_cms = sub {
        my $author = shift;
        if ( @_ ) {
            $author->permissions(0)->can_access_cms( @_ );
        }
        else {
            $author->is_superuser()
                || $author->permissions(0)->can_access_cms( @_ );
        }
    };
}

これで、あとはMT側がよしなに処理してくれます。もちろん、この権限の有無による振る舞いの分岐はそれぞれの処理に実装する必要があります。

人気記事を表示するのに GoogleAnalyticsを使ってページビューの順に表示させるという方法(PowerCMSなど)がありますがコメント機能が使われていてコメントが活発にやりとりされているサイトの場合、最近 n日のコメント数を基準にするという方法もありますね。話題になっている記事を抽出するのになかなか適した方法かと思います。

https://github.com/junnama/mt-plugin-lively-discussion-entries

<mt:LivelyDiscussionEntries days="7">
# mt:Entriesと同じタグが利用できる
</mt:LivelyDiscussionEntries>

このような処理の時は MT::Object の count_group_byメソッドを使います。

my $group_iter = MT->model( 'comment' )->count_group_by( {
                                            blog_id => $blog_id,
                                            visible => 1,
                                            created_on => { '>' => $ts } } , {
                                            (),
                                            group => [ 'entry_id' ],} );
my $ids = {};
while ( my ( $count, $entry_id ) = $group_iter->() ) {
    $ids->{ $entry_id } = $count;
}
my $i = 0;
my @entries;
for my $entry_id (sort { $ids->{ $b } <=> $ids->{ $a } } keys %$ids) {
    my $entry = MT->model( 'entry' )->load( $entry_id );
    if ( $entry && $entry->status == MT::Entry::RELEASE() ) {
        push ( @entries, $entry );
        $i++;
        if ( $i >= $lastn ) {
            last;
        }
    }
}

この記事は、「やはりお前らのMTMLは間違っている」シリーズ連載第n回目の連載です。

12303936_1018612871539819_7627081906549621209_o

MTタグが数千、下記の例では7500個書いてあるテンプレートの話しです。

    <mt:SetVar name="master_table" key="12000" value="JR山手線">
    <mt:SetVar name="master_table" key="19200" value="東京メトロ千代田線">
    ....同じような記述が7500個くらい記述が続く

DB接続するようなMTタグが一切無いにも関わらず、再構築に数十秒かかります(手元環境では30秒から45秒)。MTSpeenMeterタグを使うとビルドにかかっているのはたかだか3〜4秒。

であれば、ビルドに入る前段階で時間を食っていると考えるのが自然。

時間がかかっているのは MT::Builder の sub compile{}です。要するに正規表現でMTタグをパースする処理。MTタグが多ければ多いほどここに時間がかかります。

要するに、PHPでも以下紹介した事象がスタティックでも発生するということです。しかもPerl、スタティックのパース結果はキャッシュされない(同一リクエスト内ではキャッシュされる模様)。

対策は、MTSetVars、MTSetHashVarsを使うか、素直にデータベースを使う

MTタグの数が少なければパース時間は短くなるため、MTSetVars、MTSetHashVarsを使って纏めて記載する。

でも、この手のデータだったらカスタムオブジェクトでも何でも良いけど素直にデータベース管理が良いと思います。

こんなページができていて、いい傾向じゃないの(上から目線w)、と思ってまして。

ツッコミと言うわけではないですが、僕も最近色々思ってたことが触れられてたので。

ま、同じことなんですけど、何でこういうエントリを書いてるかというと、PerlでMTMLやHTMLを突っ込むのについついPerlのヒアドキュメントを使っちゃうことがあるからなのです。ひとつのファイルで完結するからどうしても手を抜いてそういう実装をしがちなのですが。

僕なら、テンプレートを tmpl/include に置いた上でこう書きます。

sub template_param_edit_entry {
    my ( $cb, $app, $param, $tmpl ) = @_;
    $app->{ plugin_template_path } =
        File::Spec->catfile( MT->component( 'AwesomePlugin' )->path, 'tmpl' );

    my ( $node, $attr, $include );

    $node = $tmpl->getElementById( 'header_include' );
    $attr = { name => 'include/edit_entry_header.tmpl' };
    $include = $tmpl->createElement( 'include', $attr );
    $tmpl->insertBefore( $include, $node );

    $node = $tmpl->getElementById( 'title' );
    $attr = { name => 'include/edit_entry_title.tmpl' };
    $include = $tmpl->createElement( 'include', $attr );
    $tmpl->insertBefore( $include, $node );
}

DOMに慣れている方なら直感的かと思うし。どっちがいいということではなくて、自分が理解しやすい方法で書けばいいと思います、ええ。

※どっちかと言えば各MTタグにidが振られてないとかそういうのが問題かと思う(一回バージョンアップでidが変わったことあるぜ!!)

追記:天野さんから指摘あり。

plugintemplatepath を使う方法は、複数のプラグインで同じことをしようとした時に片方が効かなくなる。

さらに追記

    $app->{ plugin_template_path } =
        File::Spec->catfile( MT->component( 'AwesomePlugin' )->path, 'tmpl' );

    # の代わりに

    $attr = { name => 'include/edit_entry_header.tmpl',
              component => 'AwesomePlugin' };

とする。

sort_by sort_order 指定してみたんだけど、効かない。

  • MT::Template::Tags::Blog::_hdlr_blogs

ハードコーディングされてるんだから。。。

$args{'sort'} = 'name';
$args{direction} = 'ascend';
my @blogs = MT::Blog->load( \%terms, \%args );

こういうところを何とかして欲しいものです。

<mt:Websites>
<mt:WebsiteId setvar="website_id">
<mt:SetHashVars name="website_data">
    name=<mt:WebsiteName>
    url=<mt:WebsiteURL>
</mt:SetHashVars>
<mt:SetHashVar name="websites">
    <mt:SetVar name="$website_id" value="$website_data">
</mt:SetHashVar>
</mt:Websites>
<mt:Loop name="websites" sort_by="key" >
    blog_id : <mt:Var name="__key__">
    blog_name : <mt:Var name="__value__" key="name">
    blog_url : <mt:Var name="__value__" key="url">
</mt:Loop>

追記 : blog_id が10超えるときは 別途並び順用の変数にキーのみ入れて MTLoop sort_by="value numeric" というツッコミが入りました。誰か修正版やっといて。。

追記: GetHashVarプラグインを使うか ObjectLoop プラグインを使えば素直に解決できる

<mt:LoopWithSort name="websites" kind="num" scope="key" order="ascend">
</mt:LoopWithSort>

<mt:ObjectLoop model="website" sort_by="id" sort_order="ascend">
</mt:ObjectLoop>

PowerCMS の Pager や PageBute を利用したページ分割は非常に効率が悪いので記事の件数が半端無く多いシチュエーションでは使わない方が絶対にいいです。

記事の更新頻度によって月別、年度別、年別等の time-basedなアーカイブにしたほうがいい。理由は、time-basedなアーカイブは更新対象のアーカイブのみが再構築されるためです。ページングって、1件増えたら、1件削除されたら全アーカイブが1件だけ「ずれて」全再構築されるのです。1件更新されても全アーカイブが再構築対象になります。

というようなことを口を酸っぱくして言ってんのに、結局破綻してしまったというケースに出くわしたので作成しました。

これ、カテゴリとかもニーズあるよね、MT7では標準機能になるよね、と振るだけ振っておいて。

アーカイブマッピングで「手動」を選択

  • https://github.com/alfasado/mt-plugin-monthly-entries-publisher
  • プラグインをインストール
  • ウェブサイト(ブログでも良い)月別アーカイブを作成し、include_blogsで含めるブログを指定する
  • テンプレートマップの再構築オプションは「手動」とする(「再構築しない」を選択しないこと:「再構築しない」だと書き出されない)
  • MTEntriesの__first__ もしくは MTEntriesHeader 内に 「<!--EntriesCount:<mt:EntriesCount>-->」記述を入れる(ファイル出力時にはこの値は削除されます)
  • mt-config.cgi に MonthlyEntriesPublisherTemplateIds テンプレートのIDカンマ区切りを記載
  • mt-config.cgi に MonthlyEntriesPublisherBlogIds として、記事保存時にキューに入れる対象のブログIDをカンマ区切りで記載

再構築はキューで実行されます。ダイナミックパブリッシングには対応していません。

Data APIから1日数千件のエントリを更新、新規追加するというような要件の案件で現場から「スピードがでないんっすよ」ということでヘルプ要請が来たのですが、「CGIの起動がクソ重いんじゃないの?」ということで複数件のJSONを纏めて投げられるエンドポイントを作成して対応しようとしたのですが、それでもまっっっったくと言っていいほどパフォーマンスの改善が見られない。再構築をキューにして凌ごうと思ったのですがそれでも一件 Insert するのに10秒も20秒もかかるのです。

  • Data APIで記事インサート、アップデート
  • 1件10〜20秒を要する

調べたところ、こいつが問題。

  • 記事カスタムフィールドが200個近く!

コードを見たり、SQLのクエリを見たり。犯人見つけましたぜ。

MTのカスタムフィールドは1フィールド1レコードで、1フィールド1SQLを発行するのです

なのです。こんなこと知ってたらカスタムフィールド200個近くも作成しないですよね? 5件単位でインサートだったらSQL1000クエリ、カスタムフィールドのためだけに投げるわけです。

なので、MySQLの Bulk Insertを使うようにしてみたらめっちゃ速くなったよというお話でした。単位は秒。Updateの場合は Insertの倍のスピードになります。SQLの数が半端ないので、1台構成より DBとCMSがわかれている複数台構成の方が効果は顕著です。

カスタムフィールドをBulk Insertにしたら300%以上高速化されたという話し(効果測定結果)

ObjectDriver が DBI::mysql で、mt-config.cgiに「BulkInsertMeta」環境変数を指定しているときのみ有効になります。あくまでも実験的な位置づけなのでノンサポート、自己責任でお願いします。

/mt/lib/MT/Meta/Proxy.pm

Oracle や SQL Serverに対応できていないので、その辺りが課題ですが、いずれにしても MTには足回りの強化をお願いしたいところです(既にお願いはしておいたんですがね)。

ま、回答数が少ないんだけど半数以上があるという回答ですね。「ある」人がこういうのに反応したんだと捉えることもできるけど、これ、僕が目にしたからこういうポストしたんですね。同じ案件で3回。

MTのアーカイブの柔軟なところは評価されていいんですが、デフォルトだとウェブページのアーカイブマッピングは以下のようになっています。

デフォルトのウェブページのアーカイブマッピング

%-c/%-f

フォルダの指定を忘れて、出力ファイル名を「index」にしてしまうと...

メインページを上書きしてしまうではないですか。

ウェブページのパス指定箇所

マニュアルに書いたでしょ? とか言わずにこういうのはシステムの方でミスを防いでやるのがスマートですよね?

plugins/NoOverwriteMainIndex/config.yaml を置いて(フォルダ名は何でもいいけど)、以下の短いコードをぺたっとするだけです。

プラグインでメインページを上書きしないように設定できる

こういう小さな工夫が使い勝手を向上させ、オペレーションミスによる問題を防いでくれる、ひいては顧客からの質問攻めによる負荷を下げてくれるってもんだと思います。

title要素やHTMLの属性値に出力する箇所

属性値やtitle属性中にはタグが使えないため。

<img src="<mt:AssetURL>" alt="<mt:ImageAlt escape="html">" />

または

<img src="<mt:AssetURL>" alt="<mt:ImageAlt remove_html="1" escape="html">" />

H1要素など、HTMLを含めることが可能な箇所

改行を含めたい等のケースがあるので要件、サイトのルールに応じて。

<h1><mt:PageTitle></h1>

URLを入力するカスタムフィールドの場合

trimを指定するのはフィールドへのコピペで前後に空白が入る可能性があるため。

<mt:ImageLinkURL trim="1" encode_url="1">

外部ユーザーが改ざん可能な値を出力する時

XSSに繋がるため

<mt:Var name="request.foo" escape="html">

MTSetVarBlock - テンプレートタグリファレンス

このブロックタグで囲んだ内容を値にした変数を設定します。このタグの中ではテンプレートタグが利用できます。変数を呼び出すときは、MTGetVar, MTVar ファンクションタグを使います。

MTSetVarTemplate ブロックタグとの違いは、囲んだ内容が処理された結果が値となるか、囲んだ内容が処理されずテンプレートのまま値となるかです。MTSetVarBlock ブロックタグでは囲んだ内容が処理された結果が値となります。

設定した値は $name_foo として、モディファイアの値としても利用することができます。

MTSetVarTemplate - テンプレートタグリファレンス

このブロックタグで囲んだ内容を値にした変数を設定します。変数を呼び出すときは、MTGetVar, MTVar ファンクションタグを使います。

MTSetVarBlock ブロックタグとの違いは、囲んだ内容が処理された結果が値となるか、囲んだ内容が処理されずテンプレートのまま値となるかです。MTSetVarTemplate ブロックタグでは囲んだ内容が処理されずテンプレートがそのまま値となり、MTGetVar, MTVar ファンクションタグで呼び出されたテンプレートで処理をおこないます。

使いどころが良くわからない的な話しがあるようですが、ひとつわかりやすい例を。

ナビゲーションブロックの組み立てのための Loop 系タグの呼び出しを1回にまとめる

グローバルナビ、サイドナビ、フッターの3カ所に MTTopLevelCategories が使われているとします。良くある話しですよね? でもHTMLの形は違うと。

ワイヤーフレーム

この時、MTTopLevelCategories の呼び出しを一回にまとめたい。だってその方が速いし。カテゴリが多く、テンプレートに MTTopLevelCategories が3回も4回も呼ばれてるケースなら相当速くなりますよ。

以下のようにすればループは一回で済みます。モジュール化して呼び出すときはキャッシュに注意ね。キャッシュされたテンプレートは(キャッシュヒットすると)評価されないので変数にセットされません。

<mt:TopLevelCategories>
<mt:SetvarBlock name="カテゴリリスト・グローバルナビ">
    カテゴリリスト・グローバルナビのMTML
</mt:SetvarBlock>
<mt:SetvarBlock name="カテゴリリスト・サイドナビ">
    カテゴリリスト・サイドナビのMTML
</mt:SetvarBlock>
<mt:SetvarBlock name="カテゴリリスト・フッタナビ">
    カテゴリリスト・フッタナビのMTML
</mt:SetvarBlock>
</mt:TopLevelCategories>
# 呼び出し
<mt:Var name="カテゴリリスト・グローバルナビ">
<mt:Var name="カテゴリリスト・サイドナビ">
<mt:Var name="カテゴリリスト・フッタナビ">

もし各ブロックのHTMLの違いが一部である場合、例えば下記のような場合は MTSetVarTemplate の出番です(class名だけが違うなど)。

<mt:SetVarTemplate name="カテゴリリスト">
    <mt:SubCatIsFirst><ul class="<mt:Var name="nav_class_name">-list"></mt:SubCatIsFirst>
    <mt:If tag="CategoryCount">
    <li class="widget-list-item"><a href="<$mt:CategoryArchiveLink$>"><$mt:CategoryLabel$></a>
    <mt:Else>
    <li class="widget-list-item"><$mt:CategoryLabel$>
    </mt:If>
    <$mt:SubCatsRecurse$>
    </li>
    <mt:SubCatIsLast></ul></mt:SubCatIsLast>
</mt:SetVarTemplate>

<mt:TopLevelCategories>
    <mt:Var name="カテゴリリスト" nav_class_name="gnavi" setvar="カテゴリリスト・グローバルナビ">
    <mt:Var name="カテゴリリスト" nav_class_name="snavi" setvar="カテゴリリスト・サイドナビ">
    <mt:Var name="カテゴリリスト" nav_class_name="fnavi" setvar="カテゴリリスト・フッタナビ">
</mt:TopLevelCategories>
# 呼び出し
<mt:Var name="カテゴリリスト・グローバルナビ">
<mt:Var name="カテゴリリスト・サイドナビ">
<mt:Var name="カテゴリリスト・フッタナビ">

ちょっと自分でもわかりにくいのを先日書いてしまったので懺悔の意味で書いた。後悔はしていない。

ここが変だよ Movable Type シリーズの続きです。「ニュースカテゴリに属する記事をトップページに最新10件表示して」で、後で追加で「非表示フラグ設定できるようにしてよ」って。もう、あるある過ぎますよね。

そこで、みんな大好きカスタムフィールド。チェックボックスフィールドをひとつ作ってフラグ付きを除外するとか、やるじゃないですか。で、以下のようにする。

チェックボックスカスタムフィールドを追加する

<mt:Entries limit="10" field:entrynotop="0" category="ニュース">

</mt:Entries>

実はこいつは上手くいかない。

上手く行きますよね? 上手く行くと思うでしょ? でもこれ実は上手く行かないんだよ既に記事が入っていると。なぜならカスタムフィールドを追加で記事に対して作成しても、既に存在する記事フィールドには値は入ってくれんのである。じゃぁ、「表示フラグ」に変更して、初期値を1(checked) にしたら、ってこれも同じですわ。ね、やはりお前らのMTは間違っているでしょ?

そんな時のために、案件開始時には mt_dir/tools の下にひとつスクリプトを置いておくといいです。クライアント名とかプロジェクト名とかサイト名でいいです。あなたがビートルズのサイトを構築しているなら

mt_dir/tools/Beatles

のようなファイルです。ファイルの内容は以下でいいです。気になる人は package MT::Tool::Foo; を package MT::Tool:: Beatles; とかに変更すればいい。

https://gist.github.com/alfasado/4167792

#!/usr/bin/perl
package MT::Tool::Foo;
use strict;
use warnings;
use File::Spec;
use FindBin;
use lib map File::Spec->catdir( $FindBin::Bin, File::Spec->updir, $_ ), qw/lib extlib/;
use base qw( MT::Tool );

sub usage { '--debug 1' }

sub help {
    return q {
        Description Foo.
        --debug 1
    };
}

my ( $debug );

sub options {
    return (
        'debug=s'   => \$debug,
    );
}

sub main {
    my $class = shift;
    my ( $verbose ) = $class->SUPER::main( @_ );
    if ( $debug ) {
        print 'Some debug message.' ."\n";
    }
    # Do something.
    1;
}

__PACKAGE__->main() unless caller;

で、今回の要件なら main サブルーチンに以下のように書いて,

my $field = 'field.entrynotop';
my $iter = MT::Entry->load_iter( { class => 'entry' } );
while ( my $obj = $iter->() ) {
    if (! $obj->$field ) {
        $obj->$f( 0 );
        $obj->save or die $obj->errstr;
    }
}

サーバーにログインして以下を実行すればOK。今後追加される記事は値が空になることはないしね。

$ cd path/to/mt_dir 
$ perl tools/Beatles

他にも色々使えるよ。

  • ブログで作成していたテンプレートを全部グローバルテンプレートに移し替えたい
  • カテゴリの指定が聞いてた話しと途中で変わってしまった
  • 大量のファイルをアップしておいてまとめてアイテムに登録したい
  • アップしちゃいけないファイルがアップされているので纏めて削除したいがアイテムが1万件くらいある

とかね。まー要件変更とかあるあるだから、スクリプト置いておいて簡単な処理が書けるようにしておけば、芸は身を助けますよ。これ本当。

KeynoteScreenSnapz001

この記事は Movable Type Advent Calendar 2015 の最終日の記事です。

いや、今年も色々ありましたが実は地味に今年のトピックは Movable Type でテンプレートをフル実装する機会に恵まれたことですね。自分のブログ以外ではそんな機会はこれまでなかったのです。しかも PowerCMS じゃありません。予算の関係もあり、今回は Movable Type で行きましょう、ということになったのです。

本題に入る前に少し MTDDC の続きを

日本語変数名の使いどころ

日本語変数名の話しを少ししたよね。実際に有用なシーンに出会ったので紹介しておく。多言語サイトで。

<mt:BlogLanguage setvar="language">
<mt:If name="language" eq="ja">
    <mt:Setvars>
        サイト・パス=/jp/
        ホーム=ホーム
        最新ニュース=最新ニュース
        関連ニュース=関連ニュース
        年度=年度
        キーワード検索=キーワード検索
        サイト内検索=サイト内検索
    </mt:Setvars>
<mt:Else>
    <mt:Setvars>
        サイト・パス=/en/
        ホーム=Home
        最新ニュース=Latest News
        関連ニュース=Related News
        年度=Fiscal
        キーワード検索=Keyword Search
        サイト内検索=Search
    </mt:Setvars>
</mt:If>

<h1><mt:Var name="キーワード検索"></h1>

超わかりやすくないですかい? <mt:Var name="keyword_search_title"> みたいにわざわざ指定する必要ないんだから。同じテンプレートで行けるし、わざわざたくさんのカスタムフィールドを作る必要もない。比較や変数渡しの必要がなければ日本語でOK。

SetVarTemplate の使いどころ

Movable Type Advent Calendar の初日で BUNさんがちょうど書いていましたが、function(関数)のような形で使いまわしできるのがこのタグの良いところです。

複雑な分岐ロジックでなくとも、以下のようなテンプレートを直に分岐等を入れながら書くと、コーディングに修正が入った時の修正箇所が多くなります。

<mt:SetvarTemplate name="breadcrumbs_tmpl" note="パンくずのマークアップ">
<mt:Unless name="no_image"><img class="breadcrumb" src="/assets/img/common/breadcrumb.png" alt=""></mt:Unless>
<mt:If name="breadcrumbs_url">
<a class="breadcrumb" href="<mt:Var name="breadcrumbs_url">">
</mt:If>
<mt:Var name="breadcrumbs_title">
<mt:If name="breadcrumbs_url"></a>
</mt:If>
</mt:SetvarTemplate>

<mt:Var name="breadcrumbs_tmpl" breadcrumbs_url="$url" breadcrumbs_title="$title">

(※コーディングがどうとか言うなよ。俺が書いたんじゃねーんだ。) 要するに、URLとタイトル、画像の有無を渡せばパンくずのマークアップで出力できるというもの。

ここから本題。やはりお前らのMTは間違っている!

タイトルは釣り本気ですが、おおまかなテンプレート構築のフローを書きながらどんな風に作ったのかご紹介しつつ、何が間違っているのかについて考えていきましょう。

※ここで紹介したツール群は、年始に PowerCMS およびそれ以外の方法で(単品やクラウドサービスなど)でご提供する予定です。一部については既に公開しているものもあります。

コーディング素材を受け取って、MTのテンプレートに登録する

まず、制作会社から上がってきたコーディングデータと画像等の素材一式をMTのインデックステンプレートやアイテムに登録します。いくらなんでも自分でチマチマやるのは何なので、新人のデザイナーに「ここからこうやって登録...」といいかけて思ったのです。ハッ! いくら新人でもこんな単純作業に時間を割かせて良いものだろうか。今回の案件ではコーディングに SSIを利用していました。公開サイトでは動的なしくみは使えない制約があったので、ま、MTIncludeにすればええやと思いつつ...

納品されたコーディングデータ素材

とりあえずプラグインを作るところから始めました。でき上がったのがこいつです。

共通アイテム登録画面

ZIPファイルに素材を固めて管理画面からアップロードすると拡張子によってテキストファイル系はテンプレートに(インデックスか、テンプレートモジュールに)、画像等はアイテムに(@commonタグ付きで)登録してくれるものです。インクルードファイルをモジュールに、インクルード元ファイルをインデックス・テンプレートに登録すると、ちゃんと MTIncludeに変換してくれるんですぜ。

テンプレート編集画面の不便いろいろ

これはもう色々あるんですが、最初に大きな声で言っておきたいのは「お前ら声が小さい」「お前ら文字が小さい!」。デフォルトのフォントサイズは14ポイントです。で、これ設定とかで変えられません。アクセシビリティを何だと思ってるのか。年寄りを何だと思っているのか。

ということで次に作ったのがこいつです。ついでに最新の CodeMirror を入れてキーバインディングやスキンを変更できるようにしました。で、僕は MTのテンプレートにおいては「行を折り返す派」です!

テンプレート編集の設定

どうです? 見やすくなったでしょ?

改良後のテンプレートエディタ

次に手をつけたのはウィジェットとメモ欄です。メモ欄のことは MTDDCで話したので今日は詳しく触れませんが、ここ ( alfasado/mt-plugin-template-note ) からダウンロードできます改良されたテンプレート編集画面

しかしそれよりも何よりも「インクルードテンプレート」ウィジェットがあるのに「インクルードされているテンプレート」ウィジェットがないじゃん!

ということで次にこれを作りました。全テンプレートをロードするのでこれ、非同期読み込みです。ええ、これ書くために jQuery 勉強したんだぜマジで。

リンクされているテンプレートウィジェット

これで完璧だよね、とか自己満足して作業進めてたらそれでも足りない何かに気づいたのです。グローバルテンプレートへの移動が不便! そんなこともあろうかと、こいつ ((MT5)ユーザーダッシュボードに左メニューを追加する。 - Junnama Online) を入れておいたのです。でれでもテンプレート編集してる時だし移動したいのは。ということで、次にこれ作ったのさ。テンプレートをブックマーク保存できて、一覧画面、編集画面の右側のウィジェットに表示されるように。

ブックマークウィジェット

グローバルテンプレートの改良

(追記)グローバルテンプレート嫌いな人いるよとなんてうだけど、多言語サイトの共通テンプレートとかどこかのブログに置くのも気持ち悪いし、ブログIDが開発と本番で変わることってざらにあるのだからやはりそういうのはグローバルテンプレートが素直だと思う。ウェブサイトの配下にブログって手もあるけど、ひとつのサイトなのにブログ数が多いのってスマートじゃないと思う。いろんな意味で。(追記ここまで)

Twitter 検索してみたら色々出てくるじゃん。

あれ、途中からリビジョンの話しになったよ。グローバルテンプレートにはリビジョンがない。

2015年ですぜこれ。MT Studioで対応してたので、そいつを切り出して移植。ついでに Diffを見られるようにした(グローバルテンプレートに限らない)。

グローバルテンプレートのリビジョン対応

続いてこれ。

これ見たの、東京から新大阪へ向かう新幹線の中。京都に着いたあたりで見たんだよね。で、新大阪に着くまでにかけるやろかと思って、書けた。

シェルで、また敷居が高いとか何とか、ちゃんとUIも作っといた。

テンプレート一覧画面から実行できる

てか、そもそもテーマが当てられた時にリビジョンが保存されるようにしておいた。

これもなぁ。。。多分MT4で導入された MTInclude多様のテンプレートでページの先頭に空行たくさん入る問題の解決のためなんだろうが、これこそ安易な解決をするな、の好例だと思う。これについての対処としては、コメントタグを先頭に入れて正規表現で置換と言う美しくない解決を(今回は)した。

納品ドキュメントを作りやすく

乗ってきたのでついでに作った。というか上半期末の全社会議での社員の発表で話題に上がっていたので作った。テンプレートに「メモ」を付けておけば、それを含めて一覧に表示してくれる。HTMLで生成したけど、PDF化すればいいんじゃないかと(CSVも考えたけどこっちのほうが現実的っぽかった)。

テンプレートの納品ドキュメント

いい加減疲れてきた。他にも色々作った。他の Advent Calendar に書いたネタ( Authoring Tool Accessibility Guidelines の観点から CMSの画像挿入フローを検証する - Junnama Online )ですが、リッチテキストエディタと画像挿入のところを何とかしたくて リッチテキストエディタ及びエディタへのアイテム挿入に関する機能を拡張する Advanced Editorというプラグインを作った。

だいたい、記事編集画面の title要素は何だ ?

この画面キャプチャ見て何とも思いませんか?

記事編集画面の title要素が不適切。全ての記事で「記事の編集 - ブログ名 - ソフト名」となっている

アクセシビリティもそうだし、そもそもウィンドウやタブをいっぱい開いて作業してる時に何が何だかわかんないと思うんだがこれ、いつからなの? 誰も指摘しなかったのだろうか。プラグイン書いといたので、ご自由に使ってください。

ヒントは現場に転がっているし、声を上げることがコミュニティへの貢献に繋がる

このエントリで言いたかったことは、まさにこの一文なのです。MTQ でも FogBugz でも日本語でもOK。お前ら本当に Data APIが欲しかったの? いや、 Data APIが駄目だとか言ってるわけではなくて、普段 Movable Type 乗りまわしてるのってテンプレート、サイト構築してる我々じゃないか。何も考えずに目の前の仕事を進めるのではなく、考えるヒントは日常の仕事の中に転がっている。“事件は会議室で起こっているんじゃない、現場で起こってるんだ” ってことを再認識した師走の午後でした。

それでは、メリークリスマス、良いお年をお迎えください。

この記事は Web Accessibility Advent Calendar 2015 の19日目の記事です。

職業柄 CMS を扱うことが日常になっているわけですが、私たちにはCMSから出力されるコンテンツのウェブアクセシビリティを確保することが求められます。

テンプレートをアクセシブルにすれば後はコンテンツ登録者に委ねられるというのは一見最もな主張に聞こえますが、アクセシブルなコンテンツ入力がしやすいよう、もしくはそこにコンテンツ登録者が注意を払えるように入力インターフェイスを設計するのはCMS開発者の義務だと言えるでしょう。

W3Cには Authoring Tool Accessibility Guidelines (以後ATAG) というガイドラインがあります。文字通り、オーサリングツールのアクセシビリティに関するガイドラインです。

このガイドラインでは、オーサリングツールそのものがアクセシブルであることはもちろん、オーザリングツールがアクセシブルなコンテンツを作成できるようにすること、そのためのガイドラインが前者(オーサリングツールそのものがアクセシブルであること)については別の機会に譲り(ここが難しくて、自分たちもできていないという認識はあります)、ここでは後者、「オーザリングツールがアクセシブルなコンテンツを作成できるようにする」部分、しかも一点、画像の代替テキストを指定するためのCMSの実装について少しだけ考察してみます。

ATAGには Guideline B.2.3: Assist authors with managing alternative (非テキストコンテンツの代替コンテンツを管理して著者を支援する) とあります。ざっくり紹介します(訳には誤りがある可能性があります)。

  • 不適切に生成された代替コンテンツは、ウェブコンテンツのアクセシビリティの問題を発生させ、アクセシビリティチェックを妨げる可能性があります。
  • 代替コンテンツが編集可能であること。オーサリングツールが非テキストコンテンツを追加するための機能を提供する場合は、コンテンツ作成者は、非テキストコンテンツのためのプログラムで関連付けられている代替テキストを変更することができます。
  • 非テキストコンテンツが、装飾、フォーマッティング、不可視またはCAPTCHAである時に例外を指定できること。
  • 代替テキスト生成を自動化する場合、例えば「画像」や、ファイル名、ファイル形式等であってはいけません。また、自動で生成されることをコンテンツ作成者が拒否できるようにしてください(勝手に自動生成しない、キャンセルできる)。
  • 指定した代替テキストは保存されており、同じ非テキストコンテンツが再利用された場合に代替テキストが自動的にオーサリングツールによって提案されています。そしてその保存された代替テキストを編集または削除するオプションがあります。

WYSIWYG リッチテキストエディタでは代替テキストを確認できない

WYSIWYGリッチテキストエディタ

CMSの多くで採用されている WYSIWYG リッチテキストエディタでは画像の代替テキストを視覚的に確認することができません。WYSIWYG エディタは ATAG の対象とされています。

WYSIWYG エディタにおける画像挿入の課題は以下の2点に集約されると思います。

  • 画像挿入時における代替テキストの指定インターフェイス
  • 画像挿入後の代替テキストの確認、修正方法

WordPress の画像の挿入フロー

WordPress のファイルアップロードインターフェイス

さて、ここではCMSへの画像の挿入フローの例として、WordPress と Movable Type を取り上げます。まずは WordPress。最新のWordPress(4.4)では、投稿画面からメディアの挿入を選択した後、ドラッグ&ドロップで画像をアップロードできます(もちろんファイル選択してアップロードすることもできます。ドラッグ&ドロップのみではそれ自体が非アクセシブルになってしまいます)。

WordPress のメディア挿入インターフェイス

メディア挿入のインターフェイスでは「代替テキスト」の入力欄があります。アップロードした時点では、値は入っていません。唯一残念なのはまとめての指定ができない点でしょうか。選択状態の画像の代替テキストを選択状態を変えながら一つ一つ指定していくことになります。

コントロールキー+クリックでエディタ上から画像のプロパティを編集できる

エディタからコントロールキー+クリックで画像のプロパティを編集できます。一度指定した代替テキストは保存されており、次回同じ画像を挿入する際にも初期値として指定されており、このあたりはATAGに沿っていると言えます。

ATAGに照らし合わせて唯一問題となっている点は、代替テキストが空の時にファイルのbasename(拡張子を除いたファイル名)が入ってしまう点です。つまり、代替テキストフィールドが空の時、タイトルフィールドの値が入ってしまうのです。タイトル欄を空欄にすれば、alt属性は空になりますが、これは少々わかりにくい実装です。代替テキストというラベルのついているフィールドが空なのに、自動生成された値が入ってします。これはガイドラインに沿っていない点かと思います。

<img class="alignnone size-medium wp-image-16" src="http://localhost/wordpress/wp-content/uploads/2015/12/banner-sample-02-300x100.gif" alt="banner-sample-02" width="300" height="100" />

それでも、WordPress の画像の挿入に関する機能よく考えられている印象でした。

Movable Type の画像の挿入フロー

Movable Type のファイルアップロードインターフェイス

最新のMovable Type(6.2)では、投稿画面からメディアの挿入を選択した後、ドラッグ&ドロップで画像をアップロードできます(もちろんファイル選択してアップロードすることもできます。このあたりはWordPressと同じですね)。

Movable Typeの画像挿入インターフェイス

問題は次のステップです。 Movable Type の場合、アップロード後に各画像の「編集」をクリックして画像のプロパティを指定していかなければなりません。1点ずつ指定しなければならないのは WordPressと同じですが画面遷移が伴うだけひと手間多くかかります。また、ここで「挿入」ボタンをクリックしてしまうと、画像の代替テキストを指定することができません。

Movable Type の画像挿入インターフェイスの次のステップ。代替テキストの指定ができない。

貼り付け後、HTML編集モードにして代替テキストを指定することはできます。尚、Movable Type の場合、ラベルを空にすれば alt属性値は空になります。ここはガイドラインに沿った挙動と言えます。

尚、WordPressにあった、貼り付け済みの画像のプロパティをコントロール+クリックで再編集する機能は Movable Type にはありません(これについては後述します)。

PowerCMS の場合

最後に、PowerCMS の場合です。PowerCMS のベースエンジンは Movable Type ですから、基本的には Movable Type の画像挿入フローを継承していますが、代替テキスト指定については独自に機能を追加しています。詳細は以下の記事をご覧下さい。

PowerCMS の画像挿入インターフェース

また、WordPress にあって Movable Type にないエディタ上で画像のプロパティを編集する方法ですが、システム >PowerCMS設定 >TinyMCE設定で advanced_buttons1 に「image」を追加することでできるようになります。Movable Type でも plugins に「advimage」を追加し、「image」を追加することでできるようになります(この機能から代替テキストを修正した場合、この値を次回同じ画像を利用するときの推奨値として利用してくれない点は課題として残りますが)。

TinyMCE設定画面

エディタから画像のプロパティを編集できる

PowerCMS / Movable Type のオプションプラグインである PowerCMS 8341では別途「画像の検証」ボタンがあり、画像と画像の代替テキストを一覧化して表示できます。画面キャプチャのように、不適切な値(例:ファイル名)は指摘してくれ、この画面から纏めて修正を可能にしています。

PowerCMS8341の画像の検証画面

尚、PowerCMSではサイドバーへのドラッグ&ドロップで画像をアップロードする際に、日本語ファイル名を指定しておくと、拡張子を削除した日本語のファイル名が代替テキストとして登録される裏技? があります。

Authoring Tool Accessibility Guidelines 目線で CMSを再評価する

今年は MTDDC をはじめとして CMS夏祭り(東京/大阪)とかCMSを比較したり座談会のようなイベントに本当に多数呼んでいただいたのですが、ウェブアクセシビリティに関する質問をされたことは一度もありませんでした。CMS は ATAGで定義されているオーサリングツールに他なりません。こういった視点で CMSを再評価するような場があってもいいのではないでしょうか。他の CMS のエバンジェリストの方の見解も是非おきかせください。現場からは、以上です。

MTDDC で話してきました。

セッション中の私ー

MTでのCMS構築案件で皆あれこれ工夫しているというのは理解できるのですが、傾向として結果として見通しの悪いテンプレートになっているケースを目にすることが最近増えてきたように思います。理由はいくつかあると思うのですが、以下のようなことではないかと思います。

  • 案件の規模が大きくなり、顧客の要望が増え仕様が複雑になってきている
  • 実装者のスキルが上がってきており、複雑な要件もMTMLで実現できるようになってきている

後者は良い傾向であるともいえますが、結果として初心者が修正できないテンプレートになったりしては本末転倒かと思います(場合によっては上級者でも他人のテンプレートがわからないといったケースも)。

一例を挙げます(実際はもっと多岐に渡り複雑なケースが散見されます)。

<mt:Entries<mt:If name="want_filter"> field.foo="1"</mt:If>>
...
</mt:Entries>

上記のような書き方をしたいが、できないので色々と工夫しているということでした。以下の例では IF文を一度評価した後で decode_html して再度 mteval でビルド しています。

<mt:For decode_html="1" mteval="1">
&lt;mt:Entries <mt:If name="want_filter">field.foo="1"</mt:If>&gt;
...
&lt;/mt:Entries&gt;
</mt:For>

また、下記のようにビルド結果のテキストを置換して再度ビルドするというテクニックを使っているというケースもありました。

<mt:setVarBlock name="search1">/<lz:/g</mt:setVarBlock>
<mt:setVarBlock name="replace1"><</mt:setVarBlock>
<mt:setVarBlock name="replace1" append="1">mt:</mt:setVarBlock>

<mt:setVarBlock name="search2">/<\/lz:/g</mt:setVarBlock>
<mt:setVarBlock name="replace2"><</mt:setVarBlock>
<mt:setVarBlock name="replace2" append="1">/mt:</mt:setVarBlock>

<mt:For regex_replace="$search1","$replace1" regex_replace="$search2","$replace2" mteval="1">
    <lz:Entries <mt:If name="want_filter">field:foo="1"</mt:if>>
        <lz:If name="__first__"><ul></lz:If>
        <li><lz:EntryTitle></li>
        <lz:If name="__last__"></ul></lz:If>
    </lz:Entries>
</mt:For>

やりたいことは実現できているのですが、以下の課題が残ります。

  • 管理画面のテンプレート検索で目的の箇所がヒットしない
  • decode_html のパターンでは、HTML(デザイン)の修正時に修正が困難
  • やりすぎると見通しが悪くなる
  • replace のパターンでは文字列「

DOMDocument プラグインを利用すれば、以下のように書けます。

<mt:getElementById id="tmpl_foo" setvar="tmpl_foo">
<mt:removeAttribute name="id" node="tmpl_foo">

<mt:If name="want_filter">
    <mt:setAttribute node="tmpl_foo" attr="field.foo","1">
</mt:If>

<mt:Entries id="tmpl_foo">
...
</mt:Entries>

まず、1行目で MTgetElementById タグで MTEntries ブロック(Node)を取得して変数 tmpl_foo に格納します。 2行目で、MTEntriesブロックから id モディファイアを削除しています。idモディファイアがあると entry_idでのフィルタリングがかかってしまうためです。その後、 MTIFタグで want_filter に値がある時、MTsetAttribute タグで field.foo=1 を追加します。

MTEntries に id 指定しなくても、以下のようにタグ名と index (n番目=1番目は「0」)指定で取得することもできます。

<mt:getElementsByTagName tag_name="Entries","0" setvar="tmpl_foo">

また、何をやっているかをわかりやすくするために GetHashVar プラグインの note モディファイアを使ってコメントを該当タグの中に直接記載することができます(そもそも存在しないモディファイアは無視されるので、プラグインがなくてもこう書けます)。

<mt:getElementsByTagName tag_name="Entries","0" setvar="tmpl_foo"
    note="1番目の mt:Entriesタグにカスタムフィールド 
    foo によるフィルタリングをかける">
<mt:If name="want_filter">
    <mt:setAttribute node="tmpl_foo" attr="field.foo","1"
    note="want_filterが1の時フィルタリング">
</mt:If>

<mt:Entries id="tmpl_foo"
    note="DOM操作でフィルタ指定対象のブロック">
...
</mt:Entries>

ま、スライド見ていただければ言わんとするとことはおわかりいただけるかと思いますが、この手の話しならこれからいくらでもするからな! これからは間違うなよ!

セッション中に紹介したプラグイン

Facebook

Twitter

このアーカイブについて

このページには、過去に書かれたブログ記事のうちMovable Typeカテゴリに属しているものが含まれています。

前のカテゴリはソーシャルメディアです。

次のカテゴリはMovable Typeプラグインです。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

Powered by Movable Type 6.2.6