WPのカスタム投稿とカスタムタクソノミーに関するメモ

カスタム投稿タイプとカスタムタクソノミーに関して調べたことをメモ的に。だいぶ散らかっている上に、調査やスキル不足のため内容が間違っている可能性もあり、読まれる方は留意ください。

[追記:2021.10.1]
大幅に内容を修正。

[追記:2021.10.4]
パーマリンク構造からカスタムタクソノミーのスラッグを取り除く方法を追加修正。

前提と実現したいこと

  • WordPressでカスタム投稿タイプを設定し専用でカスタムタクソノミーも設定
  • カスタム投稿タイプとカスタムタクソノミーは1対1の対応関係(複数のカスタムタクソノミーを設定しない)

上記の前提で以下の実現を模索。

  • パーマリンク構造からカスタムタクソノミーのスラッグ($taxonomy)を取り除きたい
  • 表示中の記事がカスタム投稿タイプか否かなどを判定したい
  • カスタム投稿タイプ名を記述するようなハードコーディングは避けたい
  • 表示中の記事のカスタム投稿タイプのスラッグを出力したい

対応策

パーマリンク構造からカスタムタクソノミーのスラッグを取り除きたい

  • カスタム投稿タイプ: products
  • カスタムタクソノミー: product_cat
  • 追加するカテゴリー: item1item1-child
  • タクソノミーは階層構造あり: item1/item1-child

上記前提の場合、Custom Post Type Permalinksを使っても通常であれば以下のようになってしまいます。


https://example.com/products/product_cat/item1/item1-child/記事のスラッグ

上記を以下のような構造にしたいと仮定します。


https://example.com/products/item1/item1-child/記事のスラッグ

結論

試した範囲では、2種類の方法である程度は実現できました。ただしいずれも問題が残る状態です。

  • カスタムタクソノミーの階層構造に対応できる方法
    (※ただし一部ページの扱いが微妙に変わってしまう可能性がある)
  • カスタムタクソノミーの階層構造に対応できない方法
    (※確認できていないが他にも不具合をもつ可能性がある)

カスタムタクソノミーの階層構造に対応できる方法

一部のWPテンプレートタグで妙な判定になりますが、カスタムタクソノミーが階層構造を持っていても対応できる方法です。ただしバッドノウハウの可能性が高いのでお勧めはしません。

  • カスタム投稿タイプを設定し、taxonomiesにカスタムタクソノミーのスラッグを追加(自力でアプリでも可)
  • カスタムタクソノミーを設定し、任意のカスタム投稿タイプに紐付けし、hierarchicalrewritehierarchicaltrueに設定
  • Custom Post Type Permalinksでカテゴリーベースでパーマリンクを設定

functions.phpやプラグイン用phpファイルなどに上記を設定後、続いて以下を記載。
なお、試作のためおかしな部分があると思われますので、その点は予め了解ください。


function my_custom_post_type_permalinks_set($termlink, $term, $taxonomy)
{
    return str_replace('/' . $taxonomy . '/', '/', $termlink);
}
add_filter('term_link', 'my_custom_post_type_permalinks_set', 11, 3);

class CustomWalker extends Walker_Category
{
    public function start_el(&$output, $category, $depth = 0, $args = [], $id = 0)
    {
        $homeUrl = home_url('/');
        $ulr = get_term_link($category);
        $path = str_replace($homeUrl, '', $ulr);
        $output .= rtrim($path, '/') . '<br>';
    }
}

function custom_rewrite_rule()
{
    $post_slug = 'カスタム投稿タイプのスラッグ';
    $tax_slug = 'カスタムタクソノミーのスラッグ';
    $terms = get_terms($tax_slug, ['hide_empty' => false]);
    $pathArray = [];

    if (!empty($terms) && !is_wp_error($terms)) {
        $paths = walk_category_tree($terms, 0, [
            'use_desc_for_title' => false,
            'style' => '',
            'walker' => new CustomWalker(),
        ]);
        $pathArray = explode('<br>', $paths);
    }

    if (!empty($pathArray)) {
        $pathArray = array_filter($pathArray, 'strlen');

        foreach ($pathArray as $path) {
            $array = explode('/', $path);
            $cat = end($array);
            add_rewrite_rule($post_slug . '/' . $path . '/?$', 'index.php?post_type=' . $post_slug . '&' . $tax_slug . '=' . $cat, 'top');
            add_rewrite_rule($post_slug . '/' . $path . '/page/([0-9]+)/?$', 'index.php?' . $tax_slug . '=' . $cat . '&paged=$matches[1]', 'top');
        }
    }
}
  add_action('init', 'custom_rewrite_rule', 0);

