Hexoでプラグインを使わずパンくずリストを表示する

(2020-07-30) コードを書き直しました。こちらをご覧ください。

このブログはHexoで構築しており、全ページにパンくずリストを設置しています。このパンくずリストを設置するのがなかなか大変だったので、メモを残しておきます。

使用しているテンプレートエンジンはEJSです。

ソース

<nav class="breadcrumbs">
    <ol class="breadcrumbs__list">
        <li class="breadcrumbs__item">
            <a class="breadcrumbs__item-link" href="/">
                ホーム
            </a>
        </li>
        <% if (is_home()) { %>
            <% if(page.current !== 1){ %>
                <li class="breadcrumbs__item">
                    <a class="breadcrumbs__item-link" href="/page/<%= page.current %>/">
                        ページ<%= page.current %>
                    </a>
                </li>
            <% } %>
        <% } else if (is_category()) { %>
            <%
            const category_slug = page.path.replace(config.category_dir + '/', '').replace(/\/page\/.+/,'').replace(/\/index.html/, '').split('/');
            let category_url = '';
            %>
            <% category_slug.forEach(function(item, i){ %>
                <%
                category_url += '/' + item;

                const category_name = Object.keys(config.category_map).filter( (key) => {
                  return config.category_map[key] === item;
                 });
                %>
                <li class="breadcrumbs__item">
                    <a class="breadcrumbs__item-link" href="/<%= config.category_dir %><%= category_url %>/">
                        <% if(category_name.length === 0){ %><%= item %><%}else{%><%= category_name %><%}%>
                    </a>
                </li>
            <% }) %>
            <% if(page.current !== 1){ %>
                <li class="breadcrumbs__item">
                    <a class="breadcrumbs__item-link" href="/<%= config.category_dir %><%= category_url %>/page/<%= page.current %>/">
                        ページ<%= page.current %>
                    </a>
                </li>
            <% } %>
        <% } else if (is_tag()) { %>
            <%
            const tag_slug = page.path.replace(config.tag_dir + '/', '').replace(/\/page\/.+/,'').replace(/\/index.html/, '').split('/');
            let tag_url = '';
            %>
            <% tag_slug.forEach(function(item, i){ %>
                <%
                tag_url += '/' + item;

                const tag_name = Object.keys(config.tag_map).filter( (key) => {
                  return config.tag_map[key] === item;
                 });
                %>
                <li class="breadcrumbs__item">
                    <a class="breadcrumbs__item-link" href="/<%= config.tag_dir %><%= tag_url %>/">
                        #<% if(tag_name.length === 0){ %><%= item %><%}else{%><%= tag_name %><%}%>
                    </a>
                </li>
            <% }) %>
            <% if(page.current !== 1){ %>
                <li class="breadcrumbs__item">
                    <a class="breadcrumbs__item-link" href="/<%= config.tag_dir %><%= tag_url %>/page/<%= page.current %>/">
                        ページ<%= page.current %>
                    </a>
                </li>
            <% } %>
        <% } else if (is_archive()) { %>
            <li class="breadcrumbs__item">
                <a class="breadcrumbs__item-link" href="/<%= config.archive_dir %>/<%= page.year %>/">
                    <%= page.year %>年
                </a>
            </li>
            <% if(page.month){ %>
              <li class="breadcrumbs__item">
                  <a class="breadcrumbs__item-link" href="<%- url_for(path) %>">
                      <%= page.month %>月
                  </a>
              </li>
            <% } %>
        <% } else if(is_post()) { %>
            <% page.categories.forEach(function(item){ %>
                <li class="breadcrumbs__item">
                    <a class="breadcrumbs__item-link" href="<%= url_for(item.path) %>">
                        <%= item.name %>
                    </a>
                </li>
            <% }) %>
                <li class="breadcrumbs__item">
                    <a class="breadcrumbs__item-link" href="<%- url_for(path) %>">
                        <%- page.title %>
                    </a>
                </li>
        <% } else if (is_page()) { %>
            <li class="breadcrumbs__item">
                <a class="breadcrumbs__item-link" href="<%- url_for(path) %>">
                    <%- page.title %>
                </a>
            </li>
        <% } %>
    </ol>
</nav>

仕様

