WordPressの表示フローは概ね以下のようになる。

パーマリンク生成

  1. 設定 > パーマリンクからリライトルールを設定
  2. WordPressはパーマリンクを作成
    • 新規投稿時や記事/アーカイブ表示時にget_permalink()関数で、1で設定したリライトルールに沿ってprettyパーマリンクを生成

投稿表示

  1. WordPressは↑のprettyパーマリンクを、リライトルールに応じてuglyパーマリンクに変換
  2. uglyパーマリンクのquery_varsに応じて対象を抽出して表示する

Ref

概要

WordPressのパーマリンクは大きく2つに分けられる。

  • デフォルト:Uglyパーマリンク(例: https://example.com/index.php?p=12345&cat=123)
  • mod_rewrite:Prettyパーマリンク(例: https://example.com/sample/12345)

本記事ではPrettyパーマリンクがUglyパーマリンクに変換されるフローを記載する。

※ Prettyパーマリンクの構造は、基本的に設定 > パーマリンクから指定する。 ※ 個々の投稿のパーマリンク文字列自体はDBに保存されていない。permalink_structureの設定と投稿データ(post_nameなど)から、リクエストのたびに動的に組み立てられる。

リライトの流れ

  1. 設定 > パーマリンクで設定を変更
  2. 上記設定によってwp_options.option_nameのrewrite_rules(WP_Rewrite::rules)が決まる

アクセス

  1. Prettyパーマリンクでアクセス(例: https://example.com/category-sample/1)
  2. PrettyパーマリンクをWP_Rewrite::rulesをもとにUglyパーマリンクに変換(例: https://example.com?p=1&category-name=category-sample&page=)
  3. Uglyパーマリンクのパブリッククエリをもとに、投稿抽出クエリの条件であるWP_Query::query_varsが設定され、投稿抽出SQLが発行される

サイト表示時、HTTPリクエストを受け取って起動したWordPressは、リクエストに含まれるクエリ文字列やURLの一部を「パブリッククエリ変数」として解釈し、それをもとにWordPressクエリを組み立てて実行する(参考: プライム・ストラテジー株式会社『詳解WordPress』p.95付近の解説を要約)。

パブリック変数はwp-includes/class-wp.phpで定義されている。

<?php
/**
 * WordPress environment setup class.
 *
 * @package WordPress
 * @since 2.0.0
 */
class WP {
	/**
	 * Public query variables.
	 *
	 * Long list of public query variables.
	 *
	 * @since 2.0.0
	 * @var string[]
	 */
	public $public_query_vars = array( 'm', 'p', 'posts', 'w', 'cat', 'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence', 'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order', 'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second', 'name', 'category_name', 'tag', 'feed', 'author_name', 'pagename', 'page_id', 'error', 'attachment', 'attachment_id', 'subpost', 'subpost_id', 'preview', 'robots', 'favicon', 'taxonomy', 'term', 'cpage', 'post_type', 'embed' );

リライトルール

パブリッククエリをリライトルールに沿って解釈し、問い合わせ(SQL)を発行して投稿を取得する。

アクセス時のパブリッククエリはWP_Query::query_varsに格納される。

wp-settings.phpの中で、WordPressによるURLの解釈・リライトのために重要な2つのオブジェクト、$wp_queryと$wp_rewriteが初期化される(参考: Rewrite APIその1「Rewriteとパーマリンク」)。

WordPressのリライトルールは、グローバル変数$wp_rewriteに格納されている。

DBテーブル

リライト関連の情報はwp_optionsに格納されている。 以下に代表的なレコードを記載する。

option_name option_value
permalink_structure /%category%/%post_id%
rewrite_rules マッチ規則の正規表現がシリアライズされて保存

リライトルールを追加

参考: WordPress パーマリンク リライトルール

  • add_rewrite_rule
  • add_rewrite_tag
  • pre_post_link(get_permalink()の中でapplyされる)
  • add_permastruct
  • rewrite_rules_arrayフィルター
  • flush_rewrite_rules()
    • リライトルールは、コードでflush_rewrite_rules()を呼び出すか、管理画面のパーマリンク設定画面を開いて保存すると自動で反映される

プラグインでリライトルールを追加した際は、WordPressのデータベースにtransientキャッシュとして登録されているリライトルールを更新(flush_rewrite_rules())する処理を忘れずに加える必要がある(参考: Rewrite APIその1「Rewriteとパーマリンク」)。

備考

$wp_rewrite

WP_Rewrite Object
(
    [permalink_structure] => /%postname%/
    [use_trailing_slashes] => 1
    [author_base] => author
    [page_structure] => %pagename%
    [search_base] => search
    [comments_base] => comments
    [pagination_base] => page
    [comments_pagination_base] => comment-page
    [feed_base] => feed
    [comment_feed_structure] => comments/feed/%feed%
    [feed_structure] => feed/%feed%
    [front] => /
    [root] => 
    [index] => index.php
    [matches] => 
    [rules] => Array
        (
            [^wp-json/?$] => index.php?rest_route=/
            [^wp-json/(.*)?] => index.php?rest_route=/$matches[1]
            [^index.php/wp-json/?$] => index.php?rest_route=/
            [^index.php/wp-json/(.*)?] => index.php?rest_route=/$matches[1]
            [category/(.+?)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?category_name=$matches[1]&feed=$matches[2]
            [category/(.+?)/(feed|rdf|rss|rss2|atom)/?$] => index.php?category_name=$matches[1]&feed=$matches[2]
            [category/(.+?)/embed/?$] => index.php?category_name=$matches[1]&embed=true
            [category/(.+?)/page/?([0-9]{1,})/?$] => index.php?category_name=$matches[1]&paged=$matches[2]
            [category/(.+?)/?$] => index.php?category_name=$matches[1]
            [tag/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?tag=$matches[1]&feed=$matches[2]
            [tag/([^/]+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?tag=$matches[1]&feed=$matches[2]
            [tag/([^/]+)/embed/?$] => index.php?tag=$matches[1]&embed=true
            [tag/([^/]+)/page/?([0-9]{1,})/?$] => index.php?tag=$matches[1]&paged=$matches[2]
            [tag/([^/]+)/?$] => index.php?tag=$matches[1]
            [type/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?post_format=$matches[1]&feed=$matches[2]
            [type/([^/]+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?post_format=$matches[1]&feed=$matches[2]
            [type/([^/]+)/embed/?$] => index.php?post_format=$matches[1]&embed=true
            [type/([^/]+)/page/?([0-9]{1,})/?$] => index.php?post_format=$matches[1]&paged=$matches[2]
            [type/([^/]+)/?$] => index.php?post_format=$matches[1]
            [.*wp-(atom|rdf|rss|rss2|feed|commentsrss2)\.php$] => index.php?feed=old
            [.*wp-app\.php(/.*)?$] => index.php?error=403
            [.*wp-register.php$] => index.php?register=true
            [feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?&feed=$matches[1]
            [(feed|rdf|rss|rss2|atom)/?$] => index.php?&feed=$matches[1]
            [embed/?$] => index.php?&embed=true
            [page/?([0-9]{1,})/?$] => index.php?&paged=$matches[1]
            [comments/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?&feed=$matches[1]&withcomments=1
            [comments/(feed|rdf|rss|rss2|atom)/?$] => index.php?&feed=$matches[1]&withcomments=1
            [comments/embed/?$] => index.php?&embed=true
            [search/(.+)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?s=$matches[1]&feed=$matches[2]
            [search/(.+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?s=$matches[1]&feed=$matches[2]
            [search/(.+)/embed/?$] => index.php?s=$matches[1]&embed=true
            [search/(.+)/page/?([0-9]{1,})/?$] => index.php?s=$matches[1]&paged=$matches[2]
            [search/(.+)/?$] => index.php?s=$matches[1]
            [author/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?author_name=$matches[1]&feed=$matches[2]
            [author/([^/]+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?author_name=$matches[1]&feed=$matches[2]
            [author/([^/]+)/embed/?$] => index.php?author_name=$matches[1]&embed=true
            [author/([^/]+)/page/?([0-9]{1,})/?$] => index.php?author_name=$matches[1]&paged=$matches[2]
            [author/([^/]+)/?$] => index.php?author_name=$matches[1]
            [([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]&feed=$matches[4]
            [([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/(feed|rdf|rss|rss2|atom)/?$] => index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]&feed=$matches[4]
            [([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/embed/?$] => index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]&embed=true
            [([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/page/?([0-9]{1,})/?$] => index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]&paged=$matches[4]
            [([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/?$] => index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]
            [([0-9]{4})/([0-9]{1,2})/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?year=$matches[1]&monthnum=$matches[2]&feed=$matches[3]
            [([0-9]{4})/([0-9]{1,2})/(feed|rdf|rss|rss2|atom)/?$] => index.php?year=$matches[1]&monthnum=$matches[2]&feed=$matches[3]
            [([0-9]{4})/([0-9]{1,2})/embed/?$] => index.php?year=$matches[1]&monthnum=$matches[2]&embed=true
            [([0-9]{4})/([0-9]{1,2})/page/?([0-9]{1,})/?$] => index.php?year=$matches[1]&monthnum=$matches[2]&paged=$matches[3]
            [([0-9]{4})/([0-9]{1,2})/?$] => index.php?year=$matches[1]&monthnum=$matches[2]
            [([0-9]{4})/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?year=$matches[1]&feed=$matches[2]
            [([0-9]{4})/(feed|rdf|rss|rss2|atom)/?$] => index.php?year=$matches[1]&feed=$matches[2]
            [([0-9]{4})/embed/?$] => index.php?year=$matches[1]&embed=true
            [([0-9]{4})/page/?([0-9]{1,})/?$] => index.php?year=$matches[1]&paged=$matches[2]
            [([0-9]{4})/?$] => index.php?year=$matches[1]
            [.?.+?/attachment/([^/]+)/?$] => index.php?attachment=$matches[1]
            [.?.+?/attachment/([^/]+)/trackback/?$] => index.php?attachment=$matches[1]&tb=1
            [.?.+?/attachment/([^/]+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?attachment=$matches[1]&feed=$matches[2]
            [.?.+?/attachment/([^/]+)/comment-page-([0-9]{1,})/?$] => index.php?attachment=$matches[1]&cpage=$matches[2]
            [.?.+?/attachment/([^/]+)/embed/?$] => index.php?attachment=$matches[1]&embed=true
            [(.?.+?)/embed/?$] => index.php?pagename=$matches[1]&embed=true
            [(.?.+?)/trackback/?$] => index.php?pagename=$matches[1]&tb=1
            [(.?.+?)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?pagename=$matches[1]&feed=$matches[2]
            [(.?.+?)/(feed|rdf|rss|rss2|atom)/?$] => index.php?pagename=$matches[1]&feed=$matches[2]
            [(.?.+?)/page/?([0-9]{1,})/?$] => index.php?pagename=$matches[1]&paged=$matches[2]
            [(.?.+?)/comment-page-([0-9]{1,})/?$] => index.php?pagename=$matches[1]&cpage=$matches[2]
            [(.?.+?)(?:/([0-9]+))?/?$] => index.php?pagename=$matches[1]&page=$matches[2]
            [[^/]+/attachment/([^/]+)/?$] => index.php?attachment=$matches[1]
            [[^/]+/attachment/([^/]+)/trackback/?$] => index.php?attachment=$matches[1]&tb=1
            [[^/]+/attachment/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?attachment=$matches[1]&feed=$matches[2]
            [[^/]+/attachment/([^/]+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?attachment=$matches[1]&feed=$matches[2]
            [[^/]+/attachment/([^/]+)/comment-page-([0-9]{1,})/?$] => index.php?attachment=$matches[1]&cpage=$matches[2]
            [[^/]+/attachment/([^/]+)/embed/?$] => index.php?attachment=$matches[1]&embed=true
            [([^/]+)/embed/?$] => index.php?name=$matches[1]&embed=true
            [([^/]+)/trackback/?$] => index.php?name=$matches[1]&tb=1
            [([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?name=$matches[1]&feed=$matches[2]
            [([^/]+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?name=$matches[1]&feed=$matches[2]
            [([^/]+)/page/?([0-9]{1,})/?$] => index.php?name=$matches[1]&paged=$matches[2]
            [([^/]+)/comment-page-([0-9]{1,})/?$] => index.php?name=$matches[1]&cpage=$matches[2]
            [([^/]+)(?:/([0-9]+))?/?$] => index.php?name=$matches[1]&page=$matches[2]
            [[^/]+/([^/]+)/?$] => index.php?attachment=$matches[1]
            [[^/]+/([^/]+)/trackback/?$] => index.php?attachment=$matches[1]&tb=1
            [[^/]+/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?attachment=$matches[1]&feed=$matches[2]
            [[^/]+/([^/]+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?attachment=$matches[1]&feed=$matches[2]
            [[^/]+/([^/]+)/comment-page-([0-9]{1,})/?$] => index.php?attachment=$matches[1]&cpage=$matches[2]
            [[^/]+/([^/]+)/embed/?$] => index.php?attachment=$matches[1]&embed=true
        )

    [extra_rules] => Array
        (
        )

    [extra_rules_top] => Array
        (
            [^wp-json/?$] => index.php?rest_route=/
            [^wp-json/(.*)?] => index.php?rest_route=/$matches[1]
            [^index.php/wp-json/?$] => index.php?rest_route=/
            [^index.php/wp-json/(.*)?] => index.php?rest_route=/$matches[1]
        )

    [non_wp_rules] => Array
        (
        )

    [extra_permastructs] => Array
        (
            [category] => Array
                (
                    [with_front] => 1
                    [ep_mask] => 512
                    [paged] => 1
                    [feed] => 1
                    [forcomments] => 
                    [walk_dirs] => 1
                    [endpoints] => 1
                    [struct] => /category/%category%
                )

            [post_tag] => Array
                (
                    [with_front] => 1
                    [ep_mask] => 1024
                    [paged] => 1
                    [feed] => 1
                    [forcomments] => 
                    [walk_dirs] => 1
                    [endpoints] => 1
                    [struct] => /tag/%post_tag%
                )

            [post_format] => Array
                (
                    [with_front] => 1
                    [ep_mask] => 0
                    [paged] => 1
                    [feed] => 1
                    [forcomments] => 
                    [walk_dirs] => 1
                    [endpoints] => 1
                    [struct] => /type/%post_format%
                )

        )

    [endpoints] => Array
        (
        )

    [use_verbose_rules] => 
    [use_verbose_page_rules] => 1
    [rewritecode] => Array
        (
            [0] => %year%
            [1] => %monthnum%
            [2] => %day%
            [3] => %hour%
            [4] => %minute%
            [5] => %second%
            [6] => %postname%
            [7] => %post_id%
            [8] => %author%
            [9] => %pagename%
            [10] => %search%
            [11] => %category%
            [12] => %post_tag%
            [13] => %post_format%
        )

    [rewritereplace] => Array
        (
            [0] => ([0-9]{4})
            [1] => ([0-9]{1,2})
            [2] => ([0-9]{1,2})
            [3] => ([0-9]{1,2})
            [4] => ([0-9]{1,2})
            [5] => ([0-9]{1,2})
            [6] => ([^/]+)
            [7] => ([0-9]+)
            [8] => ([^/]+)
            [9] => ([^/]+?)
            [10] => (.+)
            [11] => (.+?)
            [12] => ([^/]+)
            [13] => ([^/]+)
        )

    [queryreplace] => Array
        (
            [0] => year=
            [1] => monthnum=
            [2] => day=
            [3] => hour=
            [4] => minute=
            [5] => second=
            [6] => name=
            [7] => p=
            [8] => author_name=
            [9] => pagename=
            [10] => s=
            [11] => category_name=
            [12] => tag=
            [13] => post_format=
        )

    [feeds] => Array
        (
            [0] => feed
            [1] => rdf
            [2] => rss
            [3] => rss2
            [4] => atom
        )

)

参考: WordPress パーマリンク リライトルール

// カスタム投稿タイプmanualのrewriteルールを追加
add_filter( 'rewrite_rules_array', function ( $rules ) {
    $new_rules = array(
        'manual/([0-9]+)/?$' => 'index.php?post_type=manual&p=$matches[1]',
    );

    return $new_rules + $rules;
});

add_filter( 'pre_post_link', function ( $permalink, $post, $leavename ) {
    // 投稿スラッグ(post_name)が数値のみの場合はカテゴリー付きの構造に切り替える例
    if ( is_numeric( $post->post_name ) ) {
        return '/%category%/%postname%';
    }
    // $permalink をカスタマイズ
    return $permalink;
}, 10, 3 );

add_filter( 'rewrite_rules_array', function ( $rules ) {
    $new_rules = array(
        'info/.+/([0-9a-zA-Z-_]+)$' => 'index.php?name=$matches[1]',
    );

    return $rules + $new_rules;
});

※ 元のコードには2点誤りがあった。

  • pre_post_linkフィルターの条件$post->ID !== $post->post_nameは、intとstringを厳密比較(!==)しているため常に真になり意味を成さない。スラッグが数値のみかどうかを判定するis_numeric( $post->post_name )に修正した。
  • 3つ目のrewrite_rules_arrayの例で、'index.php?name=matches[1]'のmatches[1]の前に$が抜けており、実際には置換されない不具合があったため$matches[1]に修正した。