補足

2つの関数の役割はそれぞれ以下の通りで、両方が必要です。

  • タームのリンク先からカスタムタクソノミーのスラッグを削除する
  • リライトルールを設定して、パーマリンク からカスタムタクソノミーのスラッグを取り除く

リライトルールの方の流れは以下の通りです。

  • get_terms()walk_category_tree()を利用して、親子関係を持った状態でのURLを取得し、add_rewrite_rule()の正規表現に代入可能な値の配列を作成する
  • 作成した配列を回してadd_rewrite_rule()を実行し登録する

検索で出てくる例を用いると、add_rewrite_rule()のみでは、階層構造を持つURLで、かつ、URLの末尾が/(スラッシュ)の場合に、カスタム投稿タイプのページまで置換対象になってしまいます。

検索すると以下のような例が出てきますが、この記述はタクソノミーが階層を持つ場合には使えないと思います。


add_rewrite_rule('カスタム投稿タイプのスラッグ/([^/]+)/?$', 'index.php?post_type=カスタム投稿タイプのスラッグ&カスタムタクソノミーのスラッグ=$matches[1]', 'top');

以下の場合は2階層に対応していますが、1階層目に属する投稿や、3階層以上には使えません。
正規表現の([^/]+)/を増やすことで多層化に対応できるかもしれませんが、タクソノミーの最大階層数と記事のスラッグのある階層が一致してしまうとエラーになります。


add_rewrite_rule('カスタム投稿タイプのスラッグ/([^/]+)/([^/]+)/?$', 'index.php?post_type=カスタム投稿タイプのスラッグ&カスタムタクソノミーのスラッグ=$matches[2]', 'top');

正規表現のみで「URLがアーカイブなのか投稿ページなのか」を判別することはできないと思われたので、視点を変えて、必要なパターンを全て列記してadd_rewrite_rule()で登録する方法を検討した次第です。

親子関係を持った状態でカスタムタクソノミーのURLの一部を取得し配列にする方法

かなり無理やりなのですが以下の部分が該当します。


class CustomWalker extends Walker_Category
{
    public function start_el(&$output, $category, $depth = 0, $args = [], $id = 0)
    {
        $homeUrl = home_url('/');
        $ulr = get_term_link($category);
        $path = str_replace($homeUrl, '', $ulr);
        $output .= rtrim($path, '/') . '<br>';
    }
}

function custom_rewrite_rule()
{
    $post_slug = 'カスタム投稿タイプのスラッグ';
    $tax_slug = 'カスタムタクソノミーのスラッグ';
    $terms = get_terms($tax_slug, ['hide_empty' => false]);
    $pathArray = [];

    if (!empty($terms) && !is_wp_error($terms)) {
        $paths = walk_category_tree($terms, 0, [
            'use_desc_for_title' => false,
            'style' => '',
            'walker' => new CustomWalker(),
        ]);
        $pathArray = explode('<br>', $paths);
    }
    // 省略
}

流れは以下の通り。

  1. get_terms()で指定のカスタムタクソノミーの情報を取得
  2. walk_category_tree()に入れて、不要なHTMLタグを消す
  3. さらにwalkerを利用して、カスタム投稿タイプの親子関係を持つURLから不要な部分を削った状態で取得し、brタグを加えた文字列を作成
  4. 作成した文字列をexplode()で配列に変換する

親子関係を持つ状態で必要なパスを習得できる方法が見当たらず、本来はHTMLとして出力するwalk_category_tree()を無理やり利用している状態です。
もっとスマートな方法があると思うのですが…。

問題点

