【MovableType】記事一覧を複数の条件でソートする。ついでに見出しも付ける。

MovableTypeのテンプレートで記事一覧を出力するときに、公開日や記事タイトルなどでソートができます。
ですが、デフォルトで用意されている項目以外や、複数の値を組み合わせてのソートはひと手間必要で、意外とめんどくさいので忘れたときの為に記しておきます。

今回の完成形はこちら。

まず公開日の年度別にソート。その次にカテゴリ別でソート。さらに公開日時でソートしています。
各年度とカテゴリの先頭に分かりやすく見出しを付けて完成です。

基本のソート

<mt:Entries sort_by="authored_on" sort_order="descend">
  <a href="<mt:EntryPermalink>"><mt:EntryTitle></a>
</mt:Entries>

MovableTypeのドキュメント通り、新着順に並べるとこんな感じになります。
sort_byがソートの基準にする値(ここでは公開日)。
sort_orderが昇順(ascend)or降順(descend)の設定です。

デフォルトで設定可能な値

デフォルトでsort_byに設定できる値はこちらです。

  • authored_on (公開日・初期値)
  • author_id (作成ユーザー ID)
  • basename (出力ファイル名)
  • comment_count (コメントの件数) 
  • created_on (作成日)
  • excerpt (概要)
  • modified_on (変更日時)
  • ping_count (トラックバックの件数) 
  • text (本文)
  • text_more (続き)
  • title (記事のタイトル)
  • rate (レート)
  • score (スコア)
  • field:customfieldbasename (カスタムフィールドのベースネーム)

複数の値を基準にしたソート

ここから本題です。
複数の値を使ってソートしたいとき、

<mt:Entries sort_by="authored_on,category" sort_order="descend">

のように、sort_byに複数の値を設定することはできません。(categoryはそもそもsort_byに指定できないですね。)

なので、一旦記事を配列に格納して、その配列をmt:Loopで出力するという方法をとります。その際にkeyを指定しておくことで、mt:Loopの時に意図した値でソートが可能になります。

記事の一覧をkeyを指定して配列に格納する

まずは<mt:Entries lastn="0">で順番関係なく全件取得し、entry_arrayという配列に記事を格納します。

<mt:Ignore> entry_array に記事のhtml部分を配列で入れておく </mt:Ignore>
<mt:SetVar name="entry_array">
<mt:Entries lastn="0">
  <mt:SetVarBlock name="key_name"><MT:EntryDate format="%Y">_<mt:CategoryBasename>_<MT:EntryDate format="%Y%m%d%H%M%S"></mt:SetVarBlock>
  <mt:SetVarBlock name="entry_array" function="push" key="$key_name">
    <mt:Ignore> ▼ 後でmt:loopで呼び出す用のHTML ▼ </mt:Ignore>
    <article>
      <h4><a href="<mt:EntryPermalink>"><mt:EntryTitle></a></h4>
      <div><mt:EntryBody remove_html="1" trim_to="32+..."></div>
      <span><mt:EntryDate format="%Y/%m/%d"></span>
    </article>
</mt:SetVarBlock>
</mt:Entries>

ポイントは、keyの指定です。

entry_arrayの配列に記事を追加する際、key="$key_name"を指定します。

<mt:SetVarBlock name="entry_array" function="push" key="$key_name">

key_nameは何かというと、その前の行で定義してあります。

<mt:SetVarBlock name="key_name"><MT:EntryDate format="%Y">_<mt:CategoryBasename>_<MT:EntryDate format="%Y%m%d%H%M%S"></mt:SetVarBlock>

今回は、まず年別に、その次にカテゴリで並べたいので、key_nameはソートの優先度の高いものから先に、{公開日(年)}_{カテゴリ}_{公開日(年月日時分秒)}になるようにしています。
実際のkey_name2024_cat2_20240507231049という形になります。

最後の{公開日(年月日時分秒)}は、年・カテゴリが同一の記事を公開日順で並べるために優先順の最後につけています。

Checkpoint

key_name{年}_{カテゴリ}_{年月日時分秒}となっているのを、あとで{年}{カテゴリ}{年月日時分秒}にそれぞれ分割します。
_は、分割する際の目印にするためにつけています。
分割する際の目印は、ソート対象の値に含まれていない必要があります。ご自身の環境に合わせて、任意の記号などに変更してください。
{年}@@@{カテゴリ}@@@{年月日時分秒}など、区切りは一文字でなくても大丈夫です。

