a-blog cms でパンくずリストをホームから並べる


ページの上に「ホーム > ブログ名 > カテゴリー > 記事タイトル」と現在位置を並べたい。a-blog cmsでは、Topicpathモジュールをテンプレートに書けばパンくずリストが出ます。

ただ、モジュールを置いただけだと、子ブログのページでは子ブログ名から始まってしまいます。サイトのトップから並べるにはモジュールIDの設定が要るので、記事の後半はその手順です。最後に、検索結果にパンくずを出すための構造化データも足します。Twigテンプレートを使うテーマ向けの書き方は、記事の終わりにまとめました。

次の画像が完成形で、子ブログ「blog テーマ確認用」の中にある親子2階層のカテゴリーを開いたところです。

ホーム、子ブログ、親カテゴリー、子カテゴリーの順に並んだパンくずリスト
完成形

テンプレートにTopicpathモジュールを書く

テーマの include/topicpath.html などに次のコードを書き、各ページのレイアウトから @include("/include/topicpath.html") で読み込みます。

<!-- BEGIN_IF [%{RBID}/eq/%{BID}/_and_/%{VIEW}/eq/top] --><!-- ELSE -->
<nav aria-label="現在位置">
  <!-- BEGIN_MODULE Topicpath -->
  <ol class="topicpath"><!-- BEGIN blog:loop -->
    <li><a href="{url}">{name}</a></li><!-- END blog:loop --><!-- BEGIN category:loop -->
    <li><a href="{url}">{name}</a></li><!-- END category:loop --><!-- BEGIN entry -->
    <li><a href="{url}" aria-current="page">{title}</a></li><!-- END entry -->
  </ol>
  <!-- END_MODULE Topicpath -->
</nav>
<!-- END_IF -->

Topicpathモジュールは、ブログ、カテゴリー、エントリーの順にブロックを出力します。blog:loopとcategory:loopは階層の数だけ繰り返され、entryが出るのはエントリーの詳細ページだけです。

外側のBEGIN_IFは、ルートブログのトップページでは何も出さないための条件です。トップページで「ホーム」1つだけのパンくずリストを出しても意味がないので、外しています。公式のblogテーマに入っているinclude/topicpath.htmlも同じ条件です。

エントリーの<li>にだけaria-current="page"を付けたのは、entryブロックが出るのがそのエントリーを開いているときに限られるからです。カテゴリーの一覧ページでは、どの項目にも付きません。

区切りの「>」はHTMLに書かず、CSSの::beforeなどで付けると、<ol>の中身が項目だけになり扱いやすくなります。

子ブログでは、子ブログ名から始まってしまう

ここまでの状態で子ブログのエントリーを開くと、パンくずリストは子ブログ名から始まります。ルートブログが入りません。

子ブログ名から始まり、ルートブログが入っていないパンくずリスト
モジュールを書いただけの状態

Topicpathの初期設定では、ブログの階層を「末端から1つだけ」表示するようになっているためです。ブログが1つだけのサイトならこのままで困りませんが、子ブログを使っているサイトでは設定を変えます。

モジュールIDを作って、ルートブログから並べる

管理画面の「モジュールID」から、モジュールに「トピックパス ( Topicpath )」を選んで新しく作ります。ここではモジュールIDをtopicpathにしました。「作成」を押すと編集画面に切り替わり、「表示設定」タブが増えます。

作成直後の条件設定は、引数のチェックがすべて外れ、階層は「下階層のブログを含めない ( self )」になっています。このまま使うと、エントリーの詳細ページを開いてもエントリー名が出ません。モジュールをidなしで書いたときとは初期値が違うので、作ったら必ず次の4か所を直してください。

  • グローバル: 「下の階層のブログが利用することを許可する」にチェック。ルートブログで作ったモジュールIDを子ブログでも使うために必要

  • 引数: カテゴリーID(cid)とエントリーID(eid)にチェック。開いているページのカテゴリーとエントリーを拾うようになる

  • 階層のブログ: 「下階層のブログも含める ( descendant-or-self )」

  • 階層のカテゴリー: 「下階層のカテゴリーも含める ( descendant-or-self )」

モジュールIDの条件設定。グローバル、cid と eid の引数、階層の設定
モジュールIDの条件設定

続けて「表示設定」タブの「ブログ」で、階層の起点を「ルート」にし、階層の数を空欄にします。空欄にすると、ルートブログから今いるブログまでのすべての階層が並びます。

起点の代替ラベルに「ホーム」と入れると、先頭のルートブログ名が「ホーム」に置き換わります。ルートブログの名前がサイト名になっているなら、ここで短くしておくと読みやすくなります。

表示設定のブログ。階層の起点がルート、階層の数が空欄、起点の代替ラベルがホーム
モジュールIDの表示設定

カテゴリーは初期設定のままで、ルートのカテゴリーからすべての階層が並びます。

保存したら、テンプレートのBEGIN_MODULEにモジュールIDを指定します。

<!-- BEGIN_MODULE Topicpath id="topicpath" -->

子ブログのエントリーを開き直すと、「ホーム」から始まり、エントリー名まで並びます。

ホームから子ブログ名、カテゴリー、エントリー名の順に並んだパンくずリスト
モジュールIDを指定したあと

検索結果向けに構造化データを出す

Googleの検索結果にパンくずを表示させたい場合は、同じモジュールでBreadcrumbListの構造化データを<head>内に出します。