ここまでの作業である程度の動作は確認できますが、以下の問題が発生します。

  • カスタムタクソノミーを追加するたびに、パーマリンクの更新が必要になる
  • カスタムタクソノミーのアーカイブで、カスタム投稿タイプのアーカイブを判定するためのis_post_type_archive()trueとして判定されるようになる

カスタムタクソノミーのアーカイブで、is_post_type_archive()trueになると、分岐の状態によっては意図しない動作をする可能性があります。

状況にもよりますが、is_post_type_archive()でカスタム投稿タイプのアーカイブのみを判定できる必要があります。
試したところ以下の書き方で、is_post_type_archive()を使ったカスタム投稿タイプのアーカイブの判定が可能でした。


if( is_tax() === false && is_post_type_archive() ){
  //カスタム投稿タイプのアーカイブ
}

しかしながら上記のように個別に対応できたとしても、下記のような懸念点があるので現時点では案件には使えないと判断しています。
理解が進めば案件に利用可能だと判断できるとは思いますが…。

  • is_post_type_archive()がなぜtrueになるのか不明
  • is_post_type_archive()trueになるのが正常な動作なのか不明
  • is_tax() === false && is_post_type_archive()で判定できることが正常なのか不明
  • 他にも把握できていない問題が発生している可能性がある

参考サイト

カスタムタクソノミーの階層構造に対応できない方法

「カスタムタクソノミーに階層構造を持たせないか、持たせても1階層しか使わない」という制限が許容できるのであれば以下で確認できる情報で実現できました。

確認したところポイントは以下の4つかなと思います。なおプラグインは使いません。

  • カスタム投稿タイプのhas_archiveにカスタム投稿タイプのスラッグを設定する
    例:has_archive: 'product'
  • カスタム投稿タイプのrewriteのスラッグでカスタムタクソノミーを含む値を設定する
    例:'rewrite' => ['slug' => 'product/%product_cat%']
  • カスタムタクソノミーのrewriteでカスタム投稿タイプのスラッグを設定
    例:'rewrite' => ['slug' => 'product']
  • post_type_linkフィルターを用いて、カスタムタクソノミーのスラッグを置換する

問題点

  • カスタムタクソノミーで追加したカテゴリーの子カテゴリーのアーカイブにアクセスすると404になる
    例:/products/item1/は表示されるが、/products/item1/item1-child/は404
  • rewriteのスラッグにproduct/%product_cat%のような設定ができるというドキュメントはない模様で、たまたま動いているだけの可能性があるかもしれない

上記はさらに調査や試作を重ねれば解決できるかもといろいろ試していたのですが、現状私のスキルでは無理でした。

参考サイト

実際に上記の内容を試しますと、いずれも効果が確認できないかエラーや404となって意図した状態にはなりませんでした。

おそらく主な原因は、カスタムタクソノミーに階層構造を持たせ、それがパーマリンクに反映された状態を求めていたからかもしれません。

1つのカスタム投稿タイプに複数のカスタムタクソノミー

1つのカスタム投稿タイプに複数のカスタムタクソノミーを設定したい場合、Custom Post Type Permalinksでは対応できません。
何か一つのタクソノミーを選んで記述するしかなく、選ばなかった方のタクソノミーでエラーが生じます。

これはslugにタクソノミースラッグを記載するパターンの方法でも同様です。

おそらくリライトルールなどを駆使すればできるのではと考えていますが、理解不足だからか実現できませんでした。

表示されているページに関する判定と値の出力

カスタム投稿タイプとカスタムタクソノミーに関する値を取得したり判別したりする部分を検討してみました。
限られた範囲と用途内での動作(つまり動作前に条件を絞っている)を目的としているので、参考にされる方はその点留意ください。

値が取れない時の返り値はまだ検討中ですが、概ね以下のようなコードで目的は達成できそうです。


class MyClass
{
    // 以降のコメントにあるように、この変数にはカスタム投稿タイプのスラッグのみが入る想定
    public $post_slug = '';

