WordPressの termmeta(タームメタ)は、カテゴリやタグなどのターム(taxonomy term)ごとに任意のメタデータを保存するためのWordPress標準APIです。これを使えばカテゴリページ・タグページのSEOタイトルやメタディスクリプションを、AIOSEOやYoast SEOなどのプラグインに頼らず自作テーマだけで管理できます。
この記事はWordPress投稿のSEOカスタムフィールド実装ガイドの続編で、投稿側で自作した「SEOタイトル・メタディスクリプション」と同じ仕組みをカテゴリページ・タグページにも実装する方法を解説します。register_term_meta から編集画面のフィールド追加、保存フック、wp_head での出力まで、WordPress公式APIだけで動く最小実装をコピペで動く形で順に紹介します。
termmetaとは|postmetaとの違いと使いどころ
termmetaは、カテゴリ・タグ・カスタムタクソノミーのタームに紐づくメタデータをデータベース(wp_termmeta テーブル)に保存する仕組みです。投稿に対するメタデータが wp_postmeta に保存されるのと同じ構造で、ターム版の「カスタムフィールド」と理解すると分かりやすいです。
postmeta と termmeta の主な違いを整理すると次のようになります。
| 項目 | postmeta | termmeta |
|---|---|---|
| 保存先テーブル | wp_postmeta | wp_termmeta |
| 紐づく対象 | 投稿・固定ページ・カスタム投稿 | カテゴリ・タグ・カスタムタクソノミー |
| 取得関数 | get_post_meta() | get_term_meta() |
| 更新関数 | update_post_meta() | update_term_meta() |
| 登録関数 | register_post_meta() | register_term_meta() |
| 編集画面フック | add_meta_boxes | {$taxonomy}_edit_form_fields/{$taxonomy}_add_form_fields |
| 保存フック | save_post | edited_{$taxonomy}/created_{$taxonomy} |
関数名・フック名のパターンが post_meta と term_meta で対称になっているため、投稿側で自作SEOメタを実装した経験があれば、ほぼ同じ設計をターム側に転用できます。
なぜカテゴリ・タグページにもSEOメタを自作するのか
カテゴリページ・タグページは、関連する複数記事を束ねるハブとして、特定のキーワードで検索エンジンから直接流入を獲得できる可能性があります。実際にGoogle検索で「WordPress 関数 一覧」のような複合キーワードでカテゴリ・タグページが上位表示されるケースは珍しくありません。
しかし WordPress標準ではタクソノミーページの <title> は「カテゴリ名」「タグ名」が自動生成されるだけで、<meta name="description"> に至っては何も出力されません。これを自前で制御するために termmeta を使った専用フィールドが必要になります。
SEOプラグインを使わずに自作する判断軸については WordPressのSEOを自作するメリット・デメリット にまとめています。本記事は「自作する」と決めた前提で、タクソノミー側の実装手順だけを取り上げます。
実装の全体像(4ステップ)
タクソノミー用SEOメタの実装は次の4ステップで完成します。
- ステップ1:
register_term_metaでメタキーをWordPressに登録する(REST API公開・型・サニタイズも同時設定) - ステップ2:
{$taxonomy}_edit_form_fields/{$taxonomy}_add_form_fieldsでカテゴリ・タグの編集画面にフィールドを追加する - ステップ3:
edited_{$taxonomy}/created_{$taxonomy}で入力値をtermmetaに保存する(nonce・権限・サニタイズ) - ステップ4:
wp_headでis_category()/is_tag()/is_tax()を分岐し、<title>と<meta name="description">を出力する
4ステップすべてが必須です。ステップ1(register_term_meta)を省略しても動作はしますが、サニタイズ・REST API公開・型の宣言ができないため、安全性と拡張性の観点から最初に登録するのを推奨します。コードは functions.php またはテーマ内の任意のPHPファイルにそのまま貼って動作確認できます。
ステップ1:register_term_metaでメタ項目を登録する
register_term_meta は、特定のタクソノミーに対してメタキーを公式に登録するWordPress標準関数です。サニタイズ関数・REST API公開フラグ・型情報を一度に宣言できるため、保存・取得時の安全性を WordPress 側に任せられます。
<?php
add_action( 'init', 'my_term_seo_register_meta' );
function my_term_seo_register_meta() {
$taxonomies = array( 'category', 'post_tag' );
foreach ( $taxonomies as $taxonomy ) {
register_term_meta( $taxonomy, '_my_term_seo_title', array(
'type' => 'string',
'description' => 'タクソノミーページのSEOタイトル',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => function() {
return current_user_can( 'manage_categories' );
},
) );
register_term_meta( $taxonomy, '_my_term_meta_description', array(
'type' => 'string',
'description' => 'タクソノミーページのメタディスクリプション',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'sanitize_textarea_field',
'auth_callback' => function() {
return current_user_can( 'manage_categories' );
},
) );
}
}
メタキーの先頭にアンダースコアを付ける(例:_my_term_seo_title)のは、postmeta と同じく「保護されたメタ」として扱われ、ターム編集画面の汎用カスタムフィールドUIに二重表示されないようにするためです。auth_callback には manage_categories 権限チェックを入れ、REST API経由での書き込みを編集者以上に限定しています。
sanitize_callback に sanitize_text_field(タイトル用)と sanitize_textarea_field(メタディスクリプション用)を使い分けるのが WordPress 公式の作法です。これを宣言しておくと、後述の保存処理で個別にサニタイズを呼ばなくても WordPress 側で自動的に通してくれます。
ステップ2:カテゴリ・タグの編集画面にフィールドを追加する
タクソノミー編集画面のフォームには2つのフックが用意されています。既存タームの「編集」画面用と「新規追加」画面用で別のフック名になっている点に注意します。
| 用途 | フック名 | 第一引数 |
|---|---|---|
| 既存タームの編集画面 | {$taxonomy}_edit_form_fields | WP_Term オブジェクト |
| 新規ターム追加画面 | {$taxonomy}_add_form_fields | タクソノミー名(string) |
カテゴリとタグの両方に同じフィールドを追加する例です。category_edit_form_fields と post_tag_edit_form_fields の2つに対して同じコールバックを登録します。
<?php
// 編集画面(既存タームを編集する画面)
add_action( 'category_edit_form_fields', 'my_term_seo_edit_fields' );
add_action( 'post_tag_edit_form_fields', 'my_term_seo_edit_fields' );
function my_term_seo_edit_fields( $term ) {
$seo_title = get_term_meta( $term->term_id, '_my_term_seo_title', true );
$seo_desc = get_term_meta( $term->term_id, '_my_term_meta_description', true );
wp_nonce_field( 'my_term_seo_save', 'my_term_seo_nonce' );
?>
<tr class="form-field">
<th scope="row">
<label for="my_term_seo_title">SEOタイトル</label>
</th>
<td>
<input type="text" id="my_term_seo_title" name="my_term_seo_title"
value="<?php echo esc_attr( $seo_title ); ?>"
style="width:95%;" />
<p class="description">検索結果の<title>に表示されます(推奨32文字程度)。</p>
</td>
</tr>
<tr class="form-field">
<th scope="row">
<label for="my_term_meta_description">メタディスクリプション</label>
</th>
<td>
<textarea id="my_term_meta_description" name="my_term_meta_description"
rows="4" style="width:95%;"><?php echo esc_textarea( $seo_desc ); ?></textarea>
<p class="description">検索結果のスニペットに表示されます(推奨120〜160文字)。</p>
</td>
</tr>
<?php
}
新規追加画面用の add_form_fields 側も同じくフィールドを足しておくと、新しいカテゴリ・タグを作成する時点でSEOメタを入力できます。HTML構造が <tr><th><td> ではなく <div class="form-field"> に変わる点だけ注意してください。
<?php
// 新規追加画面
add_action( 'category_add_form_fields', 'my_term_seo_add_fields' );
add_action( 'post_tag_add_form_fields', 'my_term_seo_add_fields' );
function my_term_seo_add_fields( $taxonomy ) {
wp_nonce_field( 'my_term_seo_save', 'my_term_seo_nonce' );
?>
<div class="form-field">
<label for="my_term_seo_title">SEOタイトル</label>
<input type="text" id="my_term_seo_title" name="my_term_seo_title" value="" />
<p>検索結果の<title>に表示されます。</p>
</div>
<div class="form-field">
<label for="my_term_meta_description">メタディスクリプション</label>
<textarea id="my_term_meta_description" name="my_term_meta_description" rows="4"></textarea>
<p>検索結果のスニペットに表示されます。</p>
</div>
<?php
}
編集画面と新規追加画面で同じ nonce を出すことで、後述の保存処理を一本化できます。出力時に esc_attr・esc_textarea を必ず通すのも postmeta 編集画面と同じ作法です。
ステップ3:入力値をtermmetaに保存する
タームを編集または新規作成したときに発火するフックを使って update_term_meta を呼び、wp_termmeta テーブルに保存します。save_post に対応するフックが「編集」と「作成」で別名になっている点だけ気を付ければ、構造は postmeta の保存処理とほぼ同じです。
<?php
// 編集時(既存タームを保存したとき)
add_action( 'edited_category', 'my_term_seo_save_meta' );
add_action( 'edited_post_tag', 'my_term_seo_save_meta' );
// 新規作成時
add_action( 'created_category', 'my_term_seo_save_meta' );
add_action( 'created_post_tag', 'my_term_seo_save_meta' );
function my_term_seo_save_meta( $term_id ) {
// 1. nonce検証
if ( ! isset( $_POST['my_term_seo_nonce'] )
|| ! wp_verify_nonce( $_POST['my_term_seo_nonce'], 'my_term_seo_save' ) ) {
return;
}
// 2. 権限チェック
if ( ! current_user_can( 'manage_categories' ) ) {
return;
}
// 3. サニタイズして保存
if ( isset( $_POST['my_term_seo_title'] ) ) {
update_term_meta(
$term_id,
'_my_term_seo_title',
sanitize_text_field( wp_unslash( $_POST['my_term_seo_title'] ) )
);
}
if ( isset( $_POST['my_term_meta_description'] ) ) {
update_term_meta(
$term_id,
'_my_term_meta_description',
sanitize_textarea_field( wp_unslash( $_POST['my_term_meta_description'] ) )
);
}
}
権限チェックは edit_post ではなく manage_categories を使います。これがタクソノミー編集に対する WordPress 標準のケイパビリティです。wp_unslash で WordPress 自動エスケープを外してからサニタイズに渡すのも postmeta と同じ流儀です。
postmeta側の save_post と違い、ターム保存フックには「自動保存(DOING_AUTOSAVE)」の概念がないため、その分岐は不要です。
ステップ4:wp_headでSEOタイトルとメタディスクリプションを出力する
termmeta に保存された値をフロントに出力するには、wp_head で現在のページがカテゴリ・タグページかどうかを判定し、get_queried_object() から term_id を取得します。
SEOタイトルは document_title_parts フィルタ、メタディスクリプションは wp_head での直接 echo が WordPress 公式の流儀です。
<?php
// SEOタイトル(document_title_parts フィルタを使う)
add_filter( 'document_title_parts', 'my_term_seo_title_filter' );
function my_term_seo_title_filter( $parts ) {
if ( ! is_category() && ! is_tag() && ! is_tax() ) {
return $parts;
}
$term = get_queried_object();
if ( ! $term || ! isset( $term->term_id ) ) {
return $parts;
}
$seo_title = get_term_meta( $term->term_id, '_my_term_seo_title', true );
if ( ! empty( $seo_title ) ) {
$parts['title'] = $seo_title;
}
return $parts;
}
// メタディスクリプション(wp_headで直接出力)
add_action( 'wp_head', 'my_term_seo_meta_description', 1 );
function my_term_seo_meta_description() {
if ( ! is_category() && ! is_tag() && ! is_tax() ) {
return;
}
$term = get_queried_object();
if ( ! $term || ! isset( $term->term_id ) ) {
return;
}
$seo_desc = get_term_meta( $term->term_id, '_my_term_meta_description', true );
// フォールバック:未入力ならターム説明文を160字で切る
if ( empty( $seo_desc ) ) {
$seo_desc = wp_strip_all_tags( term_description( $term->term_id ) );
$seo_desc = mb_substr( $seo_desc, 0, 160 );
}
if ( ! empty( $seo_desc ) ) {
printf(
'<meta name="description" content="%s" />' . "\n",
esc_attr( $seo_desc )
);
}
}
分岐に is_tax() も加えているのは、カスタムタクソノミーのアーカイブページにも対応するためです。カテゴリ・タグだけで運用しているサイトでも入れておくと、将来カスタムタクソノミーを追加したときに修正不要になります。
フォールバックには term_description()(ターム編集画面の「説明」フィールド)を使っています。これは postmeta側で get_the_excerpt() を使うのと同じ発想で、SEOメタ未入力タームでも空タグを出さない安全策です。
SEOタイトル用とdescription用は別々のフィールドにする
タクソノミーには標準で「説明(description)」フィールドが存在し、これは term_description() で取得してカテゴリページ・タグページの上部に表示される「読者向けの説明文」です。SEOメタディスクリプション(検索結果のスニペット)と役割が違うため、必ず別フィールドとして実装します。
| フィールド | 表示される場所 | 読み手 | 適切な文体 |
|---|---|---|---|
| 標準 description | カテゴリ・タグページ上部 | ページ訪問者 | 「この一覧では○○について解説します」など読者向けの導入文 |
| termmeta description | 検索結果のスニペット | 検索結果クリック前のユーザー | 「○○を実装する手順を15記事で解説。初心者から逆引きで使えます」など便益訴求型 |
「同じ説明文を両方に使い回す」と、検索結果に表示するスニペットがページ上部の前置きと丸ごと同じになってしまい、クリック率(CTR)が下がります。1つのフィールドで兼用するのは一見ラクですが、運用が進んだあとに分離するコストは大きいため、最初から別フィールド設計にしておくのが安全です。meta descriptionの書き方ガイド で文字数・テンプレート・確認方法を解説しています。
カスタムタクソノミーへの拡張
イベント・スタッフ・商品カテゴリのようなカスタムタクソノミーを使っている場合も、同じ仕組みでSEOメタを追加できます。ステップ1〜3でターゲットにするタクソノミー配列を増やすだけです。
<?php
// 対応するタクソノミーを配列で増やすだけ
$taxonomies = array( 'category', 'post_tag', 'genre', 'product_cat' );
foreach ( $taxonomies as $taxonomy ) {
// register_term_meta は同じ
register_term_meta( $taxonomy, '_my_term_seo_title', /* ... */ );
// 編集画面・新規追加画面フックも動的に登録
add_action( "{$taxonomy}_edit_form_fields", 'my_term_seo_edit_fields' );
add_action( "{$taxonomy}_add_form_fields", 'my_term_seo_add_fields' );
// 保存フックも同じ
add_action( "edited_{$taxonomy}", 'my_term_seo_save_meta' );
add_action( "created_{$taxonomy}", 'my_term_seo_save_meta' );
}
注意点として、register_taxonomy 時に show_in_rest => true が指定されていないカスタムタクソノミーは REST API で見えません。SEOプラグインや管理スクリプトから操作する予定がある場合は、タクソノミー登録側で REST 公開を有効化しておきます。
投稿側のSEOメタ実装と組み合わせる
本記事の termmeta 実装と、投稿側のSEOカスタムフィールド実装 を組み合わせると、サイト内の 投稿・固定ページ・カテゴリ・タグ・カスタムタクソノミー すべてに対して SEO タイトル・メタディスクリプションを自作のメタフィールドだけで管理できる構成になります。
運用上のおすすめは、両方の出力処理を wp_head で1関数にまとめておくことです。「投稿ページなら postmeta から」「タクソノミーページなら termmeta から」と分岐を1つの場所に集約すれば、フォールバック設計や出力順(titleタグ→meta description→OGP→Twitter Card)の制御が一貫します。
実装した自作SEOメタが 本番でちゃんと出力されているか を確認するには、WordPress公式ディレクトリで配布している ORECTIC SEO CHECK(無料プラグイン)が役立ちます。各ページのSEOタイトル・メタディスクリプション・OGP・構造化データの設定状況を一覧で確認できるので、自作タームページが意図通りの値を返しているかひと目で判別できます。
よくある実装ミス5つと回避策
termmeta 自作実装でよく見かけるトラブルと、その回避方法を5つ挙げます。公開前のチェックリストとして使ってください。
| ミス | 症状 | 回避策 |
|---|---|---|
| 新規追加画面のフックを忘れる | 新しく作ったタームでSEOメタを入力できない | {$taxonomy}_add_form_fields も edit_form_fields とセットで登録する |
権限ケイパビリティを edit_post にしてしまう | 編集者がタクソノミーを保存できない | タクソノミーは manage_categories を使う |
分岐に is_tax() を入れ忘れる | カスタムタクソノミーで出力されない | is_category() || is_tag() || is_tax() をセットで書く |
| フォールバック未設計 | 未入力タームで空タイトル・空descriptionが出力される | 空のとき term_description() を160字で切って代用 |
| 出力時のエスケープ忘れ | termmeta経由でHTML/JS注入(XSS)が成立 | esc_attr または esc_html を必ず通す |
特に1つ目(新規追加画面のフック忘れ)は実装時に発見しにくいミスです。最初に作ったタームでは編集画面から入力できるので動作したように見えますが、その後に追加した新規タームではフィールドが表示されず、編集画面を開かないと気づけません。
まとめ|投稿+タクソノミーで自作SEO構成が完成する
WordPress標準の termmeta API(register_term_meta + get_term_meta + update_term_meta)と、4種類のフック(edit_form_fields/add_form_fields/edited_{$taxonomy}/created_{$taxonomy})だけで、カテゴリ・タグページのSEOメタは自作できます。実装の核は postmeta 側と同じで、フック名と権限ケイパビリティ(manage_categories)が変わるだけです。
投稿側の自作実装(WordPress投稿のSEOカスタムフィールド実装ガイド)と組み合わせれば、サイト内すべてのページタイプでSEOタイトル・メタディスクリプションを自前で管理できる状態が完成します。SEOプラグインに依存しないテーマを目指す場合の必須セットです。
よくある質問(FAQ)
Q. termmetaとpostmetaは何が違いますか?
保存先テーブルが wp_termmeta か wp_postmeta かの違いです。紐づく対象がターム(カテゴリ・タグ・カスタムタクソノミー)か投稿かが異なるだけで、関数名・フック名・サニタイズの考え方はほぼ対称になっています。投稿側で自作SEOメタを実装した経験があれば、同じ設計をターム側に転用できます。
Q. プラグインを使わずにカテゴリ・タグのSEOタイトルを変更できますか?
できます。register_term_meta でメタキーを登録し、ターム編集画面に入力フィールドを追加した上で、document_title_parts フィルタで <title> を差し替えるのがWordPress公式の最小構成です。SEOプラグイン特有の機能(XMLサイトマップ・スキーマ自動生成など)が必要なければ、これだけで標準的なSEOタイトル制御は完結します。
Q. カスタムタクソノミーでも同じコードで動きますか?
動きます。register_term_meta と各種フックは第一引数にタクソノミー名を取るため、category・post_tag の代わりにカスタムタクソノミー名(例:genre・product_cat)を渡せば同じ仕組みで動作します。ただしREST API公開が必要な場合は、register_taxonomy 側で show_in_rest が true になっているかを確認してください。
Q. 編集画面で入力した値が保存されません。原因は?
よくある原因は3つです。1つ目は保存フックを save_post にしている(タクソノミーは edited_{$taxonomy}/created_{$taxonomy} が正解)。2つ目は権限チェックを edit_post で書いている(タクソノミーは manage_categories)。3つ目は nonce 名が編集画面と保存処理で一致していない。この3点を順に確認してください。
Q. wp_headでメタディスクリプションが出力されません。何を確認すればいい?
まず is_category() || is_tag() || is_tax() の分岐に該当しているか、テンプレートで wp_head() が呼ばれているかを確認します。次に get_queried_object() が WP_Term オブジェクトを返しているか(フロントページや404でnullになる場合あり)。最後に termmeta が空でフォールバックも空になっていないかをチェックしてください。
Q. 既存のSEOプラグインから termmeta データを移行できますか?
可能です。Yoast SEOは wp_termmeta に wpseo_title・wpseo_desc といったキーで保存しているため、get_term_meta で旧キーから読んで自作キー(例:_my_term_seo_title)に update_term_meta で書き写すマイグレーションスクリプトを書けます。Rank Math や All in One SEO は保存形式が異なるため、各プラグインのドキュメントで保存先(termmeta/専用テーブル)を確認してから移行スクリプトを設計してください。