<!-- BEGIN_MODULE Topicpath id="topicpath" -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [<!-- BEGIN blog:loop --><!-- BEGIN glue -->,<!-- END glue -->
    {
      "@type": "ListItem",
      "position": {sNum},
      "name": "{name}[raw|jsonEscape]",
      "item": "{url}"
    }<!-- END blog:loop --><!-- BEGIN category:loop --><!-- BEGIN glue -->,<!-- END glue -->
    {
      "@type": "ListItem",
      "position": {sNum},
      "name": "{name}[raw|jsonEscape]",
      "item": "{url}"
    }<!-- END category:loop --><!-- BEGIN entry --><!-- BEGIN glue -->,<!-- END glue -->
    {
      "@type": "ListItem",
      "position": {sNum},
      "name": "{title}[raw|jsonEscape]",
      "item": "{url}"
    }<!-- END entry -->
  ]
}
</script>
<!-- END_MODULE Topicpath -->

{sNum}は、ブログ、カテゴリー、エントリーを通した連番です。ブロックごとに1から数え直さないので、そのままpositionに使えます。

glueブロックは、2つ目以降の項目の前にだけ出力されます。ここにカンマを書いておけば、配列の最後に余計なカンマは付きません。ブログ、カテゴリー、エントリーの境目でも、入るカンマは1つだけです。

名前には[raw|jsonEscape]を付けています。a-blog cmsの変数は初期状態でHTML用にエスケープされるので、そのままだとタイトルの"が&quot;のまま構造化データに入ります。rawでHTMLのエスケープを外し、jsonEscapeでJSONの文字列としてエスケープし直すという組み合わせです。

書き方にも注意が要ります。{ "@type": "ListItem", "position": {sNum}, ... }のように1項目を1行にまとめたところ、出力の先頭部分が欠けて正しいJSONになりませんでした。波括弧の直後で改行し、1行に1つずつキーを書く形なら正しく出力されます。公式のblogテーマのinclude/structured-data/breadcrumb-list.htmlも、この改行した書き方です。

書いたら、ページのソースからJSONを取り出し、Googleのリッチリザルトテストなどで読み込めるか確かめてください。

Twigテンプレートで書く場合

テーマでTwigテンプレートを有効にしているなら、module()関数でV2_Topicpathモジュールを呼び出します。Twigから呼び出せるのは名前がV2で始まるモジュールだけなので、ここまでのTopicpathは使えません。

{% set topicpath = module('V2_Topicpath', 'v2_topicpath', { bid: BID, cid: CID, eid: EID }) %}

{% if topicpath.items is not empty and not (BID == RBID and VIEW == 'top') %}
  <nav aria-label="現在位置">
    <ol class="topicpath">
      {% for item in topicpath.items %}
        <li><a href="{{ item.url }}"{% if item.type == 'entry' %} aria-current="page"{% endif %}>{{ item.name }}</a></li>
      {% endfor %}
    </ol>
  </nav>
{% endif %}

V2_Topicpathは、ブログ、カテゴリー、エントリーを1つの配列itemsにまとめて返します。各項目のtypeにはblog・category・entryのどれかが入るので、エントリーかどうかはこの値で見分けます。

モジュールIDはV2_Topicpath用に別に作る

Twigのmodule()が探すのは、モジュールの種類が「V2_トピックパス ( V2_Topicpath )」のモジュールIDだけです。標準テンプレート用に作ったtopicpathを指定してもエラーにはならず、モジュールIDの設定が反映されないまま、子ブログ名から始まるパンくずリストが出ました。

モジュールIDの名前は、モジュールの種類が違っても重複できません。標準テンプレートとTwigの両方で使うサイトなら、v2_topicpathのように別の名前を付けます。

グローバルにチェックを入れることと表示設定の値は、標準テンプレートのときと同じです。表示設定では、ブログの階層の起点を「ルート」、階層の数を空欄、起点の代替ラベルを「ホーム」にします。

違いは引数です。Twigではmodule()の第3引数でカテゴリーIDとエントリーIDを渡すので、引数にチェックを入れなくてもエントリー名まで表示されました。一方で、階層の2か所は「下階層のブログも含める ( descendant-or-self )」「下階層のカテゴリーも含める ( descendant-or-self )」に変える必要があります。selfのままだとルートブログが出ず、子ブログ名が「ホーム」に置き換わって表示されます。

Twigで構造化データを出す

{% set topicpath = module('V2_Topicpath', 'v2_topicpath', { bid: BID, cid: CID, eid: EID }) %}

{% if topicpath.items is not empty %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {% for item in topicpath.items %}
    {
      "@type": "ListItem",
      "position": {{ item.sNum }},
      "name": {{ item.name|json_encode|raw }},
      "item": {{ item.url|json_encode|raw }}
    }{% if not loop.last %},{% endif %}
    {% endfor %}
  ]
}
</script>
{% endif %}

Twigではloop.lastで最後の項目のカンマを省けるので、glueに当たるものは要りません。名前とURLはjson_encodeフィルターでJSONの文字列にし、rawでHTMLのエスケープを外しています。"や&を含む名前でも、正しいJSONとして読み込めることを確かめました。

参考

検証環境

Ver. 3.2.36で、2026-10-05に確認しました。管理画面の表示は、お使いのバージョンによって異なる場合があります。

同じタグ付けがされている記事