【WordPress】カスタム投稿タイプのパーマリンクをいい感じにする方法の備忘録【パーマリンク】

普段からWordPressよく触ってるくせに「あ、カスタム投稿のパーマリンクってどうすりゃいい感じになるんだっけ（忘却）」となるアホな筆者のための備忘録。
標準の投稿については[以前書いた記事](https://qiita.com/ryujisanagi/items/cd29b52fb9b45c1dadec)でふわっと書いておりますので、よろしければそちらもご参照くださいまし。


# 条件
- カスタム投稿タイプのパーマリンクを、親→子→孫となるような構造にする。（下記の『目標のパーマリンク構造』を参照）
- `get_permalink()`や`get_post_type_archive_link()`など、パーマリンクを取得する系統のWordPress標準関数でちゃんと目的のパーマリンクを取得できるようにする
- XMLサイトマップを吐き出すプラグインやパンくずリストを実装するプラグイン（筆者は共にYoast SEOで済ませる場合が多い）や、その他さまざまなプラグインを入れてもちゃんと問題なく動くようにする
- ~~忘れたらコピペすればOKな記事にする~~

## 目標のパーマリンク構造

- カスタム投稿アーカイブ
`https://www.example.com/{カスタム投稿スラッグ}/`

- カスタム投稿アーカイブ2ページ目以降
`https://www.example.com/{カスタム投稿スラッグ}/page/{ページ数}/`

- カスタム投稿に紐づくカスタムタクソノミーのアーカイブ
`https://www.example.com/{カスタム投稿スラッグ}/{タームスラッグ}/`

- カスタム投稿に紐づくカスタムタクソノミーのアーカイブ2ページ目以降
`https://www.example.com/{カスタム投稿スラッグ}/{タームスラッグ}/page/{ページ数}/`

- カスタム投稿の個別ページ
`https://www.example.com/{カスタム投稿スラッグ}/{タームスラッグ}/{投稿ID or スラッグ}/`

- カスタム投稿の個別ページ、ページ区切り使用時の2ページ目以降
`https://www.example.com/{カスタム投稿スラッグ}/{タームスラッグ}/{投稿ID or スラッグ}/{ページ数}/`

要するに、パンくずリストで言えば **【ホーム > 投稿タイプ名 > カテゴリ名 > 記事】** って感じのURL構造になるようにする。

# ここから実装手順

## 1. カスタム投稿タイプ、カスタムタクソノミーを設定する

`function.php`に以下をコピペ。`{投稿タイプ名}`、`{投稿タイプスラッグ}`、`{タクソノミー名}`、`{タクソノミースラッグ}`はそれぞれ適宜変更する。

```php:function.php
function register_my_custom_posts() {
	$post_args = array(
		// 特に関連しない部分はガッツリ削っているので（labels等）、良しなに設定すること。
		'label' => __( '{投稿タイプ名}', '' ),
		'show_in_rest' => true,		// ブロックエディタ（Gutenberg）を使うかどうか。使わないならfalseにする
		'has_archive' => true,		// 必須
		'rewrite' => array(
			'with_front' => false,	// 必須
		),
		'taxonomies' => array( '{タクソノミースラッグ}' ),
	);

	$tax_args = array(
		// 良しなに。
		'label' => __( '{タクソノミー名}', '' ),
		'show_in_rest' => true,		// カスタム投稿のshow_in_restをtrueにしている場合、タクソノミーもtrueにしないと編集画面にUIが表示されない
		'rewrite' => array(
			'with_front' => false,	// 必須
		),
		'show_admin_column' => true,	// 関連付けられている投稿タイプがある場合、その投稿タイプのメニューにタクソノミー一覧のリンクが追加される。基本はtrueにしとくと便利
		'hierarchical' => true,		// チェックボックスのUIにする（階層を持たせる）ならtrue。GutenbergではこれでしかUIを変えられない 
		/*'meta_box_cb' => 'post_categories_meta_box',*/	// hierarchicalをfalseにしたい（階層は持たせたくない）けどチェックボックスUIにしたい場合はこれを追加（ただしクラシックエディタでしか有効にならない）
	);

	register_post_type( '{投稿タイプスラッグ}', $post_args );
	register_taxonomy( '{タクソノミースラッグ}', array( '{投稿タイプスラッグ}' ), $tax_args );
}
add_action( 'init', 'register_my_custom_posts' );
```
特に解説するようなことは無い…はず。
ちなみにここでは`register_post_type()`を使って投稿タイプ・タクソノミーを追加しているが、Custom Post Type UIとかを使っても良い。
その場合はカスタム投稿タイプでは **『アーカイブあり』をtrue** に、 **『フロントでのリライト』をfalse** に。
カスタムタクソノミーでは **『フロントでのリライト』をfalse** に。（他の設定は対応する箇所を適宜参照する）

あんまり関係ない話だけど、`'hierarchical'`は基本`true`で良いのではと個人的には思う。入力形式のUIってぶっちゃけ使いづらいし…
'meta_box_cb'`がGutenbergだと無視される問題は現在（2021年8月末）でも修正されていない。Gutenbergが出た頃からずっとある問題なんだけど…一応、[issueとして挙げられてはいる模様。](https://github.com/WordPress/gutenberg/issues/13816)

## 2. カスタム投稿のパーマリンクを設定する

`function.php`に以下をコピペ。`{投稿タイプスラッグ}`、`{タクソノミースラッグ}`はそれぞれ適宜変更する。

```php:function.php
function my_cpt_permalinks($post_type, $post_type_object) {
	if ($post_type === '{投稿タイプスラッグ}') {
		global $wp_rewrite;

		$rewrite_tag = '%{投稿タイプスラッグ}_id%';
		//$rewrite_tag = '%{投稿タイプスラッグ}name%';		// スラッグ（通常の投稿でいう%postname%）にする場合

		$permastruct = $post_type.'/%'. '{タクソノミースラッグ}' .'%/'.$rewrite_tag;

		add_rewrite_tag($rewrite_tag, '([0-9]+)', "post_type={$post_type}&p=");
		//add_rewrite_tag($rewrite_tag, '([^/]+)', "post_type={$post_type}&name=");	// スラッグにする場合

		add_permastruct($post_type, $permastruct, false);

		add_rewrite_rule("{$post_type}/?$", "index.php?post_type={$post_type}", 'top');
		add_rewrite_rule("{$post_type}/{$wp_rewrite->pagination_base}/([0-9]{1,})/?$", "index.php?post_type={$post_type}".'&paged=$matches[1]', 'top');

		add_rewrite_rule("{$post_type}/([^/]+)/([0-9]{1,})/([0-9]{1,})/?$", "index.php?post_type={$post_type}&p=".'$matches[2]&page=$matches[3]', 'top');
		//add_rewrite_rule("{$post_type}/([^/]+)/((?!({$wp_rewrite->pagination_base}))[^/]+)/([0-9]{1,})/?$", "index.php?post_type={$post_type}&name=".'$matches[2]&page=$matches[4]', 'top');	// スラッグにする場合
	}
}
add_action('registered_post_type', 'my_cpt_permalinks', 10, 2);
```

シングルページのパーマリンク設定は、`add_rewrite_tag`と`add_permastruct`。
`add_rewrite_rule`の1つ目が通常アーカイブ、2つめがアーカイブの2ページ目以降の、3つ目がシングルページの2ページ目以降のパーマリンク設定。
パーマリンクをスラッグにする場合、タクソノミーアーカイブの2ページ目以降の`pagination_base`部分と投稿スラッグの位置がかぶってしまうため、`page`で始まる文字列はリライトルールから除外するようにする。
そのため、投稿スラッグをパーマリンクにする場合は「page～」というようなスラッグは使えない事に注意。特にこだわりが無ければ投稿IDを使う方がラク。
リライトルール周りの解説を始めるとそれだけですっごい記述量になってしまうのであとは割愛するが、必須の記述は上記のみでOK。~~筆者はこの辺を毎回忘れる。~~



## 3. カスタムタクソノミーアーカイブのパーマリンクを設定する

`function.php`に以下をコピペ。`{投稿タイプスラッグ}`、`{タクソノミースラッグ}`はそれぞれ適宜変更する。

```php:function.php
function my_taxes_permalinks($taxonomy, $object_type, $args) {
	global $wp_rewrite;
	if ($taxonomy === '{タクソノミースラッグ}') {
		$rewrite_tag = "%{タクソノミースラッグ}%";
		$permastruct = "{投稿タイプスラッグ}/{$rewrite_tag}";
		add_rewrite_tag($rewrite_tag, '([^/]+)', '{タクソノミースラッグ}=');
		add_permastruct('{タクソノミースラッグ}', $permastruct, false);
		add_rewrite_rule('{投稿タイプスラッグ}/([^/]+)/?$', 'index.php?post_type={投稿タイプスラッグ}&{タクソノミースラッグ}=$matches[1]', 'top');
	}
	return $args;
}
add_action('registered_taxonomy', 'my_taxes_permalinks', 10, 3);
```

特に語ることはない。投稿タイプの時と同じ要領でリライトタグ、パーマストラクト、必要なリライトルールを設定する。
アーカイブのリライトルールを作っておけば2ページ目以降のリライトルールは不要。


## 4. 出力されるリンクを設定する

ここまでの記述でリライトルール自体は設定完了しているので、パーマリンク設定で『変更を保存』した後にURLを直打ちすれば目的のページに飛べるようになっているはず。
しかし`get_permalink()`などで取得するURLではリライトタグが変換されず`%post_id%`などがそのまま出てくるので、変換処理を追加する。

```php:function.php
// 投稿シングルページのパーマリンク
function my_cpt_link( $post_link, $post ){
	if($post->post_type === '{投稿タイプスラッグ}') {
		// 投稿に登録されているタームを取得
		if(function_exists('YoastSEO')){
			// ※筆者はいつもYoast SEOプラグインを使用するので、その場合はプライマリタームを取得
			//$taxonomy = new WPSEO_Primary_Term('{タクソノミースラッグ}', $post->ID);
			//$primary_term = $taxonomy->get_primary_term();
			//$term = get_term($primary_term);
		｝else {
			// 投稿に登録されているタームの1つ目を取得
			$terms = get_the_terms($post->ID, '{タクソノミースラッグ}');
			$term = $terms[0];
		}
		
		// タームとpostID（もしくはスラッグ）のリライトタグを置き換える
		$post_link = str_replace('%{タクソノミースラッグ}%', $term->slug, $post_link);			// タームを置き換え
		$post_link = str_replace('%{投稿タイプスラッグ}_id%', $post->ID, $post_link);				// 投稿IDを置き換え
		//$post_link = str_replace('%{投稿タイプスラッグ}name%', $post->post_name, $post_link);	// 投稿スラッグの場合はこちら
	}
	return $post_link;
}
add_filter('post_type_link', 'my_cpt_link' , 10, 2);
add_filter('post_link', 'my_cpt_link' , 10, 2);

// タクソノミーアーカイブのパーマリンク
function my_tax_link($termlink, $term, $taxonomy ){
	if($taxonomy === '{タクソノミースラッグ}') {
		$termlink = str_replace('%{タクソノミースラッグ}%', $term->slug, $termlink);
	}
	return $termlink;
}
add_filter('term_link', 'my_tax_link' , 10, 3);
```


## 5. エラー回避

これでOKなのだが、投稿にタクソノミーが何も設定されていない場合、パーマリンクが正常に発行されない。
これは『未分類』というタームを最初に作っておき、投稿の保存・更新時にタクソノミーの登録が無かった場合、そのタクソノミーを自動的に登録するようにすれば回避できる。

`{未分類ターム名}`、`{未分類タームスラッグ}`はそれぞれ適宜変更する。

```php:function.php
// 『未分類』タームの生成
// 『3. カスタムタクソノミーアーカイブのパーマリンクを設定する』の時と同じアクションフックなので、
// そこに追記しても構わない
function uncategorized_tax_registration($taxonomy, $object_type, $args) {
	if ($taxonomy === '{タクソノミースラッグ}') {
		wp_insert_term( '{未分類ターム名}', $taxonomy, array('slug' => '{未分類タームスラッグ}' ) );
	}
	return $args;
}
add_action('registered_taxonomy', 'uncategorized_tax_registration', 10, 3);

// 投稿の保存・更新時にタクソノミー登録がない場合、『未分類』タームを登録させる
function add_uncategorized_tax_automatically($post_ID) {
	$current_term = wp_get_object_terms($post_ID, '{タクソノミースラッグ}');
	if (count($current_term) === 0) {		//ターム未登録時
		$term = '{未分類タームスラッグ}';
		wp_set_object_terms($post_ID, $term, '{タクソノミースラッグ}');
	}
}
add_action('publish_{投稿タイプスラッグ}', 'add_uncategorized_tax_automatically');
```


# 終わり

最後に　管理画面のパーマリンク設定→変更を保存　で、リライトルールの再構成を忘れずに。
『Custom Post Type Permalinks』などのプラグインを使えば近いことはできるが、タクソノミーアーカイブのパーマリンクが`{カスタム投稿スラッグ}/{タクソノミースラッグ}/{タームスラッグ}/`で固定だったり、2ページ目以降のURLが固定だったりする。
できるだけ自分で記述すれば、かゆいところに手が届きやすい。