用意した配列をループで回す

<mt:Loop name="entry_array" sort_by="key reverse">
  <mt:Var name="__value__"><mt:Ignore> 格納しておいたHTMLソースが入る </mt:Ignore>
</mt:Loop>

先ほど作成したentry_arrayの配列を、mt:Loopを使用してループ処理を行います。

ポイントはsort_by="key reverse"の部分。設定しておいたkey_nameを使用して、降順に並べる指定です。
sort_by="key"だと、デフォルトで昇順になります。

<mt:Var name="__value__">には、登録しておいた<article>~</article>がそのまま入ります。

順番に並べるだけだとこれで完成です。

見出しをつける

一風変わった並び順になるのでユーザーが混乱するかもしれません。見出しを付けてあげると親切かと思います。
先ほどのコードを修正します。

<mt:SetVar name="title_year">
<mt:SetVar name="title_category">
<mt:Loop name="entry_array" sort_by="key reverse">
  <mt:Ignore> 年の見出し </mt:Ignore>
  <mt:Var name="__key__" trim_to="4" setvar="entry_year">
  <mt:If name="title_year" ne="$entry_year">
    <mt:Var name="title_year" value="$entry_year">
    <mt:SetVar name="title_category">
    <h2><mt:Var name="title_year"></h2>
  </mt:If>
  
  <mt:Ignore> カテゴリの見出し </mt:Ignore>
  <mt:Var name="__key__" regex_replace="/^.*?_/","" regex_replace="/_.*?$/","" setvar="entry_category">
  <mt:If name="title_category" ne="$entry_category">
    <mt:Var name="title_category" value="$entry_category">
    <mt:TopLevelCategories show_empty="0" sort_order="ascend">
      <mt:CategoryBasename setvar="basename">
      <mt:If name="title_category" eq="$basename">
        <h3>- <mt:CategoryLabel></h3>
      </mt:If>
    </mt:TopLevelCategories>
  </mt:If>

  <mt:Var name="__value__"><mt:Ignore> 格納しておいたHTMLソースが入る </mt:Ignore>
</mt:Loop>

一気にコードが長くなりました…。

<mt:SetVar name="title_year">
<mt:SetVar name="title_category">

ここで、変数title_yeartitle_categoryを宣言(初期化)して、年の見出しとカテゴリの見出しのフラグを立てています。

年数の見出し

  <mt:Ignore> 年の見出し </mt:Ignore>
  <mt:Var name="__key__" trim_to="4" setvar="entry_year">
  <mt:If name="title_year" ne="$entry_year">
    <mt:Var name="title_year" value="$entry_year">
    <mt:SetVar name="title_category">
    <h2><mt:Var name="title_year"></h2>
  </mt:If>

まず年の値ですが、<mt:Var name="__key__" trim_to="4" setvar="entry_year">でkeyの先頭4桁を取得してentry_yearにセットしています。(正規表現めんどくさかったので簡単にしてしまいました。)

<mt:If name="title_year" ne="$entry_year">の分岐で、最初に設定したフラグと、この記事の年数を比較して、一致しなければIfの中を通ります。

1件目の記事(仮に2024年の記事とする)、2023年の1件目の記事、2022年の1件目の記事、2021年の1件目の記事…の時に、Ifの中を通ることになります。

Ifの中を通ったら、<mt:Var name="title_year" value="$entry_year">でフラグを更新して、見出しを出力します。

このIfの中を通るタイミングで<mt:SetVar name="title_category">でカテゴリのフラグも初期化します。
ここでカテゴリの初期化をしないと、例えば、2024年の最後の記事と2023年の最初の記事のカテゴリが同じ場合に、2023年の最初のカテゴリの見出しが表示されなくなります。

カテゴリの見出し

  <mt:Ignore> カテゴリの見出し </mt:Ignore>
  <mt:Var name="__key__" regex_replace="/^.*?_/","" regex_replace="/_.*?$/","" setvar="entry_category">
  <mt:If name="title_category" ne="$entry_category">
    <mt:Var name="title_category" value="$entry_category">
    <mt:TopLevelCategories show_empty="0" sort_order="ascend">
      <mt:CategoryBasename setvar="basename">
      <mt:If name="title_category" eq="$basename">
        <h3>- <mt:CategoryLabel></h3>
      </mt:If>
    </mt:TopLevelCategories>
  </mt:If>

