本ページはプロモーションが含まれている場合があります

引っ越しの準備や、ウェブ解析士の学習用テキストの編集など、いろいろプライベートは忙しい感じですが、10月の連休の時間を使ってMicrosoft Copilot(個人版)でMovable Type向けの目次出力プラグインを作ってみました(なぜ)。
なぜ静的目次プラグインを作ったのか?
このブログは現在Movable Typeで作成されています。目次表示にはjQueryを使った「jQuery.toc.js」ライブラリを利用していて、特に運用上問題や不便はありませんでした。
私は「jQuery.toc.js」のv0.3.2(2014年)を使っていました。
👇最近のバージョンはこちら。今のバージョンはjQueryは不要みたいですね。
JavaScript(jQuery)で作る目次は便利なのですが、Movable Typeは静的サイト生成(SSG)なので、クローラーが目次を認識しにくい問題が考えられるのでSEO面で不利なのかなと感じていました😣
また、ページ読み込み後にJavaScriptで目次描画をするので、目次がなかなか見れない、ページ表示ががたつくなどでユーザーが閲覧するときの体験低下も考えられました😣
JavaScriptを使った目次作成のデメリットはまとめるとこんな感じです。
- 😣SEOで不利:検索エンジンのクローラーがJSを完全に実行しない場合、目次構造が見えにくくなる
- 😣ユーザー体験の低下:ページ読み込み時にJavaScriptが実行されるまで目次が表示されない
- 😣アクセシビリティの低下:検索エンジンのクローラー同様、スクリーンリーダーが目次を正しく認識しにくい
それであれば、HTML出力時に目次が出力されていればいいのでは?ということでプラグインを作ろうと思ったのです。
Movable Typeのプラグインを使って静的生成であればHTMLを出力した時点で目次が完成しています。軽量でSEOフレンドリーになります。
実は以前Anthropic ClaudeでMovable Typeプラグイン作成をチャレンジしたことがあります。今回Microsoft Copilotを使ったのはほぼ無制限でGPT-5が使えるのと、よくMicrosoft Copilotを使っているので、実力を試してみたかったからってことです。
Copilotとのやり取りによる開発プロセス
やったことはチャットするだけ
開発といってもVisual Studio Code上で行うわけでもなく、Microsoft Copilotのチャットだけで作りました。「こうしたい」という要望をMicrosoft Copilotにチャットで投げかけ、返ってきた提案を検証していくシンプルな形です。
最初のチャットはこんな感じでした。最初は(MTMLのテンプレートタグだけの)モジュールで作成しようとしていました。
Movable Typeで<mt:EntryBody><$mt:EnrtyMore$>を評価して、その中にあるH1~H6タグを目次として出力するモジュールは作成できますか?
Microsoft Copilotから返ってきた提案はこんな感じ。
はい、Movable Typeでも「本文+続き(<mt:EntryBody>+<$mt:EntryMore$>)を評価して、その中に含まれる H1〜H6 見出しタグを抽出し、目次(TOC)を出力するモジュール」を作成できます。
ただし、Movable Typeのテンプレートタグだけでは「HTMLを解析して見出しを抽出する」処理は難しいため、プラグイン(Perlコード) でフィルタを追加するのが一般的なアプローチです。
Perlプラグインでフィルタを定義
Movable Typeは MT::Template::Context->add_global_filter を使って独自フィルタを追加できます。
そのフィルタ内で HTML をパースし、<h1>〜<h6> を抽出。
各見出しに id を付与し、目次用の <ul><li><a href="#id">見出し</a></li></ul> を生成。
ということでプラグイン作成がスタートしたわけです。
理解できていない返信はその場で質問しておく
返答の中に「独自フィルタ」という言葉が出てきたので「Movable Typeの独自フィルタを解説してください」と解説をしてもらいました。何をしているのがわからないと後々困るかもしれないので。
独自フィルタの解説の中で提示されたサンプルは以下のとおり。
グローバルモディファイアで有効にすると、文字列を逆順に出力するプラグインですね。つまり独自フィルタを作るとグローバルモディファイアが追加できるということみたいですね。
最低限必要なのは Perl モジュールファイル(例: MyFilter.pl)。
package MT::Plugin::MyFilter;
use strict;
use MT;
use MT::Template::Context;
# フィルタを追加
MT::Template::Context->add_global_filter(
myreverse => sub {
my ($text, $ctx) = @_;
return scalar reverse $text;
}
);
1;
<mt:EntryTitle myreverse="1">
プラグインは最低限「.pl」ファイル1個でもできるので、これで作ってみることにしました。.plファイル1つだけのプラグインって過去に多くありましたよね。
最初にできたプラグイン
できあがったプラグインと出力方法がこちらです。目次を出力してくれます。
package MT::Plugin::TOC;
use strict;
use MT;
use MT::Template::Context;
MT::Template::Context->add_global_filter( make_toc => sub {
my ($text, $ctx) = @_;
my $toc = "<ul class='toc'>\n";
my $count = 1;
# H1〜H6を抽出
while ($text =~ m{<(h[1-6])[^>]*>(.*?)</\1>}gis) {
my $tag = lc $1;
my $title = $2;
my $id = "heading-" . $count++;
# 見出しにidを付与
$text =~ s{(<$tag)([^>]*>.*?\Q$title\E.*?</$tag>)}{$1 id="$id"$2}i;
# TOCに追加
$toc .= qq{ <li class="$tag"><a href="#$id">$title</a></li>\n};
}
$toc .= "</ul>\n";
return $toc;
});
1;
<mt:SetVarBlock name="temp"><mt:EntryBody><$mt:EntryMore$></mt:SetVarBlock>
<$mt:Var name="temp" make_toc="1"$>
追加したい機能と訂正したい部分をチャットする
その後、以下の内容をチャットで行いました。
- 階層構造を反映した TOCに変更してください
- 最初から見出しタグにidが付いている場合でも正常に動作しますか?
- 見出しタグとして認識するレベルの指定ができるようにできますか? 例えはH3タグから目次にしたいとか、H2タグから見出しにしたいとか。デフォルトはH2タグが良いです。
- 実際にはH4まで出す方が良いので、H5タグ以下は無視できる設定も付けられますか?
- 「Table Of Contents」というプラグイン名にしたいです
- descriptionにモディファイアの使い方を加えてください
ここまで要望を伝えて自分の思っている目次出力に近づいてきました!
Movable Typeにインストール!しかし…
プラグインに警告が表示された
Movable Typeにインストールしてみました。
インストールはpluginフォルダーに作った.plファイルを入れるだけです。すると、システムのプラグイン管理画面に以下の警告メッセージが。
このプラグインは、 Movable Type 8向けにアップグレードされていません。そのため、動作しない場合があります。 MT::Template::Context::add_global_filterはRegistry (path: tags, modifier)に直してください。
メッセージをそのままチャットで聞いてみます。
インストールしたら以下のメッセージが管理画面で表示されました。解消するための修正はできますか? このプラグインは、 Movable Type 8向けにアップグレードされていません。そのため、動作しない場合があります。 MT::Template::Context::add_global_filterはRegistry (path: tags, modifier)に直してください。
Copilotから以下の回答があり、.yamlファイルと.pmファイルに分離することにしました。最近のプラグインでは標準的な作りですよね。
Movable Type 8 からは Perlコードで MT::Template::Context->add_global_filter を直接書く方法は非推奨になり、代わりに Registry(プラグインの設定ファイル)でモディファイアを登録する必要があります。
✅ 修正の方向性
- Perl側はフィルタ処理のサブルーチンだけを定義する。
- config.yaml(またはプラグインの .yaml ファイル)で「modifier」として登録する。
その後のチャットに以下の内容を依頼しました。
- config.yaml 側で「デフォルト引数」を設定できるようにしましょう
- 管理画面から変更できるように変更してください
- やはり管理画面から変更するのはなしで
パッケージ名とファイル配置の不一致
できあがったものをMovable Typeにインストールしても以下のエラーが。
failed loading package MT::Plugin::TableOfContents for routine MT::Plugin::TableOfContents::make_toc: Can't locate MT/Plugin/TableOfContents.pm in @INC
どうやらパッケージ名とファイル配置の不一致だそうで、その点を修正してもらいました。その中の説明で、ファイルの位置の指示があったのですが、普通のプラグインとは違う指示がありました。
mt/
├─ lib/
│ └─ MT/
│ └─ Plugin/
│ └─ TableOfContents.pm ← ここに本体
└─ plugins/
└─ TableOfContents/
└─ config.yaml ← Registry 定義
普通ならこんな感じですよね。
mt/
└─ plugins/
└─ TableOfContents/
├─ config.yaml
└─ lib/
└─ TableOfContents/
└─ Plugin.pm
「plugins/TableOfContents/lib/TableOfContents/ にplugin.pmとして配置する場合の書き方に書き直してください」とチャットして、この形でインストールできるように指示して、パッケージ名などの処理も変更してもらいました。
さらに使い勝手を上げる&不具合を修正
まずは動くものができたので、使いやすくする処理と、考えられるパターンで不具合をなくす処理をチャットで聞いていき完成させます。
- 「目次生成用フィルタ」と「本文に id を付与するフィルタ」を分けたサンプルコードを提示してください
- 最大見出しレベルを初期値として調整方式にできますか? 例:make_toc="2-4" なら H2を初期値 例:make_toc="3" なら H3を初期値 例:make_toc なら デフォルトのH2を初期値
- 出力するときは以下のようにしたときに、mt:EntryBody mt:EntryMore で見出しのIDは重複することはありませんか? <$mt:EntryBody add_heading_ids="1"$> <$mt:EntryMore add_heading_ids="1"$>
- カウンタを初期化する仕組みを付けてください
- Pageオブジェクトにも使えるようにしてください
- MT::ContentDataにも対応してください
説明書もCopilotで作成
ここまでMicrosoft Copilotで作成したので、使い方の説明書(readme.md)もMicrosoft Copilotに作ってもらいましょう。やり取りを記憶しているので簡単ですね🤩
使い方をくわしく書いたドキュメントの.mdファイルを作成してください
できた.mdファイルはプラグインのZIPファイルの中にそのまま入っています😊
できあがったプラグイン