    // まずカスタム投稿タイプのスラッグを取得
    function __construct()
    {
        if (is_singular() && get_post_type() !== 'post' && get_post_type() !== 'page' && get_post_type() !== 'attachment') {
            // is_singular()が真でpost_typeがpost,page,attachmentでなければ、カスタム投稿のページと判定できる
            $this->post_slug = get_post_type();
        } elseif (is_tax()) {
            // is_tax()はカスタム タクソノミーのアーカイブページの時のみtrueを返す
            // 参考: https://wpdocs.osdn.jp/%E9%96%A2%E6%95%B0%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9/is_tax
            $taxonomy = get_query_var('taxonomy');
            $this->post_slug = get_taxonomy($taxonomy)->object_type[0];
        } elseif (is_archive()) {
            // get_query_var('post_type')で値が取れるのは、表示しているページが「カスタム投稿」「カスタム投稿のアーカイブ」「検索結果のページ」の時のみ
            // つまりis_tax()が偽でis_archive()が真であり、get_query_var('post_type')に空文字以外が入っていれば、それは「カスタム投稿タイプのスラッグ」となる
            // 参考: https://wemo.tech/2043
            $this->post_slug = get_query_var('post_type');
        }
    }
    // カスタム投稿タイプのスラッグをそのまま返す
    public function get_custom_post_type_slug()
    {
        if($this->post_slug !== ''){
            return $this->post_slug;
        }
        return $this->post_slug;
    }
    // 投稿タイプのスラッグからlabelを取得して返す
    public function get_custom_post_type_name()
    {
        $post_obj = get_post_type_object($this->post_slug);
        if($post_obj){
            return $post_obj->label;
        }
        return null;
    }
    // カスタムタクソノミーのタクソノミー名を取得する
       // 参考: https://into-the-program.com/get-taxonomy-name/
    public function get_taxonomy_name()
    {
        $taxonomy_slug = array_keys(get_the_taxonomies());
        if ($taxonomy_slug) {
            $taxonomy = get_taxonomy($taxonomy_slug[0]);
            $taxonomy_name = $taxonomy->name;
            return $taxonomy_name;
        }
        return null;
    }
    // 表示されているページがカスタム投稿タイプに属しているかどうかを判定する
    public function belong_custom_post_type()
    {
        return (is_singular() && $this->post_slug !== '') || is_tax() || is_post_type_archive();
    }
    // 表示されているページがカスタム投稿タイプかどうかを判定する
    public function is_custom_post_type()
    {
        return is_singular() && $this->post_slug !== '';
    }
}

その他の参考サイト

WordPress Codex 日本語版のページには「今後更新されない」という趣旨の一文が書かれていますので、現状ではあまり参照するべきではありません。

しかしながら、こちらを見よと示されたサイトは非常に使いづらく感じます。
勘違いかもしれませんが、サイト内検索はフォーラム用しかなく、ドキュメント検索目的には使えないように見えました。

結び

カスタム投稿タイプとカスタムタクソノミーの組み合わせは、かなり面倒に思います。

求める条件次第では楽に設定できるものの、そこから一歩外れると難度が跳ね上がるという印象です。

パーマリンクのカスタマイズに関して

パーマリンクからカスタムタクソノミーのスラッグを取り除くことを断念しましたが、おそらく以下あたりが重要では思います。

  • カスタム投稿タイプとカスタムタクソノミーの設定
  • add_rewrite_rule()などによるリライトルールの書き換え
  • post_type_linkterm_linkリンクなど、テーマが出力する各種リンクの出力内容の書き換え

リライトルールがうまく動いていれば想定しているURLの直打ちで表示されると思いますが、テーマによって出力されたリンク先が想定している状態でなければ、結果として404になってしまいます。
もちろん逆もまた然り。

関連する事柄を全て理解している方であればかなり自由にカスタマイズできるかもしれませんが、そうではないなら、パーマリンクからカスタムタクソノミーのスラッグを削除することは諦めた方が安全ではと思います。

0人がこの記事を評価

役に立ったよという方は上の「記事を評価する」ボタンをクリックしてもらえると嬉しいです。

連投防止のためにCookie使用。SNSへの投稿など他サービスとの連動は一切ありません。

コメント欄