<mt:Var name="__key__" regex_replace="/^.*?_/","" regex_replace="/_.*?$/","" setvar="entry_category">の部分で、keyからカテゴリのベースネームを抜き出しています。
ここは致し方ないのでregex_replaceで正規表現で置換処理しています。regex_replaceを2回使って、最初の_から前を削除。最後の_から後を削除。しています。(ほんとは1回で真ん中だけ抜き出せるけど、正規表現こっちの方が簡単だったので…。)

年数の時と同様に、<mt:If name="title_category" ne="$entry_category">で、フラグとこの記事のカテゴリが一致しないときだけIfの中を通ります。

    <mt:TopLevelCategories show_empty="0" sort_order="ascend">
      <mt:CategoryBasename setvar="basename">
      <mt:If name="title_category" eq="$basename">
        <h3>- <mt:CategoryLabel></h3>
      </mt:If>
    </mt:TopLevelCategories>

この部分、keyで設定したのがカテゴリのベースネームだったので、カテゴリ名を取得するためにごにょごにょしてます。
もとからカテゴリ名をkeyに設定していてもよかったのですが、後でカテゴリの表示内容が変わるかもしれなかったので、なんとなくこの方式にしています。

完成したコード

最終的なコードがこちらです。

<mt:Ignore> entry_array に記事のhtml部分を配列で入れておく </mt:Ignore>
<mt:SetVar name="entry_array">
<mt:Entries lastn="0">
  <mt:SetVarBlock name="key_name"><MT:EntryDate format="%Y">_<mt:CategoryBasename>_<MT:EntryDate format="%Y%m%d%H%M%S"></mt:SetVarBlock>
  <mt:SetVarBlock name="entry_array" function="push" key="$key_name">
    <mt:Ignore> ▼ 後でmt:loopで呼び出す用のHTML ▼ </mt:Ignore>
    <article>
      <h4><a href="<mt:EntryPermalink>"><mt:EntryTitle></a></h4>
      <div><mt:EntryBody remove_html="1" trim_to="32+..."></div>
      <span><mt:EntryDate format="%Y/%m/%d"></span>
    </article>
</mt:SetVarBlock>
</mt:Entries>

<mt:Ignore> entry_array ループ </mt:Ignore>
<mt:SetVar name="title_year">
<mt:SetVar name="title_category">
<mt:Loop name="entry_array" sort_by="key reverse">
  <mt:Ignore> 年の見出し </mt:Ignore>
  <mt:Var name="__key__" trim_to="4" setvar="entry_year">
  <mt:If name="title_year" ne="$entry_year">
    <mt:Var name="title_year" value="$entry_year">
    <mt:SetVar name="title_category">
    <h2><mt:Var name="title_year"></h2>
  </mt:If>
  
  <mt:Ignore> カテゴリの見出し </mt:Ignore>
  <mt:Var name="__key__" regex_replace="/^.*?_/","" regex_replace="/_.*?$/","" setvar="entry_category">
  <mt:If name="title_category" ne="$entry_category">
    <mt:Var name="title_category" value="$entry_category">
    <mt:TopLevelCategories show_empty="0" sort_order="ascend">
      <mt:CategoryBasename setvar="basename">
      <mt:If name="title_category" eq="$basename">
        <h3>- <mt:CategoryLabel></h3>
      </mt:If>
    </mt:TopLevelCategories>
  </mt:If>

  <mt:Var name="__value__"><mt:Ignore> <li>~</li>が入る </mt:Ignore>
</mt:Loop>

このやり方で複数の値を使用したソートが可能です。

ただし・・・

ただし、今回は優先順が年数→カテゴリで、一つ目の値が4桁の数字固定だったのですんなりでしたが、優先順がカテゴリ→年数だったら、もうひと手間必要な気がしています。

厳密にいうと、年数→カテゴリの順で比較しているわけではなく、keyひとつの文字列として比較しているからです。

カテゴリベースネームをtrim_toで文字数固定にしてしまうとかで何とかなりそうかと想定してますが、まだ検証できていません…。WordPressみたいに簡単にソートできるようアップデートに期待です。

ちなみに・・・

話はそれますが、最初に完成形として提示している画像のコーディングは、Water.cssを使用しています。(コーディングしていない)
クラス名もインラインのスタイル定義も必要なく、マークアップのみでいい感じの見た目にしてくれるので、ロジックの検証をする時に重宝しています。