Chienomi

事前生成戦略動画CMSの別解 〜 Mewduct Tigerroad

開発::web

Mewductはサーバーアプリのない動画CMSだ。

つまり(いつもの)事前生成戦略である。

その仕組みは、HTMLファイルがスクリプトローダーかつGUIフレームになっており、スクリプトによって生成されたJSONをロードしてJavaScriptのDOM操作でページを組み上げる方式だ。

だが、このアプローチにはもうひとつの解がある。 「全部を組み込んだHTMLを吐き出しておく」だ。

Mewductの仕組みのおさらい

MewductはCLIツールで動画ファイルとJSONファイルを作る、というのが肝。

  • mewduct-encode.zshffmpegを使って動画を複数のフォーマットにエンコードする
  • mewduct-import.zshで動画ファイルを配信ディレクトリ以下に配置
    • ついでに他のスクリプトを呼ぶ
  • mewduct-update.rbで動画データのJSONを作る
  • mewduct-user.rbでユーザーデータ(ユーザーの動画リスト)のJSONを作る
  • mewduct-home.rbでホームページの動画リスト/ユーザーリストのJSONを作る

配信されるHTMLファイルはindex.html, user.html, play.htmlの3つ。 それぞれのHTMLファイルは骨組みだけで、ページはJavaScriptがJSONファイルをロード、JSONファイルをもとにDOM操作で組み上げる。

仕立てとしてはSPAに近いが、それぞれのページは独立したHTMLファイルになっているのでSPAではない。HTMLファイルナビゲーションを使うけれどページ自体はJavaScriptで作られるというのはちょっとめずらしい形式。

Mewductの欠点

Mewductはシンプルできれいなアプリケーションだ。 基本的なつくりはとても良いと思っている。

が、明らかな欠点がひとつある。 ページのコンテンツはJavaScriptがDOM操作で作るので、ボットから見えないのだ。

このため、SEO的に困るという話があった。 また、SNSでシェアしてもからっぽになるので、そこも微妙という話があった。

で、実はMewductみたいなやり方をするのであれば、JSONじゃなくHTMLを作ってしまうということは普通にできる。 そもそも、MewductのJavaScriptがやっているのはメタデータを読んでDOMを組み上げるということだから、テンプレート展開でもできるようなことをしている

といっても、JavaScriptのDOM操作は構造に対する操作で、eRubyは文字列に対する操作だから、ロジックの美しさとしてはMewductのほうが良いと思う。

Tigerroadの概要

Mewduct Tigerroadは当初、Mewductの一部コンポーネントを置き換える形で動作することが想定されており、Mewductのコードをコピーして作り始めた。

だが最終的にはTigerroadのCLIはMewductのJSONをロードしてHTMLを作る、要はMewductのJavaScriptアプリケーションがやっていることをRubyのCLIツールでやるという感じのものになった。

一方、HTMLファイルからロードされるスクリプトはMewductのようにDOMを構築する必要がないため、テンプレートで読むスクリプトが変更されており、ここはMewductのものを置き換える形になっている。

そして当然ながら、MewductではDOMを構築するためのスクリプトがロードされており、そこからDOM構築がなくなったので非常に薄いものになっている。

Tigerroadのテクニック

JSON埋め込み

知らなかったのだけど、検索エンジンボットは

<script type="applicaion/json">
</script>

の中身をJSONとして読むらしい。へー。

中身がJSONならば、

const data = JSON.parse(document.getElementById("MetaJson").textContent)

のようにスクリプトからも扱える。

だが問題として、script要素の中身としてJSONを置くならば、エスケープが必要だ。 けれど、検索エンジンボットはJSONとして解釈するのだから、HTMLエスケープすると解釈できなくなる。 なおかつ、JavaScriptからHTMLエスケープされたテキストを戻すのはかなり面倒。

これに対するアンサーがこれ。

def json_embed data
  JSON.dump(data).gsub(/[&><]/) do |s|
    {'&' => '\u0026', '>' => '\u003e', '<' => '\u003c'}[s]
  end
end

HTMLエスケープではなくJSONのUnicodeエスケープは検索エンジンボットは読めるし、JavaScriptのJSON.parse()も読める。

翻訳

Mewductの場合コンテンツをJavaScriptで組んでいる関係で、ブラウザの言語や設定の言語をもとにテキストを設定できるが、TigerroadはHTML自体を事前生成してしまうためこれができない。

JSON埋め込みはこれを補うものでもある。

各要素に翻訳データを組み込むのはやりすぎな感じになってしまうので、HTMLの生成に使ったメタデータ自体も埋め込んでしまうことで、ノードコレクションとメタデータの配列を同じインデックスで同じ要素のデータが取れるという特性を利用している。

これはメタデータが別ファイルのJSONだと、その瞬間に取得したHTMLとJSONのデータに整合性が取れていない可能性があり、HTMLを生成するときにJSONデータを組み込んでしまえばその問題は発生しないため、この方式をとったからこそ実現可能なことである。

export async function userCardLocalize() {
  // ここでドキュメントに埋め込まれたtextContentからメタデータを拾っている。
  // videosはJSON解釈しており、Arrayオブジェクト
  const videos = JSON.parse(document.getElementById("UserVideosJSON").textContent)
  cardLocalize(videos)
}
function cardLocalize(index_meta) {
  // メタデータArrayとHTMLCollectionで共有するインデックス
  let index = 0

  // document.getElementsByClassNameで得られるのはHTMLCollection
  // UserVideosJSONに埋め込まれたものと同じJSONをソースとして生成されている
  for (const elem of document.getElementsByClassName("video_card")) {
    // ここではelem[index]に対応するメタデータがindex_meta[index]で拾える
    // ひとつのHTMLファイルに含まれているものなので、齟齬は生じない
    // ...

    index++
  }
}

eRubyで共有コンポーネント

video cardとuser cardは複数の場所から使われる共通コンポーネントであり、Mewductだとcardbuilder.mjsusercard.mjsで共有コンポーネントになっている。 createCard()Elementオブジェクトが返るのでappendChild()にそのまま使える。

しかしeRubyで

% card = lambda do |meta|
...
% end

みたいにしてcard.(meta)とかしてもうまくいかない。

そのため、各CLIツールでロードして使うライブラリで定義されているモジュールで

module TigerroadLib
  #...
  def video_card meta
    ERB.new(CARD_TEMPLATE).result(binding)
  end

  #...
end

としている。これをincludeしておけば、bindingを渡すことでeRuby内で

% videos.each do |meta|
  <%= video_card meta %>
% end  

みたいなことができる。

Mewductとの共存

Tigerroadはサーバー設定が必要ではあるものの、同じwebrootをMewductと共有してMewductとMewduct Tigerroadの両方を展開することができる。

この場合、Mewductがボットから見えないことが功を奏してSEOを邪魔しない。

実際に私はMewductMewduct Tigerroadの両面展開している。

動画のSEOに関する私の意見

絶対にYouTubeが優先されるし、ボットが見るコンテンツに乏しい動画で個人サイトが上位に来ることなんてほとんどないので、そんなにSEOを気にするならYouTubeに屈しておけばいいのに。

だから、Mewduct TigerroadはSEO向けというのは「検索エンジンから認識できるようにした」程度の話で、検索上位を狙うためのものではない。 明確なベネフィットは、SNSシェア適性を持っていることだと思う。

Fedibirdでシェアした様子
Telegramでシェアした様子