プラグイン本体をダウンロード
できあがったMovable Type用目次出力プラグインはこちら。
簡単な使い方
目次作成
最初の1行目でグローバルカウンターを初期化。2行目で変数rawを初期化。4行目で処理するデータを変数rawに格納。6行目でH2~H4タグを目次として出力。
本文にカスタムフィールドなどで追加している見出しタグの要素があれば、4行目でその要素を追加してください。
<$mt:WebSiteURL reset_heading_counter setvar="dummy"$>
<$mt:SetVar name="raw" value=""$>
<mt:SetVarBlock name="raw"><$mt:EntryBody$><$mt:EntryMore$></mt:SetVarBlock>
<nav class="toc-area">
<$mt:Var name="raw" make_toc="2-4"$>
</nav>
本文出力
グローバルカウンターを初期化したあとに、本文の見出しタグ(Hタグ)のid要素がないものにidを付けて出力します。
<$mt:WebSiteURL reset_heading_counter setvar="dummy"$>
<div class="entry-body">
<$mt:EntryBody add_heading_ids="1"$>
<$mt:EntryMore add_heading_ids="1"$>
</div>
※何度も<$mt:EntryBody$>を呼び出しているので、以下のように先に見出しのidを付けたものを用意した方が何度も<$mt:EntryBody$>を呼び出さなくてもいいので負荷が減るかもしれません。
<mt:SetVarBlock name="raw_body"><$mt:EntryBody add_heading_ids="1"$></mt:SetVarBlock>
<mt:SetVarBlock name="raw_more"><$mt:EntryMore add_heading_ids="1"$></mt:SetVarBlock>
まとめ
Microsoft Copilotのおかげであっという間にMovable Typeの静的目次作成プラグインが完成しました! これでMovable TypeのブログがよりSEOフレンドリーになりますね。なお、このブログでは実装済です!
みなさんもぜひ生成AIでプラグイン作成にチャレンジしてみてくださいね!
それでは、また。
連休なので生成AIのMicrosoft Copilot(個人版)を使ってコーディング。
— 笹川純一(DTPオペ➡️ウェブ制作・EC運営担当) (@jdash2000) October 12, 2025
作ったものはMovable Type用目次出力プラグイン。
jQuery.toc.jsを使った目次表示を今まで使っていて問題はないのですが、MTなので静的表示できたらなおよし!ってことでチャレンジ。… pic.twitter.com/AYCRNfvIbw