WordPressカテゴリ・タグにSEOタイトル/メタディスクリプションを追加する方法|termmeta実装ガイド


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 の主な違いを整理すると次のようになります。

項目postmetatermmeta
保存先テーブルwp_postmetawp_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_postedited_{$taxonomy}created_{$taxonomy}

関数名・フック名のパターンが post_metaterm_meta で対称になっているため、投稿側で自作SEOメタを実装した経験があれば、ほぼ同じ設計をターム側に転用できます。


なぜカテゴリ・タグページにもSEOメタを自作するのか

カテゴリページ・タグページは、関連する複数記事を束ねるハブとして、特定のキーワードで検索エンジンから直接流入を獲得できる可能性があります。実際にGoogle検索で「WordPress 関数 一覧」のような複合キーワードでカテゴリ・タグページが上位表示されるケースは珍しくありません。

しかし WordPress標準ではタクソノミーページの <title> は「カテゴリ名」「タグ名」が自動生成されるだけで、<meta name="description"> に至っては何も出力されません。これを自前で制御するために termmeta を使った専用フィールドが必要になります。

SEOプラグインを使わずに自作する判断軸については WordPressのSEOを自作するメリット・デメリット にまとめています。本記事は「自作する」と決めた前提で、タクソノミー側の実装手順だけを取り上げます。


実装の全体像(4ステップ)

タクソノミー用SEOメタの実装は次の4ステップで完成します。

  1. ステップ1register_term_meta でメタキーをWordPressに登録する(REST API公開・型・サニタイズも同時設定)
  2. ステップ2{$taxonomy}_edit_form_fields{$taxonomy}_add_form_fields でカテゴリ・タグの編集画面にフィールドを追加する
  3. ステップ3edited_{$taxonomy}created_{$taxonomy} で入力値をtermmetaに保存する(nonce・権限・サニタイズ)
  4. ステップ4wp_headis_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_callbacksanitize_text_field(タイトル用)と sanitize_textarea_field(メタディスクリプション用)を使い分けるのが WordPress 公式の作法です。これを宣言しておくと、後述の保存処理で個別にサニタイズを呼ばなくても WordPress 側で自動的に通してくれます。


ステップ2:カテゴリ・タグの編集画面にフィールドを追加する

タクソノミー編集画面のフォームには2つのフックが用意されています。既存タームの「編集」画面用と「新規追加」画面用で別のフック名になっている点に注意します。

用途フック名第一引数
既存タームの編集画面{$taxonomy}_edit_form_fieldsWP_Term オブジェクト
新規ターム追加画面{$taxonomy}_add_form_fieldsタクソノミー名(string)

カテゴリとタグの両方に同じフィールドを追加する例です。category_edit_form_fieldspost_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_attresc_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_fieldsedit_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_fieldsadd_form_fieldsedited_{$taxonomy}created_{$taxonomy})だけで、カテゴリ・タグページのSEOメタは自作できます。実装の核は postmeta 側と同じで、フック名と権限ケイパビリティ(manage_categories)が変わるだけです。

投稿側の自作実装(WordPress投稿のSEOカスタムフィールド実装ガイド)と組み合わせれば、サイト内すべてのページタイプでSEOタイトル・メタディスクリプションを自前で管理できる状態が完成します。SEOプラグインに依存しないテーマを目指す場合の必須セットです。


よくある質問(FAQ)

Q. termmetaとpostmetaは何が違いますか?

保存先テーブルが wp_termmetawp_postmeta かの違いです。紐づく対象がターム(カテゴリ・タグ・カスタムタクソノミー)か投稿かが異なるだけで、関数名・フック名・サニタイズの考え方はほぼ対称になっています。投稿側で自作SEOメタを実装した経験があれば、同じ設計をターム側に転用できます。

Q. プラグインを使わずにカテゴリ・タグのSEOタイトルを変更できますか?

できます。register_term_meta でメタキーを登録し、ターム編集画面に入力フィールドを追加した上で、document_title_parts フィルタで <title> を差し替えるのがWordPress公式の最小構成です。SEOプラグイン特有の機能(XMLサイトマップ・スキーマ自動生成など)が必要なければ、これだけで標準的なSEOタイトル制御は完結します。

Q. カスタムタクソノミーでも同じコードで動きますか?

動きます。register_term_meta と各種フックは第一引数にタクソノミー名を取るため、categorypost_tag の代わりにカスタムタクソノミー名(例:genreproduct_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_termmetawpseo_titlewpseo_desc といったキーで保存しているため、get_term_meta で旧キーから読んで自作キー(例:_my_term_seo_title)に update_term_meta で書き写すマイグレーションスクリプトを書けます。Rank Math や All in One SEO は保存形式が異なるため、各プラグインのドキュメントで保存先(termmeta/専用テーブル)を確認してから移行スクリプトを設計してください。


関連記事