Chienomi

静的ウェブページでタグ機能を提供する

ウェブサイト開発

Mimir Yokohamaのページでタグ機能がバージョンアップし、完全に動作するようになった。

もともとWordPressで提供していたMimir Yokohamaのウェブページだが、独自システムに移行する際には「WordPressで利用していた機能はすべて提供する」という方針のもと、新しいサイト構築システムPureBuilder Simplyを開発して構築した。

PureBuilder Simplyは静的ページを生成するプログラムであり、webサーバーには静的ファイルを配置する。 これはパフォーマンス、セキュリティ、管理、リソースいずれにおいてもメリットが大きい。

基本的にこの考え方は「異なる内容を生成するタイミングより、同一の内容を返すタイミングのほうがずっと多い」ということに基づいており、キャッシュよりも合理的である。 一方、どうしても難しい要素もある。ひとつはページ生成パターンが無限である検索機能、そしてもうひとつはヒントを日本語のみにした場合のタグ機能だ。

検索機能はGoogleに頼っているが、タグ機能は難関だった。

タグ機能を作る

方針

  • 要求タイミングでの動的生成は行わない
  • PureBuilder Simplyの枠内で解決する。 もし解決不能な場合はPureBuilder Simplyを拡張する
  • (PureBuilder Simplyでサポートされている) eRubyテンプレートは使わない。あくまでPandocテンプレートで生成する
  • ファイル名が日本語になることはやむを得ないものとする (Nginxは日本語ファイル名に対してURIエンコーディングされたパスでアクセスできる)
  • リンクを日本語で書く(エンコードをブラウザに委ねる)ことは許容しない
  • ユーザー (この場合自分だけど) にタグに関して文書にタグ付けする以上の手間をかけさせない

タグをつける

既にPureBuilder Simplyでは 「Frontmatterに文書に関する追加的情報を書く」 という仕様となっている。1

単純にこれを反映したタグを書けば良い。

tags:
  - ねこ
  - かわいい

ページにタグ情報をつける

Pandocテンプレートで簡単につけることができる。

$if(tags)$
<!-- TAGS -->
<nav id="TagBox">
  <h4>タグ</h4>
  <ul id="TagCloud" class="tagcloud">
$for(tags)$
        <li class="tag"><a href="/tags/$tags$.html">#$tags$</a></li>
$endfor$
    </ul>
</nav>
$endif$

英語なら割とこれで済む話なのだけど2、日本語だと当然

<li class="tag"><a href="/tags/ねこ.html">#ねこ</a></li>

みたいなHTMLが生成されてしまう。

そこで、post generate機能を利用する。 post generate機能はページを生成したあと生成ページを加工できるものだ。 基本的に第一引数として生成されたファイルパスが渡される。 このほか環境変数を通じて他の情報にもアクセスできたりするのだが、これはあまり利用を想定していない。

post generateは.post_generateディレクトリ内のファイルを順次Perlに渡す形で実行される。 Perlはshebang行を解釈するので、Perlで書かなければならないわけではない。 そして、スクリプトの出力にファイルは置き換えられる。

これは例えば

#!/usr/bin/perl -p

s/foo/bar/g

のような非常に簡単なフィルタが書けるということだ。

これを使って

#!/usr/bin/ruby

require 'cgi'

while line = ARGF.gets
    if(line.include?("<!-- TAG_CONVERTING_TARGET -->") .. line.include?("<!-- /TAG_CONVERTING_TARGET -->"))
        line = line.gsub(/href="\/tags\/([^"]*)"/) {|i| 'href="/tags/%s"' % CGI.escape($1) }
    end

    print line
end

のように変換してあげればタグのタイトルは日本語だがURIはエンコード済み、という形ができあがる。 もちろんスラッグへのマップを書いてもいいのだが、タグを管理するのは手間なので避けた。

タグページを作る

生成されたページの情報はindexes.rbmというファイルにRuby Mershal形式で保存される。 ここには本文は含まれないが、大概メタ情報にアクセスしたい場合と本文にアクセスしたい場合は別なので、分けている。

PureBuilder Simplyにおいて、「メタ情報を文書に書き、処理した情報はデータベースに書いておく」というのは設定上の核であると行って過言ではない。 これにより、文書のメタ情報を扱うことはPureBuilder Simplyの外で行うことができるのだ。