スパゲッティーコードになっているのは申し訳ありません。自分のスキルではこれが限界です😥

問題はカテゴリーページの方にありまして、カテゴリーを階層化している場合で、子カテゴリーのページを表示させているとき、そのカテゴリーが属する親カテゴリー名を出す変数がないんですよ。たぶん。

なのでどうしているかと言いますと、URLからドメインと"categories"(デフォルト)を取り除き、スラッシュでカテゴリー名に切り分け変数に格納、そしてそれを基にURLとカテゴリー名を作りHTMLに出す…みたいなことをやっています。

カテゴリーのスラッグについて

Hexoでは、日本語カテゴリー・タグの英字スラッグを_config.ymlで指定することができます。これで日本語URLを回避できるという訳ですね。

category_map:
  CSS: css
  ドライブ: drive
  日記: diary
  未分類: uncategorized

tag_map:
  食べ物: food

先述の通り、カテゴリーページやタグページでは、URLを元にパンくずリストを作成するので、URLに現れる文字列とカテゴリー名が一致しないということが起こり得ますが、config.yml内でスラッグが指定されている場合はそれらを参照し、元のカテゴリー名を表示できる仕組みになっています。

filename_case: 1でのURL小文字統一には対応していないので、これを利用する場合でも、

category_map:
  CSS: css
  HTML: html
  Hexo: hexo
  JavaScript: javascript

のように、逐一category_mapを設定してください。

新版

あまりにもひどい仕様だったので書き直しました。

<%
let breadcrumbs = { ホーム: url_for('/')};

if(is_post()) {
    page.categories.forEach(function(item){
        breadcrumbs[item.name] = url_for(item.path);
    })
    breadcrumbs[page.title] = url_for(path);
} else if (is_page()) {
    breadcrumbs[page.title] = url_for(path);
} else if (is_category()) {
    let cat = get_category();
    for(var key in cat) {
        breadcrumbs[key] = cat[key];
    }
} else if (is_tag()) {
    breadcrumbs['#'+page.tag] = url_for(path);
} else if (is_archive()) {
    breadcrumbs['アーカイブ'] = '/' + config.archive_dir + '/';
    if(page.year){
        breadcrumbs[page.year+'年'] = '/' + config.archive_dir + '/' + page.year + '/';
    }
    if(page.month){
        breadcrumbs[page.month+'月'] = url_for(path);
    }
}
if(page.current > 1){
    breadcrumbs['ページ'+page.current] = '/' + config.pagination_dir + '/' + page.current + '/';
}

function reverse_array(a) {
    var key = [];
    for (var i in a) {
        key.push(i);
    }
    key.reverse();
    var ret = [];
    for (var i in key) {
        ret[key[i]] = a[key[i]];
    }
    return ret;
}

function get_category(){
    let arr = {};
    arr[page.category] = url_for(page.path);
    get_parent(page.category);

    function get_parent(current){
        const current_cat = site.categories.data.filter(x => x.name === current );
        const parent_id = current_cat[0].parent;

        if(parent_id) {
            const parent_name = site.categories.data.filter(x => x._id === parent_id);
            arr[parent_name[0].name] = url_for(parent_name[0].path);
            get_parent(parent_name[0].name);
        }
    }
    return reverse_array(arr);
}
%>

<nav class="breadcrumbs">
    <ol class="breadcrumbs__list">
      <% for(var key in breadcrumbs) { %>
          <li class="breadcrumbs__item">
              <a class="breadcrumbs__link" href="<%= breadcrumbs[key] %>">
                  <%= key %>
              </a>
          </li>
      <% } %>
    </ol>
</nav>

改善点

以前のものはループが複雑怪奇を極めていたので、まず最初にパンくずリストを配列でつくって、最後にまとめてHTMLに出すようにしました。

旧版ではカテゴリーページの場合に、URLからカテゴリーを取得するというトリッキーなことをやっていたのですが、カテゴリー名から親カテゴリーを取得する関数を書いたので、かなりシンプルになりました。

_config.ymlのカテゴリーのスラッグ、filename_caseの設定にも対応しています。旧版はスラッグの指定が必須でしたが、新版はスラッグを設定していなくても不具合は出ません。

次の記事

Hexoでパンくずリストの構造化マークアップをする