WordPressが自動生成するスラッグには%が含まれない

 WordPressのプラグインActivityPubが7.6.0に更新されてから、マストドンで表示される日本語のハッシュタグがエンコード文字列から%が抜けたハッシュタグになるバグが発生した。

"published":"2025-11-11T22:36:45Z","tag":[{"type":"Hashtag","href":"https://ishii00141.stars.ne.jp/tag/activitypub/","name":"#activitypub"},{"type":"Hashtag","href":"https://ishii00141.stars.ne.jp/tag/fediverse/","name":"#fediverse"},{"type":"Hashtag","href":"https://ishii00141.stars.ne.jp/tag/wordpress/","name":"#wordpress"},{"type":"Hashtag","href":"https://ishii00141.stars.ne.jp/tag/%e3%83%97%e3%83%a9%e3%82%b0%e3%82%a4%e3%83%b3/","name":"#E38397E383A9E382B0E382A4E383B3"},{"type":"Hashtag","href":"https://ishii00141.stars.ne.jp/tag/%e4%bb%95%e6%a7%98/","name":"#E4Bb95E6A798"}],

 Geminiに相談したら、その原因は、どうやら、/activitypub/includes/transformer/class-post.php のコードの次の部分らしい。

/**
* Returns a list of Tags, used in the Post.
*
* This includes Hash-Tags and Mentions.
*
* @return array The list of Tags.
*/

protected function get_tag() {
$tags = parent::get_tag();

$post_tags = \get_the_tags( $this->item->ID );
if ( $post_tags ) {
	 foreach ( $post_tags as $post_tag ) {
		// Tag can be empty.
		if ( ! $post_tag ) {
				continue;
		}

		$tags[] = array(
				'type' => 'Hashtag',
				'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
				'name' => esc_hashtag( $post_tag->slug ),
		);
	}
}

return \array_unique( $tags, SORT_REGULAR );

} 

 このコードの「'name' => esc_hashtag( $post_tag->slug )」の部分が怪しいらしい。これを「'name' => esc_hashtag( $post_tag->name )」に修正するべきらしい。

 Geminiによると「URLとスラッグの関係」は次の通りらしい。

💾 URLとスラッグの関係

WordPress のタグの URL に日本語が直接使われているように見えても、slug は内部データとして存在しています。

1. 内部データとしての slug

$post_tag->slug は、そのタグを一意に識別するためのデータベース上の値であり、URL 形式の決定に重要な役割を果たします。WordPress のターム(タグやカテゴリー)は、構造上、必ず term_idnameslug のプロパティを持っています。

2. URLエンコードとスラッグ

ユーザーが見るブラウザのアドレスバーには、日本語(プラグイン)がそのまま表示されることがあります。しかし、これはブラウザが表示のためにデコードしているだけで、実際にはサーバーへは URL エンコードされた形式で送信されています

URL の送信形式

表示サーバーへの送信形式内部の slug
~/tag/プラグイン~/tag/%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3e38397e383a9... (あなたのケース)

3. スラッグが使われる場所

たとえ URL に日本語名が使われている場合でも、WordPress がそのタグをデータベースから読み出す際、URL パス(プラグインの部分)を解析してスラッグIDを特定します。スラッグは、クエリや内部の処理で使われる、必須の識別子です。

したがって、ActivityPub プラグインが $post_tags を読み込んだ時点で、そのオブジェクトにはあなたのデータベースに格納されている $post_tag->slug の値(今回の場合は % が抜けたバイト列)が含まれています。

 さて、この記事のタイトルには"%"が含まれているのだが、記事タイトルを基に自動で生成されたスラッグを見ると次のようになっていて、"%"が抜けている。

wordpressが自動生成するスラッグにはが含まれない
スラッグ:wordpressが自動生成するスラッグにはが含まれない

 一つ勉強になった。

未分類
管理人のマストドンアカウントへのリンクなど

