Static site generator

静的サイトジェネレータで重複しないURLを生成するにはハッシュ関数を使えばよかった

早く言えシリーズ。

静的サイトジェネレータでURLを自動生成したい

過去何度か書いていますが、私はWordPressなどでいう「スラッグ」というものが嫌いです。スラッグを考えるのも、過去記事と被っていないか確認するのも面倒だからです。

なので、JekyllやHexoなどの静的サイトジェネレータでは、基本的に日付ベースのURLを利用しています。しかし、同日に複数更新する場合に、どのように一意のURLを発行するかが悩みの種でした。

現在、ブログの構築に利用しているHexoでは、ランダムなURLを生成する機能があります。しかし、これは再生成時に変わってしまうのが問題でした。

そこで、Alfred Workflow経由で、/dev/urandomを利用した乱数を生成し、それをパーマリンクに組み込むようにしていました。

しかし、そんなものより遥かにスマートな解法があることに気付きました。ハッシュ関数です。

文字列からハッシュ関数を生成

ハッシュ関数とは、ファイルなどのデータから一意の値(ハッシュ値)を求めるプログラムです。ハッシュ値が一致すればデータそのものも一致すると考えられるため、ファイルの同一性検証などに用いられます。

ここで、文字列からハッシュ値を求めれば、互いに異なる文字列に対して、容易に一意のkeyを得ることができます。

やってみた

以下のようなシェルスクリプトで、記事タイトルからハッシュ値を生成し、それを組み込んだ記事ファイルを作成することができます。