タグページは.tagcloud.rbというスクリプトによって生成しているが、 これはMarkdownページを生成する。つまり、「タグページ自体をPureBuilder Simplyによって生成すべきページとして生成する」のである。 タグを含むページを生成・更新した場合は再度タグページを生成し直すことになり、そのためにrefreshというスクリプトもある。この場合、文書ページではないタグのindexを処理されてしまうと困るので次のような処理になっている。

pbsimply-pandoc.rb .
ruby .tagcloud.rb **/.indexes.rbm~tags/.indexes.rbm
pbsimply-pandoc.rb tags
rm tags/.indexes.rbm

では.tagcloud.rbは、というとこちらも結構単純。

見ての通り著しい力技でデータベースを集計し、最終的にはMarkdownを出力している。

#!/usr/bin/ruby
require 'date'
require 'yaml'
require 'cgi'

$tags = Hash.new {|h,k| h[k] = [] }
accs = nil

ARGV.each do |arg|
  if(File.exist?(File.dirname(arg) + "/.accs.yaml"))
    accs = YAML.load(File.read(File.dirname(arg) + "/.accs.yaml"))
  else
    accs = nil
  end
  Marshal.load(File.read arg).each do |k,v|
    if v.key?("tags")
      v["tags"].each do |tag|
        $tags[tag].push({
          v["title"],
          (File.dirname(arg) + "/" + k.sub(/\.md$/, ".html")),
          v["date"],
          ( accs && accs["title"]),
          v["category"]
        })
      end
    end
  end
end


$tags.each do |tag, v|
  File.open("tags/#{tag}.md", "w") do |f|
    f.puts <<-EOF
---
title: 'タグ "\##{tag}" の検索'
date: #{Date.today}
pagetype: accsindex
indexpage: yes
---

  EOF
    v.sort_by {|i| [i[:date], i[:corner].to_s, i[:category].to_s, i[:title]] }.reverse.each do |i|
      if i[:path] !~ %r:^/:
        i[:path] = "/" + i[:path]
      end
      f.printf("* [%s](%s) \\\n  %s%s @%s\n", i[:title], i[:path], i[:corner].to_s, (i[:category] && "/" + i[:category]).to_s, i[:date])
    end
  end
end

File.open("tags/index.md", "w") do |f|
  f.puts <<-EOF
---
title: 'タグ一覧'
date: #{Date.today}
pagetype: accsindex
indexpage: yes
---

EOF
  $tags.keys.sort.each do |k|
    f.puts "* [#{k}](/tags/#{CGI.escape k}.html) (#{$tags[k].length})"
  end
end

従来もほとんどこうだったのだが、ページ側でエスケープしていないという理由でエスケープしていなかったので追加した。

おわりに

もう少し難しいかと思っていたのだが、どうやらPureBuilder Simplyの設計が思っていた以上に優れていたようで、タグ機能もスムーズに実装することができた。

PureBuilder Simplyは傑作といって差し支えない出来になっている。 もともと思っていたよりもずっと優れたツールになっているのだ。

PureBuilder SimplyはPureBuilderとしては実に3作目である。 Zshの機能をフル活用したPureBuilder, Windowsでも動作可能なようRubyで書かれたPureBuilder2。ページの生成にはいずれも活用できたが、サイト構築労力が高く、安価な案件で利用するにはしんどいものがあった。 また、構築できる内容も割と画一的だったため、様々な要求に応えるのは難しかった。 Zshで書かれたPureBuilderは構築時に任意のZshスクリプトを実行できる方式だったため、なんでもできるといえばできるのだが、サイト構築がプログラミング色の強いものになっていた。これはちょっとユーザーフレンドリーではない。

PureBuilder Simplyは名前の通りずっとシンプルだが、いままでよりずっと強力になった。

小さなスクリプトを書くことは、多くのプログラマにとってはあまり馴染みのないことかもしれないが3、やろうと思えば発想さえ知れば決して難しいことではないはずだ。


  1. これはReSTでもdocutilが許容しないような規格化されていないメタ情報を書くということだ。↩︎

  2. ちなみに、Chienomiではタグはすべて英語になっている↩︎

  3. Unixに浸っている人はむしろ息をするようにのように行動するだろう。PureBuilder Simplyの考え方はこれに基づいている。↩︎