気付いたらHexoのパーマリンクが変わっていたので自動生成+固定にできるよう対応

気付いたら、過去記事のURLが変わっていた

超びっくりした。Twitterに張ってあったリンク踏んだらなんもねーの。

HexoにはランダムなIDを生成する機能があり(Permalinks | Hexo)、これをパーマリンクに組み込んでいたのですが、どうやらこのIDがどこかのタイミングで再生成されているようです。

というわけで再現条件を検証してみましたところ、hexo cleanで生成されたコンテンツを削除し、再度生成するとIDが変わることが分かりました。

シェルのログ

1
2
sa-b:id_insp user$ hexo new testfile
[info] File created at ~/id_insp/source/_posts/testfile.md

記事ファイルを作成しておく。

1
permalink: :year/:month/:day/:id/

上記のように_config.ymlで指定して、パーマリンクにランダム生成されるIDが含まれるようにしておく。

1
2
3
4
5
6
7
8
9
10
11
sa-b:id_insp user$ hexo g
...
[create] Generated: 2014/9/15/cks0z54eyu3897ql (23ms)
...
sa-b:id_insp user$ hexo clean
[info] Deleted cache file
[info] Deleted public directory
sa-b:id_insp user$ hexo g
...
[create] Generated: 2014/9/15/qbrylfdpnfpcismr (24ms)
...

hexo cleanを行う前後でIDが変わっている。

テーマを変えるときとか、間違って作ってしまったタグをタグ一覧から削除するときなど、けっこう頻繁にhexo cleanするのに、そのたびにパーマリンクが変わってしまうのはひどすぎます。即刻やめることにしました。

用意されているパーマリンク用変数を検証する

上記ページより変数表を引用します。

変数名 説明
:year Published year of posts (4-digit)
:month Published month of posts (2-digit)
:i_month Published month of posts (Without leading zeros)
:day Published day of posts (2-digit)
:i_day Published day of posts (Without leading zeros)
:title Filename
:id Post ID
:category Categories. If the post is uncategorized, it’ll be category_dir setting.

後述しますが、実はslugという変数もあります。内容はtitleと同じです。

基本的には日付だけでいいのですが、それだと同日複数更新に対応できません。(全部最初の記事にリンクされる)

となると:titleを使うしかないのですが、個人的に日本語URLが嫌いなので、できれば使いたくありません。

title変数の詳細

まず、これはファイル名であってフロントマターのtitle:の内容ではないことに注意することがあります。

もっとも、hexo newでの生成時には同じ内容が挿入されるのですが、どちらかを変更すれば当然互いに異なる内容になります。つまり、hexo newでの記事生成の際にパーマリンクにする内容を入力し、その後ファイル内のtitle:項目を修正すればいい、という話ではあります。

しかし、いわゆるスラッグを考えるのは大嫌いだし、title:項目を毎回削除して書き直すのもごめん被りたいところです。

どうでもいいですが、ファイル名が2014-09-15-タイトルとかの場合、なぜかtitle変数の内容はタイトルになるようです。日付っぽいのをエスケープするのかな…?

hexo newコマンドを使わずAlfred Workflowで対応してみる

だったらhexo自体をカスタマイズすればいいわけですが、そこまでの知識はないので、とりあえずAlfred Workflowで対応することにしました。ターミナルコマンドを利用します。

ランダムな文字列を生成する

ユニークなURLを生成するために、ランダムな文字列を生成してみます。

上記を参考にしました。

1
key=`cat /dev/urandom | LC_CTYPE=C tr -dc '[:alnum:]' | head -c 10`

完全にランダムなので、かぶる可能性がなくはないですが、同じ日付でかぶらなければいいので、そんな天文学的確率は気にしなくていいでしょう。

ユーザ定義変数を利用して任意にスラッグをつける

Hexoでは、各記事のフロントマターに適当な項目を入力すると、変数として参照することができます。

この際、Hexoに定義されている変数名は上書きできないため避けなければなりません。実は、slugが使えなかったので定義されていることに気付いたのだったり。

記事ファイルのフロントマターに、以下のような記述を追加するとします。

1
pid: hoge

_config.ymlで以下のように記述すると、パーマリンクで参照できます。

1
permalink: :year/:month/:day/:pid/

結果として生成された記事のURLは、以下のようになります。

http://wbtmiu.herokuapp.com/2014/09/15/hoge/

後は、pid:項目をランダム生成してしまえばいいわけです。

完成したAlfred Workflow用シェルスクリプト

Alfred Workflowでこういうのを作ります。

Hexo Alfred Workflow

上記を参考に、ターミナルコマンドを作成しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
cd ~/heroku/wbtmiu.herokuapp.com/source/_posts/ #hexoプロジェクトフォルダに移動
key=`cat /dev/urandom | LC_CTYPE=C tr -dc '[:alnum:]' | head -c 10` #ランダムな文字列を生成
title=`echo "{query}" | sed -e 's/ /-/g' -e 's/[\.\/]//g'` #クエリ文字列を置換して変数に格納
cat << eot > `date '+%Y-%m-%d-'`$title.md #記事ファイルを作成
title: {query}
date: `date '+%Y-%m-%d %H:%M'`
pid: $key
tags:
-
-
---
eot
open -a FoldingText `date '+%Y-%m-%d-'`$title.md #記事ファイルをFoldingTextで開く

これで、以下の処理が行われます。

  • Hexoソースフォルダに記事ファイルを作成
    • ファイル名を生成
      • クエリをファイル名として適正にする
      • 日付を挿入
    • フロントマターを生成
      • クエリを素のままtitleに挿入
      • 日付をdateに挿入
      • pidをランダムに生成
  • FoldingTextで記事ファイルを開く

過去記事をこの形式に修正

当然、過去記事も修正する必要があります。

もともとのフロントマター形式は以下の通りです。

1
2
3
4
5
6
7
title: 気付いたらHexoのパーマリンクが変わっていたので自動生成+固定にできるよう対応
date: 2014-09-15 21:21
tags:
- Hexo
- Mac
- Alfred Workflow
---

これを、以下のように修正します。

1
2
3
4
5
6
7
8
title: 気付いたらHexoのパーマリンクが変わっていたので自動生成+固定にできるよう対応
date: 2014-09-15 21:21
pid: JQDleI4soG
tags:
- Hexo
- Mac
- Alfred Workflow
---

基本的には、sedによる置換で、tags:行の上にpid:行を挿入していくことになります。

sed内部で文字列生成(失敗)

1
find source/_posts/ -type f -exec gsed -i -e "s/^tags/pid: `cat /dev/urandom | LC_CTYPE=C tr -dc '[:alnum:]' | head -c 10`\ntags/g" {} \;

一見よさそうですが、生成される文字が全部同じなので意味ないです。

for文でループ処理(成功)

上記を参考にしました。

1
2
3
4
5
6
cd ~/heroku/wbtmiu.herokuapp.com/source/_posts/
for file in `ls`
do
key=`cat /dev/urandom | LC_CTYPE=C tr -dc '[:alnum:]' | head -c 10` #ランダムな文字列を生成
gsed -i -e "s/^tags/pid: $key\ntags/" $file
done

内容としては、

  • lsでファイル一覧を取得してfile変数に格納
  • ファイルごとにループ処理
    • 文字列生成してkey変数に格納
    • key変数込みで置換

この場合、1ループごとに文字列生成されるためファイルごとに違う文字列が挿入されます。

というか、「ファイルごとの複数処理」ってこんなんでよかったのか。もっと早くわかってればあれもこれも……うっ頭が……