1
2
3
4
5
6
7
8
9
10
11
12
cd ~/heroku/wbtmiu.herokuapp.com/source/_posts/
title=`echo "{query}" | sed -e 's/ /-/g' -e 's/[\.\/]//g'` #クエリ文字列を置換して変数に格納
key=`md5 -qs "$title"`
cat << eof > `date '+%Y-%m-%d-'`$title.md #記事ファイルを作成
title: {query}
date: `date '+%Y-%m-%d %H:%M'`
pid: $key
tags:
-
---
eof
open -a FoldingText `date '+%Y-%m-%d-'`$title.md #記事ファイルをFoldingTextで開く
  • これが→key=`cat /dev/urandom | LC_CTYPE=C tr -dc '[:alnum:]' | head -c 10
  • これに変わっている→key=`md5 -qs "$title"`

なんということでしょう。

しかも、/dev/urandomを利用する方法と違い、仮に何らかのトラブルでkeyが失われたとしても、タイトルさえ判明していれば同一のハッシュ値を得ることができるのです。

Alfred Workflowからの実行にも成功しましたので、さっそくこの記事から利用することにします。

静的サイトジェネレータの自動タグ補完的なシステムを作りたかった

Hexoでタグ入力支援が欲しい

Hexoで書いた記事もだいぶ増えてきて、どんなタグを作ったか自分でも把握しきれなくなってきました。

うっかりTypoした場合はもちろんのこと、HexoのタグはCase sensitiveなので、大文字小文字の違いで別タグ扱いになってしまいます。

しかも、一度誤ったタグ名でGenerateしてしまうと、タグ一覧に登録されたタグ自体を削除するためには、hexo cleanを行う必要があるため、そこそこ面倒です。

そこで、タグの入力補完というか、候補から選択できるようにしたいと思い、考えてみました。

とりあえずターミナルだけでやってみる

  • _postsフォルダからforループで対象ファイルを取得
    • フロントマター部分(---行まで)を切り出し
      • grep | sedで行数取得
        • 最初の1個だけ
        • 行番号を付ける
        • sedで先頭の行番号だけを残す
      • headで取得した行数以上を切り出し
      • ファイルに追記
  • ソート&重複行を削除してファイルに保存
1
2
3
4
5
6
for file in source/_posts/*.md #記事ファイルをリスト
do
fme=`grep -n -m 1 '^---' $file | sed -e 's/\(^[0-9]*\).*/\1-1/' | bc` #フロントマターの終了行番号を検出
cat $file | head -$fme | grep '^-' | sed -e 's/^- //g' >> tags.txt #フロントマター内のリスト項目(タグ)を一時ファイルに追記
done
sort tags.txt -uo tags.txt #一時ファイルの重複行を削除してファイルに保存

だいたいできたのですが、一部の重複行が残ってしまいました。

空白文字でも混じっているのか、はたまたTypoかと思いましたが、Sublime Textで「Permute Lines > Unique」すると削除できてしまいます。

色々あがいてみましたが、どうしても解決できず。ひとまず諦めてSublime Textで処理して、grep で既存タグを抽出してみます。

1
2
grep hoge tags.txt #既存タグを表示
grep hoge tags.txt | pbcopy #クリップボードにコピー

本来ならこのあたりをAlfred Workflowで自動化したいのですが、重複問題が解決できないので、とりあえず放置することにしました。というか、インクリメンタルサーチの仕方がわかんない。

AutoHotKeyだったらタグ一覧ファイルから簡単にインクリメンタルサーチできるのですが、TextExpanderとかのスニペットにしようとするとけっこう大変。

んあー。

暫定対応

Hexoプロジェクトフォルダにシェルスクリプトを保存。

chmod u+x taglist.shで実行権限を付与。./taglist.shでtags.txtを生成。

あとは適当にgrepするなりして参考にする。

その後

解決しました。

静的サイトジェネレータでテンプレート言語のコードを記述する場合のエラー対処方法

HexoでGenerateに失敗しまくった

このブログはHexoで作成していますが、ある時、Generateの際にやたらと失敗続きになることがありました。

消したり戻したりしながら原因を特定した結果、Codeblock内の一部記述が問題であることが分かりました。

具体的には、とりあえず画像で示しますが、AppleScriptの以下の部分。

AppleScript

そして、Jekyll templateの以下の部分です。

html

どうも、{}(BRACE/CURLY BRACKET)が重なっていたり、%と絡んでいる部分が引っかかる模様。

MarkdownにおけるInine code/Codeblock/Fenced codeblock全てで同様の問題が発生します。エラーが出ない場合でも、Codeの内容が空になったり。

どこでエラーが発生しているのか?

ここで、静的サイトジェネレータを利用したコンテンツ生成フローは、以下のようになっています。

静的サイトジェネレータによるコンテンツ生成フロー1

例えばインラインコードの記述を例に取るなら、まあ内容は適当ですが、以下のような順序でコンテンツが生成されます。

静的サイトジェネレータによるコンテンツ生成フロー2

どうやら、問題が起こっているのはテンプレートエンジンによる処理の部分。Hexoで利用されているテンプレートエンジンであるSwigのシンタックスとかぶるため、Swigで処理しようとしているようです。

うん、まず、<code>内をテンプレートエンジンで処理しようとするのバカなの死ぬのと言いたいのはやまやまなのですが、そうなるものは仕方ない。

となると、テンプレートエンジンに渡すデータ内で、Code部分がエスケープしてあればいいということになります。

Markdownパーサにおける特殊文字エスケープ

ここで、Markdownパーサにおける特殊文字エスケープ方法を確認してみましょう。

HTMLでは、特別な考慮を要する文字が2つあります: < と & がそれです。小なり記号はタグを書き出すのに使用しますし、アンパサンドはHTMLエンティティを示すのに使います。もしこれらの文字をそのものとして使いたければ、それぞれ< と & とHTMLエンティティの形式にエスケープ処理を施す必要があります。

Markdownはあなたの代わりに必要なエスケープ処理を実施することで、これらの文字をそのままのかたちで使用できるようにしてくれます。もしあなたがアンパサンドをHTMLエンティティの宣言のために使用している場合はそれはそのまま変換されずに置かれます。それ以外の場合には& に変換されます。

Gruber wrote a Perl script, Markdown.pl, which converts marked-up text input to valid, well-formed XHTML or HTML and replaces left-pointing angle brackets (‘<’) and ampersands with their corresponding character entity references.

ジョン・グルーバーによる「元祖Markdownパーサ」であるmarkdown.plは、<,>,&を自動的にエンティティに置き換えてエスケープします。そして、その他の特殊文字については\による手動エスケープが可能(必要)なわけです。

これはHexoでも同様で、CURLY BRACKETをBACKSLASHで/{のようにエスケープできます。(なお、ドキュメントは見付からなかったが、Hexo公式サイトトップページを見ると、たぶんGitHub Flavored Markdownに基づくパースを行っている)

1
\{% for page in site.pages %\}

しかし、Code部分でこの方法を使うと、\自体がブラウザで出力されてしまいます。上記の記述例自体がまさにそうです。

うん、Markdownパーサ、キミは悪くないそれで正しい。しかし解決には結びつかない。

HTMLエンティティによるエスケープ

ならば、HTMLエンティティ(実体参照)に置き換えたらどうか。例えば、{,}のエンティティは&#123;, &#125;ですから、以下のように記述できます。

1
&#123;% for page in site.pages %&#125;

ところが、これはCode部分ではうまく働かない場合があります。そう、&自体が自動的にエンティティに置き換えられるためです。

つまりこうです。

静的サイトジェネレータによるコンテンツ生成フロー3

ほん!とに!ややこしいなもう!

ということは、アレか。ここの処理でMarkdownパーサ挟まなければいいのか。

HTMLエンティティにエンコードし、さらに<code>,<pre>を直接記述する

結論として。

MarkdownにおけるInine code/Codeblock(Fenced codeblock)の代わりに、<code>/<pre><code>を使用し、さらにHTMLエンティティに置き換えた場合、正常に記号が表示されました。

つまり、以下のような記述です。

1
<code>&#123;% for page in site.pages %&#125;</code>

ただし、これだと静的サイトジェネレータのシンタックスハイライト機能を活かせません。

外部ソース(Gist)でEmbed

これはMarkdownパーサ由来の問題ですから、Codeblock/Fenced codeblockの場合、コード本体を外部に置いて参照する方法もあります。

自分の手が加わっているコードなら、Private GistにしてEmbedするのが、比較的楽でシンタックスハイライトも利きます。

自分の手が一切加わってないコードの場合、Gistで管理するのも抵抗がある、というか邪魔なので、おとなしく<pre><code>するほうがいいと思われます。

まとめ

  • テンプレートエンジンに渡すソースに、テンプレート言語と解釈できる部分があると、処理されてしまう
    • Codeとして記述しても無意味
  • HTMLエンティティによるエスケープは有効
    • ただし、その前にMarkdownパーサで処理するとそのままテンプレートエンジンに渡せない場合がある
  • Markdownソース内で<code>タグを利用することで、HTMLエンティティによってエスケープしたコードをそのままテンプレートエンジンに渡すことができる
  • 外部ソースからのEmbedは極めて有効