コメント

  1. ishii ishii より:

    #ActivityPub
    この記事に書いたことが誤りの可能性がある。
    この記事の「'name' => esc_hashtag( $post_tag->slug )」が怪しく、解決策として「'name' => esc_hashtag( $post_tag->name )」を使った方が良さそうなことは同じだが、これを書く前提として「$post_tag->slug」の段階で"%"が削除されていると推測していた。実際、WordPressのスラッグ自動生成では"%"が削除される。
    しかし、ハッシュタグの場合、ハッシュタグが格納されている wp_terms テーブルの該当するハッシュタグの slug が使われているらしく、その slug では%が残っていることがテーブルを見て確認できた。

    そのためのテストを行った。
    https://ishii00141.stars.ne.jp/20251114-2029-3973/
    https://ishii00141.stars.ne.jp/20251114-2034-3977/

    このテストで #テスト というハッシュタグを新たに作り、 wp_terms テーブルでは slug に「%e3%83%86%e3%82%b9%e3%83%88」と格納されたことを確認した。そして #Mastodon で確認したところ、"%"が削除されて #E38386E382B9E38388 となっていた。

    その後、wp_terms テーブルの「テスト」の slug を「%e3%82%b9%e3%83%a9%e3%83%83%e3%82%b0」に変えた。これは「スラッグ」のエンコードである。
    そして、新たな記事で #テスト というハッシュタグを付けたところ、マストドンでは「#E382B9E383A9E38383E382B0」となった。すなわち wp_terms テーブルの改竄が反映された。
    これは、JSON生成時に wp_terms テーブルの slug を読み込んでいることを示している。

    では、wp_terms テーブルの slug に"%"が残っているのにJSON生成時に"%"が消えるのはなぜか?
    #Gemini の推測では esc_hashtag 関数が怪しいとのことである。

  2. ishii ishii より:

    #WordPress #slug
    スラッグを作成するコードは、/wp-includes/formatting.php にある。

    /**
     * Sanitizes a string into a slug, which can be used in URLs or HTML attributes.
     *
     * By default, converts accent characters to ASCII characters and further
     * limits the output to alphanumeric characters, underscore (_) and dash (-)
     * through the {@see 'sanitize_title'} filter.
     *
     * If `$title` is empty and `$fallback_title` is set, the latter will be used.
     *
     * @since 1.0.0
     *
     * @param string $title      The string to be sanitized.
     * @param string $fallback_title Optional. A title to use if $title is empty. Default empty.
     * @param string $context    Optional. The operation for which the string is sanitized.
     *                 When set to 'save', the string runs through remove_accents().
     *                 Default 'save'.
     * @return string The sanitized string.
     */
    function sanitize_title( $title, $fallback_title = '', $context = 'save' ) {
      $raw_title = $title;
    
      if ( 'save' === $context ) {
        $title = remove_accents( $title );
      }
    
      /**
       * Filters a sanitized title string.
       *
       * @since 1.2.0
       *
       * @param string $title   Sanitized title.
       * @param string $raw_title The title prior to sanitization.
       * @param string $context   The context for which the title is being sanitized.
       */
      $title = apply_filters( 'sanitize_title', $title, $raw_title, $context );
    
      if ( '' === $title || false === $title ) {
        $title = $fallback_title;
      }
    
      return $title;
    }
    

    ここに $title = remove_accents( $title ); があるので、次のコードがまず実行される。

    function remove_accents( $text, $locale = '' ) {
    (中略)
      return $text;
    }
    

    これで、ü のような文字が u などASCIIの文字に置き換わる。

    さらに、/wp-includes/default-filters.php に
    add_filter( 'sanitize_title', 'sanitize_title_with_dashes', 10, 3 );
    とあるので、
    /wp-includes/formatting.php のsanitize_title_with_dashes()がデフォルトで実行される。

    "wp-includes/formatting.php"
    sanitize_title_with_dashes() – Function | Developer.WordPress.org
    https://developer.wordpress.org/reference/functions/sanitize_title_with_dashes/

    function sanitize_title_with_dashes( $title, $raw_title = '', $context = 'display' ) {
      $title = strip_tags( $title );
      // Preserve escaped octets.
      $title = preg_replace( '|%([a-fA-F0-9][a-fA-F0-9])|', '---$1---', $title );
      // Remove percent signs that are not part of an octet.
      $title = str_replace( '%', '', $title );
      // Restore octets.
      $title = preg_replace( '|---([a-fA-F0-9][a-fA-F0-9])---|', '%$1', $title );
    
      if ( seems_utf8( $title ) ) {
        if ( function_exists( 'mb_strtolower' ) ) {
          $title = mb_strtolower( $title, 'UTF-8' );
        }
        $title = utf8_uri_encode( $title, 200 );
      }
    
      $title = strtolower( $title );
    
      if ( 'save' === $context ) {
        // Convert &nbsp, &ndash, and &mdash to hyphens.
        $title = str_replace( array( '%c2%a0', '%e2%80%93', '%e2%80%94' ), '-', $title );
        // Convert &nbsp, &ndash, and &mdash HTML entities to hyphens.
        $title = str_replace( array( ' ', ' ', '–', '–', '—', '—' ), '-', $title );
        // Convert forward slash to hyphen.
        $title = str_replace( '/', '-', $title );
    
        // Strip these characters entirely.
        $title = str_replace(
          array(
            // Soft hyphens.
            '%c2%ad',
            // &iexcl and &iquest.
            '%c2%a1',
            '%c2%bf',
            // Angle quotes.
            '%c2%ab',
            '%c2%bb',
            '%e2%80%b9',
            '%e2%80%ba',
            // Curly quotes.
            '%e2%80%98',
            '%e2%80%99',
            '%e2%80%9c',
            '%e2%80%9d',
            '%e2%80%9a',
            '%e2%80%9b',
            '%e2%80%9e',
            '%e2%80%9f',
            // Bullet.
            '%e2%80%a2',
            // &copy, &reg, &deg, &hellip, and &trade.
            '%c2%a9',
            '%c2%ae',
            '%c2%b0',
            '%e2%80%a6',
            '%e2%84%a2',
            // Acute accents.
            '%c2%b4',
            '%cb%8a',
            '%cc%81',
            '%cd%81',
            // Grave accent, macron, caron.
            '%cc%80',
            '%cc%84',
            '%cc%8c',
            // Non-visible characters that display without a width.
            '%e2%80%8b', // Zero width space.
            '%e2%80%8c', // Zero width non-joiner.
            '%e2%80%8d', // Zero width joiner.
            '%e2%80%8e', // Left-to-right mark.
            '%e2%80%8f', // Right-to-left mark.
            '%e2%80%aa', // Left-to-right embedding.
            '%e2%80%ab', // Right-to-left embedding.
            '%e2%80%ac', // Pop directional formatting.
            '%e2%80%ad', // Left-to-right override.
            '%e2%80%ae', // Right-to-left override.
            '%ef%bb%bf', // Byte order mark.
            '%ef%bf%bc', // Object replacement character.
          ),
          '',
          $title
        );
    
        // Convert non-visible characters that display with a width to hyphen.
        $title = str_replace(
          array(
            '%e2%80%80', // En quad.
            '%e2%80%81', // Em quad.
            '%e2%80%82', // En space.
            '%e2%80%83', // Em space.
            '%e2%80%84', // Three-per-em space.
            '%e2%80%85', // Four-per-em space.
            '%e2%80%86', // Six-per-em space.
            '%e2%80%87', // Figure space.
            '%e2%80%88', // Punctuation space.
            '%e2%80%89', // Thin space.
            '%e2%80%8a', // Hair space.
            '%e2%80%a8', // Line separator.
            '%e2%80%a9', // Paragraph separator.
            '%e2%80%af', // Narrow no-break space.
          ),
          '-',
          $title
        );
    
        // Convert &times to 'x'.
        $title = str_replace( '%c3%97', 'x', $title );
      }
    
      // Remove HTML entities.
      $title = preg_replace( '/&.+?;/', '', $title );
      $title = str_replace( '.', '-', $title );
    
      $title = preg_replace( '/[^%a-z0-9 _-]/', '', $title );
      $title = preg_replace( '/\s+/', '-', $title );
      $title = preg_replace( '|-+|', '-', $title );
      $title = trim( $title, '-' );
    
      return $title;
    }
    
タイトルとURLをコピーしました