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 )」
続けて「表示設定」タブの「ブログ」で、階層の起点を「ルート」にし、階層の数を空欄にします。空欄にすると、ルートブログから今いるブログまでのすべての階層が並びます。
起点の代替ラベルに「ホーム」と入れると、先頭のルートブログ名が「ホーム」に置き換わります。ルートブログの名前がサイト名になっているなら、ここで短くしておくと読みやすくなります。
カテゴリーは初期設定のままで、ルートのカテゴリーからすべての階層が並びます。
保存したら、テンプレートのBEGIN_MODULEにモジュールIDを指定します。
<!-- BEGIN_MODULE Topicpath id="topicpath" -->子ブログのエントリーを開き直すと、「ホーム」から始まり、エントリー名まで並びます。
検索結果向けに構造化データを出す
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用にエスケープされるので、そのままだとタイトルの"が"のまま構造化データに入ります。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として読み込めることを確かめました。
参考
ビルトインモジュール。Topicpathのスニペットと、使える変数の一覧があります
校正オプション。
jsonEscapeの説明があります
検証環境
Ver. 3.2.36で、2026-10-05に確認しました。管理画面の表示は、お使いのバージョンによって異なる場合があります。




