この記事でjavascriptでいろいろできるんだなーと思ってはいたので、この件も解決できないかと思い立ちやってみたのでメモ。
MTの謎仕様
ネットで調べる限りあまり事例がないので、皆さんあまりそういうパターンがないのかもしれませんが説明します。
カテゴリ構成が以下のようになっているとして
- カテゴリA:記事0件
- カテゴリB:記事5件
- カテゴリC:記事10件
- カテゴリB:記事5件
カテゴリAの階層のページでカテゴリA以下のすべての記事を表示したい!というとき、MTのテンプレートでは以下のように書けばうまく動くと思われます。
<mt:Entries include_subcategories="1">
ごにょごにょ
</mt:Entries>
しかし、これだとなぜかカテゴリB、カテゴリCに記事があってもリストアップされず、作成されるのは0件の一覧になります。
試しにカテゴリAに記事を1件作成してみると、正しくリストアップされるようになりました。
そう、include_subcategories="1"
というのは記事が0件のカテゴリに対しては効かないんですね。意味ないですよねぇ。。。
バージョンによっては使えるらしいですが、今回のサイト(6.1.2)では使えませんでした。
以下のページで詳しく紹介されております。
MovableTypeでinclude_subcategories="1"が効かなかった件
include_subcategoriesに関するMovableTypeの謎な仕様と対策
上記サイト様の簡潔な一文を引用させていただくと
記事がひとつもない場合、下層に記事が100件あろうとも上層カテゴリーには何もリストアップされません。
ということです。
それでは大変に困ったので取り組みました。
仕様(実現したいこと)
複数の子カテゴリを持つ親カテゴリの階層の記事一覧に下階層のカテゴリも含む記事をリストアップする。
URLの一部から該当するカテゴリを判定する。
カテゴリ階層
- カテゴリA(cateA)
- カテゴリB(cateB)
- カテゴリC(cateC)
- カテゴリB(cateB)
例)
http://ドメイン/cateA/cateB/cateC ⇒ カテゴリCの記事だけが対象
http://ドメイン/cateA/cateB ⇒ カテゴリB、Cの記事が対象
http://ドメイン/cateA/ ⇒ カテゴリA、B、Cすべての記事が対象
条件
- 親カテゴリに記事が未登録でも子カテゴリの分だけ表示する
- 子カテゴリは複数でも可
- ページングあり(ページ番号は最大11個まで)
今回の場合、2017年以降を対象とした年別アーカイブを作る必要があったので、それを前提に書きます。
実装
やり方としてはflexible SearchとMTを組み合わせて使ってみたの方法を半分くらい利用します。
MTから2017年以降の全記事情報(本文除く)を保持するjsonファイルを吐き出します。それをjavascriptでマッチングして記事一覧を作成します。
該当するカテゴリを検索するためにMTからカテゴリ一覧も取得します。
必要なファイルは以下です。
- 記事一覧用のjsonファイル
- MovableTypeから出力
- カテゴリ名判定用のjsonファイル
- MovableTypeから出力
- javascriptファイル
- 記事の検索、HTMLの構築
JSONファイルの出力
記事一覧用のJSONファイル
記事一覧表示用のJSONファイルの出力が必要になります。これはMTで行います。
今回の場合、年別アーカイブを作成するので2017年以降の記事のみ取得するようにしています。
カスタムテンプレートを以下のような感じで作成します。
速度を少しでも早くしたい場合は、カテゴリごとに出力した方が良いです。
<mt:Unless name="compress" regex_replace="/^\s*\n/gm","">
<mt:SetVar name="items">
<mt:Entries include_blogs="2" lastn="0" days="365">
<mt:SetVarBlock name="startDate">2017.01.01</mt:SetVarBlock>
<mt:SetVarBlock name="entryDate"><$mt:EntryDate format="%Y.%m.%d"$></mt:SetVarBlock>
<mt:If name="startDate" lt="$entryDate">
<mt:SetVarBlock name="item{date}"><$mt:EntryDate format="%Y.%m.%d"$></mt:SetVarBlock>
<mt:SetVarBlock name="item{title}"><mt:EntryTitle></mt:SetVarBlock>
<mt:SetVarBlock name="item{url}">/info<mt:EntryIfCategory>/<mt:EntryPrimaryCategory><$mt:SubCategoryPath replace="_","-"$></mt:EntryPrimaryCategory></mt:EntryIfCategory>/<$mt:EntryBasename separator="-"$>.html</mt:SetVarBlock>
<mt:SetVarBlock name="item{more}"><mt:EntryMore remove_html="1" regex_replace="/\n|\t| | /g",""></mt:SetVarBlock>
<mt:SetVarBlock name="item{tag}">,<mt:EntryTags glue=","><mt:TagName regex_replace="/ | /g","%20"></mt:EntryTags>,</mt:SetVarBlock>
<mt:SetVarBlock name="item{category}"><mt:EntryCategories glue=","><mt:CategoryLabel></mt:EntryCategories></mt:SetVarBlock>
<mt:SetVarBlock name="item{new}"><mt:SetVarBlock name="entry_date_relative"><$mt:EntryDate relative="3" format=""$></mt:SetVarBlock><mt:If name="entry_date_relative"><em>NEW</em></mt:if></mt:SetVarBlock>
<mt:SetVarBlock name="items" function="push"><mt:var name="item" to_json="1"></mt:SetVarBlock>
</mt:If>
</mt:Entries>
{"items":[
<mt:Loop name="items" glue=","><mt:Var name="__value__"></mt:Loop>
]}
</mt:Unless>
-
<mt:SetVarBlock name="item{date}">
で指定しているitem{date}
のdate
の部分は後々使用するので、わかりやすい名前にしておいた方がよいです - 複数のカテゴリが設定されている場合はカンマ区切りで出力しています
- 一度Entriesで配列に格納してからLoopで書き出しています
カテゴリ名判定用のJSONファイル
URLからディレクトリ階層を抽出し、カテゴリのマッチングを行うためにカテゴリ一覧を出力します。
これが意外に苦労した。。。
<mt:Unless name="compress" regex_replace="/^\s*\n/gm","">
{"category":[
<mt:SetVar name="all_categories">
<mt:TopLevelCategories>
<mt:SetVar name="categories">
<mt:SetVar name="item">
<mt:SetVarBlock name="item{name}"><$mt:CategoryLabel$></mt:SetVarBlock>
<mt:SetVarBlock name="item{id}"><$mt:CategoryBasename$></mt:SetVarBlock>
<mt:SetVarBlock name="ParentBasename"><$mt:CategoryBasename$></mt:SetVarBlock>
<mt:SetVar name="subitems">
<mt:SubCategories>
<mt:SetVar name="subitem">
<mt:SetVarBlock name="subitem{name}"><$mt:CategoryLabel$></mt:SetVarBlock>
<mt:SetVarBlock name="subitem{id}"><$mt:GetVar name="ParentBasename" $>/<$mt:CategoryBasename$></mt:SetVarBlock>
<mt:SetVarBlock name="ParentSubBasename"><$mt:GetVar name="ParentBasename" $>/<$mt:CategoryBasename$></mt:SetVarBlock>
<mt:SetVar name="subsubitems">
<mt:SubCategories>
<mt:SetVar name="subsubitem">
<mt:SetVarBlock name="subsubitem{name}"><$mt:CategoryLabel$></mt:SetVarBlock>
<mt:SetVarBlock name="subsubitem{id}"><$mt:GetVar name="ParentSubBasename" $>/<$mt:CategoryBasename$></mt:SetVarBlock>
<mt:SetVarBlock name="subsubitems" function="push"><mt:GetVar name="subsubitem" to_json="1"></mt:SetVarBlock>
</mt:SubCategories>
<mt:SetVarBlock name="subitem{sub}" function="push"><mt:var name="subsubitems" to_json="1"></mt:SetVarBlock>
<mt:SetVarBlock name="subitems" function="push"><mt:GetVar name="subitem" to_json="1"></mt:SetVarBlock>
</mt:SubCategories>
<mt:SetVarBlock name="item{sub}" function="push"><mt:var name="subitems" to_json="1"></mt:SetVarBlock>
<mt:SetVarBlock name="categories" function="push"><mt:var name="item" to_json="1"></mt:SetVarBlock>
<mt:SetVar name="categoryInfo">
<mt:Loop name="categories" glue=",">
<mt:SetVarBlock name="categoryInfo"><mt:Var name="__value__" ></mt:SetVarBlock>
<mt:SetVarBlock name="categoryInfo"><$mt:Var name="categoryInfo" regex_replace="/\\/g","" $></mt:SetVarBlock>
<mt:SetVarBlock name="regex0">/(\"\[)/g</mt:SetVarBlock>
<mt:SetVarBlock name="regex1">[</mt:SetVarBlock>
<mt:SetVarBlock name="categoryInfo"><$mt:Var name="categoryInfo" regex_replace='$regex0','$regex1' $></mt:SetVarBlock>
<mt:SetVarBlock name="regex0">/(\]\")/g</mt:SetVarBlock>
<mt:SetVarBlock name="regex1">]</mt:SetVarBlock>
<mt:SetVarBlock name="categoryInfo"><$mt:Var name="categoryInfo" regex_replace='$regex0','$regex1' $></mt:SetVarBlock>
<mt:SetVarBlock name="regex0">/(\"{)/g</mt:SetVarBlock>
<mt:SetVarBlock name="regex1">{</mt:SetVarBlock>
<mt:SetVarBlock name="categoryInfo"><$mt:Var name="categoryInfo" regex_replace='$regex0','$regex1' $></mt:SetVarBlock>
<mt:SetVarBlock name="regex0">/(}\")/g</mt:SetVarBlock>
<mt:SetVarBlock name="regex1">}</mt:SetVarBlock>
<mt:SetVarBlock name="categoryInfo"><$mt:Var name="categoryInfo" regex_replace='$regex0','$regex1' $></mt:SetVarBlock>
</mt:Loop>
<mt:SetVarBlock name="all_categories" function="push"><mt:var name="categoryInfo"></mt:SetVarBlock>
</mt:TopLevelCategories>
<mt:Loop name="all_categories" glue=",">
<mt:Var name="__value__" >
</mt:Loop>
]}
</mt:Unless>
-
{name}
はカテゴリ名(論理名)で、{id}
はカテゴリ名(物理名)です。 - 3階層までの対応です。きっともっときれいに書けるんだと思いますが、時間の関係上上記までとしました。
-
<mt:SetVar name="categoryInfo">
以降はJSONファイルのフォーマット調整になります
正常にいけば以下のような感じで出力されます。
{"category":[
{
"sub":"",
"name":"トップ",
"id":"top"
},
{
"sub":[
{
"sub":[
{
"name":"カテゴリC",
"id":"cateA/cateB/cateC"
}
],
"name":"カテゴリB",
"id":"cateA/cateB"}
],
"name":"カテゴリA",
"id":"cateA"
}
,
{
"sub":"",
"name":"その他",
"id":"etc"
}
]}
javascriptファイル
ここまでで必要なデータは出そろったので、javascriptでカテゴリの判定と該当記事の抽出、HTMLの構築まで行います。
var json_path = "./entries.inc";
var category_path = "./categories.inc";
var ENTRY_COUNT_PER_PAGE = 20; // 1ページに表示する記事数
var VIEW_PAGENATION_COUNT = 11; // ページネーションの表示数
/**
* 一致するカテゴリの記事のみ表示する
*/
$(function() {
var currentPageNo = parseInt($('#current_page_no').val());
var currentYear = parseInt($('#current_year').val());
$.getJSON(json_path, function(dataList) {
$.getJSON(category_path, function(allCategoryList) {
// 現在のページに該当するカテゴリリストを取得
var categoryList = getCategoryListByUrl(allCategoryList['category']);
var appendText = '';
var items = dataList.items;
// 該当記事の抽出
var entryList = new Array();
for (var i in items) {
// category filtering
var myCategory = items[i].category.split(',');
var bCategory = false;
for (var j = 0; j < myCategory.length; j++) {
// カテゴリリストに照合
for (var index in categoryList) {
if (myCategory[j] == categoryList[index]) {
// 対象
bCategory = true;
break;
}
}
if (bCategory) {
// 一致あり
break;
}
}
if (!bCategory) {
// 対象外
continue;
}
entryList.push(items[i]);
}
if (entryList.length > 0) {
// 総ページ数の計算
var allPageCount = Math.ceil((entryList.length / ENTRY_COUNT_PER_PAGE));
// 不正なページ指定の是正
if (allPageCount < currentPageNo) {
currentPageNo = allPageCount
}
else if (currentPageNo <= 0) {
currentPageNo = 1;
}
// オフセットの計算
var offset = ENTRY_COUNT_PER_PAGE * (currentPageNo - 1);
// 一覧の作成
var entryCount = 0;
for (var i = 0; i < entryList.length; i++) {
if (i < offset) {
continue;
}
if (entryCount < ENTRY_COUNT_PER_PAGE) {
appendText += '<dt>' + entryList[i].date + '</dt>';
appendText += '<dd><a href="' + entryList[i].url + '">' + entryList[i].title + '</a>';
appendText += entryList[i].new;
appendText += '</dd>';
entryCount++;
}
else {
break;
}
}
$('#news_list').append(appendText);
// ページネーション作成
var viewPageCount = allPageCount;
var viewStartPage = 1; // ページネーションの開始ページ番号
if (allPageCount > VIEW_PAGENATION_COUNT) {
viewPageCount = VIEW_PAGENATION_COUNT;
var lastMiddleNum = allPageCount - Math.floor(VIEW_PAGENATION_COUNT / 2);
var firstMiddleNum = Math.ceil(VIEW_PAGENATION_COUNT / 2);
if (firstMiddleNum < currentPageNo && currentPageNo <= lastMiddleNum) {
// 中間ページ
viewStartPage = currentPageNo - Math.floor(VIEW_PAGENATION_COUNT / 2);
}
else if (currentPageNo > lastMiddleNum) {
// 後半
viewStartPage = lastMiddleNum - Math.floor(VIEW_PAGENATION_COUNT / 2);
}
}
var pagingText = '<div class="section paging">';
pagingText += '<ul>';
// Prev
if (currentPageNo > 1) {
pagingText += '<li><a href="index.html?y=' + currentYear + '&p=' + (currentPageNo - 1) + '">« Prev</a></li>';
}
for (var i = 0; i < viewPageCount; i++) {
var pageNo = parseInt(viewStartPage) + i;
if (pageNo == currentPageNo) {
pagingText += '<li><em>' + currentPageNo + '</em></li>';
}
else {
pagingText += '<li><a href="index.html?y=' + currentYear + '&p=' + pageNo + '" >' + pageNo + '</a></li>';
}
}
// Next
if (allPageCount > 1 && currentPageNo != allPageCount) {
pagingText += '<li><a href="index.html?y=' + currentYear + '&p=' + (currentPageNo + 1) +'">Next »</a></li>';
}
pagingText += '</ul>';
pagingText += '</div>';
$('.news_list').append(pagingText);
}
else {
$('#news_list').append('<p>該当する記事がありません</p>');
}
});
});
})
/**
* URLからカテゴリを判定する
* @param array allCategoryList 全カテゴリ情報リスト
*/
function getCategoryListByUrl(allCategoryList) {
var currentUrl = window.location.href;
var categoryList = new Array();
for (var index in allCategoryList) {
var categoryInfo = allCategoryList[index];
var subCategoryList = new Array();
if (categoryInfo.sub != "") {
// サブカテゴリあり
var subCategoryName = "";
for (var subIndex in categoryInfo.sub) {
var subCategoryInfo = categoryInfo.sub[subIndex];
var subsubCategoryList = new Array();
if (subCategoryInfo.sub != "") {
// さらにサブカテゴリあり
for (var subsubIndex in subCategoryInfo.sub) {
var subsubCategoryInfo = subCategoryInfo.sub[subsubIndex];
if (currentUrl.indexOf(subsubCategoryInfo.id) > -1) {
// サブサブカテゴリに該当する
return [subsubCategoryInfo.name];
}
else {
// 該当しない
subsubCategoryList.push(subsubCategoryInfo.name);
}
}
}
if (currentUrl.indexOf(subCategoryInfo.id) > -1) {
// サブカテゴリに該当する
subsubCategoryList.push(subCategoryInfo.name);
return subsubCategoryList;
}
else {
// 該当しない
subCategoryList = subCategoryList.concat(subsubCategoryList);
subCategoryList.push(subCategoryInfo.name);
}
}
// サブカテゴリに該当なし
// 自分のカテゴリ名にサブカテゴリ名をすべて付加する
if (currentUrl.indexOf(categoryInfo.id) > -1) {
// トップレベルカテゴリに該当
subCategoryList.push(categoryInfo.name);
return subCategoryList;
}
}
else {
// サブカテゴリなし
if (currentUrl.indexOf(categoryInfo.id) > -1) {
return [categoryInfo.name];
}
}
}
return false;
}
では最初から。
基本情報の取得、データファイルの読み込み
現在のページ番号、対象年を取得して、$.getJSONを使って各データファイルを読み込みます。
ページ番号、対象年は今回の場合URLにGETパラメータとして付加されるのでhiddenで持っています。
var json_path = "./entries.json";
var category_path = "./categories.json";
var ENTRY_COUNT_PER_PAGE = 20; // 1ページに表示する記事数
var VIEW_PAGENATION_COUNT = 11; // ページネーションの表示数
$(function() {
var currentPageNo = parseInt($('#current_page_no').val());
var currentYear = parseInt($('#current_year').val());
$.getJSON(json_path, function(dataList) { // entries.jsonの読み込み
$.getJSON(category_path, function(allCategoryList) { // categories.jsonの読み込み
URLから該当するカテゴリリストを抽出
アクセスされたURLから検索が必要なカテゴリを抽出してリストを返します。
これももっときれいなソースで書ければいいんですが。。。再帰とか使えばもっとシンプルになると思われ。
// 現在のページに該当するカテゴリリストを取得
var categoryList = getCategoryListByUrl(allCategoryList['category']);
こちらが関数です。
/**
* URLからカテゴリを判定する
* @param array allCategoryList 全カテゴリ情報リスト
*/
function getCategoryListByUrl(allCategoryList) {
var currentUrl = window.location.href;
var categoryList = new Array();
for (var index in allCategoryList) {
var categoryInfo = allCategoryList[index];
var subCategoryList = new Array();
if (categoryInfo.sub != "") {
// サブカテゴリあり
var subCategoryName = "";
for (var subIndex in categoryInfo.sub) {
var subCategoryInfo = categoryInfo.sub[subIndex];
var subsubCategoryList = new Array();
if (subCategoryInfo.sub != "") {
// さらにサブカテゴリあり
for (var subsubIndex in subCategoryInfo.sub) {
var subsubCategoryInfo = subCategoryInfo.sub[subsubIndex];
if (currentUrl.indexOf(subsubCategoryInfo.id) > -1) {
// サブサブカテゴリに該当する
return [subsubCategoryInfo.name];
}
else {
// 該当しない
subsubCategoryList.push(subsubCategoryInfo.name);
}
}
}
if (currentUrl.indexOf(subCategoryInfo.id) > -1) {
// サブカテゴリに該当する
subsubCategoryList.push(subCategoryInfo.name);
return subsubCategoryList;
}
else {
// 該当しない
subCategoryList = subCategoryList.concat(subsubCategoryList);
subCategoryList.push(subCategoryInfo.name);
}
}
// サブカテゴリに該当なし
if (currentUrl.indexOf(categoryInfo.id) > -1) {
// トップレベルカテゴリに該当
subCategoryList.push(categoryInfo.name);
return subCategoryList;
}
}
else {
// サブカテゴリなし
if (currentUrl.indexOf(categoryInfo.id) > -1) {
return [categoryInfo.name];
}
}
}
return false;
}
記事一覧ファイルから該当する記事の抽出
該当するカテゴリ名のリストが取得できたので、それを元に該当する記事を抽出します。
var items = dataList.items;
// 該当記事の抽出
var entryList = new Array(); // 該当記事リスト
for (var i in items) {
// カテゴリが一致するかチェックする
var myCategory = items[i].category.split(','); // 分割
var bCategory = false;
for (var j = 0; j < myCategory.length; j++) {
// カテゴリリストに照合
for (var index in categoryList) {
if (myCategory[j] == categoryList[index]) {
// 一致
bCategory = true;
break;
}
}
if (bCategory) {
// 一致あり
break;
}
}
if (!bCategory) {
// 対象外
continue;
}
entryList.push(items[i]);
}
- 単純にjsonファイルを読んで作成したリストを回して、カテゴリが一致するものを抽出しています。
該当記事数の判定
前処理で作成したリストの件数で記事数を判定します。
0件の場合はここで処理終了、それ以外の場合は次へ進みます。
if (entryList.length > 0) {
~~~これ以降でごにょごにょ~~~
}
else {
$('#news_list').append('<p>該当する記事がありません</p>');
}
不正なページ数の是正
// 総ページ数の計算
var allPageCount = Math.ceil((entryList.length / ENTRY_COUNT_PER_PAGE));
// 不正なページ指定の是正
if (allPageCount < currentPageNo) {
currentPageNo = allPageCount
}
else if (currentPageNo <= 0) {
currentPageNo = 1;
}
- ENTRY_COUNT_PER_PAGEは1ページあたりの表示件数
記事一覧HTMLの構築
記事リストを元にHTMLを構築。
// オフセットの計算
var offset = ENTRY_COUNT_PER_PAGE * (currentPageNo - 1);
// 一覧の作成
var entryCount = 0;
for (var i = 0; i < entryList.length; i++) {
if (i < offset) {
continue;
}
if (entryCount < ENTRY_COUNT_PER_PAGE) {
// 1ページの表示件数に満たない場合
appendText += '<dt>' + entryList[i].date + '</dt>';
appendText += '<dd><a href="' + entryList[i].url + '">' + entryList[i].title + '</a>';
appendText += entryList[i].new;
appendText += '</dd>';
entryCount++;
}
else {
// 1ページの表示件数オーバーの場合
break;
}
}
$('#news_list').append(appendText);
- オフセットを計算して対象ページのデータのみappendしている
ページネーションの作成
全ページ数、現在のページによってページネーションを作成します。
ページ番号の表示数は最大11個とする。11ページ目以降は現在のページを中心として前後5ページ分まで表示する。
// ページネーション作成
var viewPageCount = allPageCount;
var viewStartPage = 1; // ページネーションの開始ページ番号
// 表示番号の調整
if (allPageCount > VIEW_PAGENATION_COUNT) {
// 最大表示数よりも大きいページ番号の場合
viewPageCount = VIEW_PAGENATION_COUNT;
var lastMiddleNum = allPageCount - Math.floor(VIEW_PAGENATION_COUNT / 2);
var firstMiddleNum = Math.ceil(VIEW_PAGENATION_COUNT / 2);
if (firstMiddleNum < currentPageNo && currentPageNo <= lastMiddleNum) {
// 中間ページ
viewStartPage = currentPageNo - Math.floor(VIEW_PAGENATION_COUNT / 2);
}
else if (currentPageNo > lastMiddleNum) {
// 後半
viewStartPage = lastMiddleNum - Math.floor(VIEW_PAGENATION_COUNT / 2);
}
}
var pagingText = '<div class="pagination">';
pagingText += '<ul>';
// Prev
if (currentPageNo > 1) {
pagingText += '<li><a href="index.html?y=' + currentYear + '&p=' + (currentPageNo - 1) + '">« Prev</a></li>';
}
for (var i = 0; i < viewPageCount; i++) {
var pageNo = parseInt(viewStartPage) + i;
if (pageNo == currentPageNo) {
pagingText += '<li><em>' + currentPageNo + '</em></li>';
}
else {
pagingText += '<li><a href="index.html?y=' + currentYear + '&p=' + pageNo + '" >' + pageNo + '</a></li>';
}
}
// Next
if (allPageCount > 1 && currentPageNo != allPageCount) {
pagingText += '<li><a href="index.html?y=' + currentYear + '&p=' + (currentPageNo + 1) +'">Next »</a></li>';
}
pagingText += '</ul>';
pagingText += '</div>';
$('.news_list').append(pagingText);
- 前半でページネーションに表示するページ番号の調整をしています。VIEW_PAGENATION_COUNTは表示するページネーション数で今回は11。
- index.html等のパスは調整してください。
ここまでで下階層も含む記事一覧を作成することができました。
その他
- 検索が必要な場合はflexibleSearchも組み合わせるとうまくいきそう
- 当初はflexibleSearch使えばいけるのでは?と思ったがそこまで必要なかった
- 記事が多くなるごとに表示が遅くなると思われる。ただ、500件とかで試したが体感的には問題なし