WordPress の抜粋表示は日本語の場合めちゃくちゃになる。内部で単語数をカウントするため、半角スペースを使わない日本語の単語数を認識できない。デフォルトでは55ワードとなっているが、日本語が混ざると正常に動作しない。一方で、記事一覧では抜粋表示させたい。でも、記事を書く時に択一、<!--more-->
とか Excerpt フィールドに直接文字を入力して、となると、面倒この上ない。ということで、テーマで自動処理してしまおう。
抜粋がうまく機能していない
最初の何も手を加えていない状態だと、アーカイブの表示はこんな感じになっている。とても長い。<!--more-->
や Excerpt フィールドに入力していない場合、記事コンテンツがそのまま表示される。
ホームやアーカイブでは抜粋表示に
まず、テンプレートを編集して単一記事以外の場合は抜粋表示にするようにする。the_content
フィルターを使うかとも考えたけど、重複する無駄な処理が発生することになるので、サイト パフォーマンスの観点から 、少し複雑だけどテンプレートファイルを編集する方が良い。
Twenty Seventeen の場合
Twenty Seventeen の場合、次のラインが記事のタイトル、メタ、内容を表示する。index.php
も archive.php
も single.php
も search.php
にも全く同じラインが有る。
1 |
get_template_part( 'template-parts/post/content', get_post_format() ); |
で、この関数の中身を追っていくと、/template-parts/post/content.php
の呼び出しに行き着く。つまり、content.php
を編集すれば良い。子テーマに同様のパスと名称のファイルを作成し、それを編集する。
変更内容
/template-parts/post/content.php
の次を
1 2 3 4 5 6 7 |
the_content( sprintf( /* translators: %s: Post title. */ __( 'Continue reading<span class="screen-reader-text"> "%s"</span>', 'twentyseventeen' ), get_the_title() ) ); |
次のように変えてあげる。
1 2 3 4 5 6 7 8 9 10 11 |
if ( is_singular() ) { the_content( sprintf( /* translators: %s: Post title. */ __( 'Continue reading<span class="screen-reader-text"> "%s"</span>', 'twentyseventeen' ), get_the_title() ) ); } else { the_excerpt(); } |
これで、単一記事以外は抜粋表示の呼び出しになる。この状態で、アーカイブの表示がどのようになるか確認。次の画像のようになり、やはり長い。
文字数ではなく英単語数がカウントされる
WordPress のそのままの仕様だと、抜粋表示には内部で wp_trim_words()
という関数が呼び出され、そのコードを見ればわかるが、文字数ではなく英語の単語数をカウントしている。例えば、Word は4文字で1ワード。the_excerpt()
が呼ばれると、デフォルトでは55ワードで抜粋される仕様になっているが、日本語の中に混じった英語の中から55個目で切るとなると大変な長さになる。関数のコードを見てみよう。
WordPress コア関数: wp_trim_words()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
function wp_trim_words( $text, $num_words = 55, $more = null ) { if ( null === $more ) { $more = __( '…' ); } $original_text = $text; $text = wp_strip_all_tags( $text ); $num_words = (int) $num_words; /* * translators: If your word count is based on single characters (e.g. East Asian characters), * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'. * Do not translate into your own language. */ if ( strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ) ) { $text = trim( preg_replace( "/[\n\r\t ]+/", ' ', $text ), ' ' ); preg_match_all( '/./u', $text, $words_array ); $words_array = array_slice( $words_array[0], 0, $num_words + 1 ); $sep = ''; } else { $words_array = preg_split( "/[\n\r\t ]+/", $text, $num_words + 1, PREG_SPLIT_NO_EMPTY ); $sep = ' '; } if ( count( $words_array ) > $num_words ) { array_pop( $words_array ); $text = implode( $sep, $words_array ); $text = $text . $more; } else { $text = implode( $sep, $words_array ); } /** * Filters the text content after words have been trimmed. * * @since 3.3.0 * * @param string $text The trimmed text. * @param int $num_words The number of words to trim the text to. Default 55. * @param string $more An optional string to append to the end of the trimmed text, e.g. …. * @param string $original_text The text before it was trimmed. */ return apply_filters( 'wp_trim_words', $text, $num_words, $more, $original_text ); } |
$word_array
の中身を調べると、例えば次のような文章の場合、
このサイトは WordPress のデフォルトテーマの一つである、Twenty Seventeen を使っている。デフォルトテーマの問題点は、サンプルとして基本機能のショーケース的役割がある反面、味気無さがある。最近の記事のウィジェットもその一つだ。
デフォルトで搭載されている最近の記事のウィジェットは、最新記事幾つかをリストしてくれるが、アイキャッチ画像を表示してくれない。
これをサムネイルを表示してくれるプラグインがあった。
Recent Posts Widget With Thumbnails プラグイン
Recent Posts Widget With Thumbnails。 Dashboard -> Plugins -> Add New から Recent Posts Widget With Thumbnails と検索フォームに入力して、Install からの Activate。
使い方は簡単で、Dashboard -> Appearance -> Widgets から、Recent Posts Widget With Thumbnails を追加するだけだ。
すると、こんな感じになる。
各項目の上下ボーダーが気になるから取り除きたい。
カスタム CSS の追加
ブラウザの Inspector Element で要素を調べたら、.rpwwt-widget ul li クラスのようだ。ということで、Dashboard -> Appearance -> Customize -> Additional CSS で以下を追加。
すると、こんな感じになった。
なかなかいい感じだ。
以下の配列として扱われる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
Array ( [0] => (string, length: 6) このサイトは [1] => (string, length: 9) WordPress [2] => (string, length: 22) のデフォルトテーマの一つである、Twenty [3] => (string, length: 9) Seventeen [4] => (string, length: 75) を使っている。デフォルトテーマの問題点は、サンプルとして基本機能のショーケース的役割がある反面、味気無さがある。最近の記事のウィジェットもその一つだ。 [5] => (string, length: 63) デフォルトで搭載されている最近の記事のウィジェットは、最新記事幾つかをリストしてくれるが、アイキャッチ画像を表示してくれない。 [6] => (string, length: 26) これをサムネイルを表示してくれるプラグインがあった。 [7] => (string, length: 6) Recent [8] => (string, length: 5) Posts [9] => (string, length: 6) Widget [10] => (string, length: 4) With [11] => (string, length: 10) Thumbnails [12] => (string, length: 5) プラグイン [13] => (string, length: 6) Recent [14] => (string, length: 5) Posts [15] => (string, length: 6) Widget [16] => (string, length: 4) With [17] => (string, length: 11) Thumbnails。 [18] => (string, length: 9) Dashboard [19] => (string, length: 5) -> [20] => (string, length: 7) Plugins [21] => (string, length: 5) -> [22] => (string, length: 3) Add [23] => (string, length: 3) New [24] => (string, length: 2) から [25] => (string, length: 6) Recent [26] => (string, length: 5) Posts [27] => (string, length: 6) Widget [28] => (string, length: 4) With [29] => (string, length: 10) Thumbnails [30] => (string, length: 20) と検索フォームに入力して、Install [31] => (string, length: 3) からの [32] => (string, length: 9) Activate。 [33] => (string, length: 17) 使い方は簡単で、Dashboard [34] => (string, length: 5) -> [35] => (string, length: 10) Appearance [36] => (string, length: 5) -> [37] => (string, length: 7) Widgets [38] => (string, length: 9) から、Recent [39] => (string, length: 5) Posts [40] => (string, length: 6) Widget [41] => (string, length: 4) With [42] => (string, length: 10) Thumbnails [43] => (string, length: 9) を追加するだけだ。 [44] => (string, length: 13) すると、こんな感じになる。 [45] => (string, length: 24) 各項目の上下ボーダーが気になるから取り除きたい。 [46] => (string, length: 4) カスタム [47] => (string, length: 3) CSS [48] => (string, length: 3) の追加 [49] => (string, length: 5) ブラウザの [50] => (string, length: 9) Inspector [51] => (string, length: 7) Element [52] => (string, length: 22) で要素を調べたら、.rpwwt-widget [53] => (string, length: 2) ul [54] => (string, length: 2) li [55] => (string, length: 112) クラスのようだ。ということで、Dashboard -> Appearance -> Customize -> Additional CSS で以下を追加。 すると、こんな感じになった。 なかなかいい感じだ。 ) |
これが55要素までで切られるため、日本語の場合はかなり長くなってしまう。
WP Multibyte Patch を使ってみるも
プラグインを有効にするだけではきちんと抜粋してくれない
WP Multibyte Patch (v2.8.3) というプラグインを導入し、次を functions.php
に入れてみたが、多少短くなったものの、長さにばらつきが出る。この状態ではまだ英単語数で切っている。
1 2 3 4 |
function getExcerptMBLength( $iLength ) { return 20; } add_filter('excerpt_mblength', 'getExcerptMBLength'); |
しかし、このプラグインの中身をみていくと、
wp-multibyte-patch.php
1 2 |
if ( method_exists( $this, 'wp_trim_words' ) && false !== $this->conf['patch_wp_trim_words'] ) add_filter( 'wp_trim_words', array( $this, 'wp_trim_words' ), 99, 4 ); |
/ext/ja/class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public function wp_trim_words( $text = '', $num_words = 110, $more = '', $original_text = '' ) { if ( 0 !== strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) ) return $text; // If the caller is wp_dashboard_recent_drafts() if ( false !== $this->conf['patch_dashboard_recent_drafts'] && ( 40 === $num_words || 10 === $num_words ) && is_admin() && strpos( wp_debug_backtrace_summary(), 'wp_dashboard_recent_drafts' ) ) $num_words = $this->conf['dashboard_recent_drafts_mblength']; $text = $original_text; $text = wp_strip_all_tags( $text ); $text = trim( preg_replace( "/[\n\r\t ]+/", ' ', $text ), ' ' ); $num_words = (int) $num_words; if ( mb_strlen( $text, $this->blog_encoding ) > $num_words ) $text = mb_substr( $text, 0, $num_words, $this->blog_encoding ) . $more; return $text; } |
とあり、mb_substr()
を使っているので、文字数で切っているはずなのだがうまく言っていない様子。
/wp-content/wpmp-config.php の配置
wpmp-config-sample-ja.php
を wpmp-config.php
にリネームし、/wp-content/
下に配置すると、
うまくいった。プラグインサイトの説明では、次のように説明があり、
デフォルトで標準的な設定値が割り当てられるようになっています。これらの設定値を変更する必要がある場合は、
https://eastcoder.com/code/wp-multibyte-patch/wpmp-config-sample-ja.php
をwpmp-config.php
に変名し内容を編集してから下記のように配置してください。(WP_CONTENT_DIR
を変更している場合はその中に置いてください。)
「設定値を変更する必要が場合は」とあるものの、どうも、その必要がない場合でも置いたほうが良いみたい。 wp-multibyte-patch.php に次のようなラインがあり、設定ファイルの有無で作られるオブジェクトのクラスが違う。その為、設定ファイルが無い場合は文字数でカットする機能が働かない設計になっている模様。
1 2 3 4 5 6 7 8 9 10 11 12 |
if ( defined( 'WP_PLUGIN_URL' ) ) { if ( file_exists( dirname( __FILE__ ) . '/ext/' . get_locale() . '/class.php' ) ) { require_once dirname( __FILE__ ) . '/ext/' . get_locale() . '/class.php'; $GLOBALS['wpmp'] = new multibyte_patch_ext(); } elseif ( file_exists( dirname( __FILE__ ) . '/ext/default/class.php' ) ) { require_once dirname( __FILE__ ) . '/ext/default/class.php'; $GLOBALS['wpmp'] = new multibyte_patch_ext(); } else $GLOBALS['wpmp'] = new multibyte_patch(); } |
WP Multibyte Patch の問題点
ただこれ、使ってみて、<!--more-->
や excerpt フィールドで指定している場合も強制的に文字列がカットされる。できれば、それらの設定を尊重したい。
<!–more–>
もう少し詰める
ということで、一旦、 wpmp-config.php
は外して独自のコードで抜粋処理を施す。WP Multibyte Patch 自体は、他の検索語句だとか XML だとかでの補正機能を期待して有効にしておく。で、そうすると、excerpt_length
フィルターフックで返ってくる値が 110
となったままになる。プラグインを外すと、デフォルトの 55
。この状態だと次のような表示になる。
これはつまり、文字数ではなく英単語数、110単語目で切ってね、ということになるので、プラグインを入れる前よりも長い表示になってしまう。
しかし、コア関数 wp_trim_words()
の次の箇所を見ると文字数でも切れる機能が用意されていることがわかる。
1 2 3 4 5 6 |
if ( strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ) ) { $text = trim( preg_replace( "/[\n\r\t ]+/", ' ', $text ), ' ' ); preg_match_all( '/./u', $text, $words_array ); $words_array = array_slice( $words_array[0], 0, $num_words + 1 ); $sep = ''; } |
これに関しては、gettext_with_context
と excerpt_length
フィルターで対応できる。 WP Multibyte Patch も使うフックで、excerpt_length
に関してはプライオリティを 100
などと低くしてやる必要がある。
で、それによって、まずまずの結果は得られるのだけど、問題があって、
- Excerpt フィールドに直接文字列を入れた場合は Continue reading リンクが表れない
- 抜粋適用文字数を大きく設定した時、
<!--more-->
の箇所がそれ以内だと Continue reading リンクが表れない - 抜粋適用文字数を小さく設定した時 、
<!--more-->
の箇所以前の箇所で切られ、設定位置が尊重されない
3つ目の問題をカバーしようとすると処理が重たくなるので、上の2つをカバーするところまでやる。
PHP コード
これを functions.php
に放り込む。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
/** * Handles excerpts for multi-byte characters. * @version 1.0.2 */ class TwentySeventeenTechnotes_ExcerptHandling { public $iMaxCharacterLength = 110; public $sReadMoreLabel = null; // supports an empty string '' public $sBlogEncoding = 'UTF-8'; private $___sReadMoreCaptured = ''; private $___bLabelSet = false; private $___bIsAdmin = false; private $___bMBSupport = true; public function __construct( $iMaxCharacterLength=110, $sReadMoreLabel=null ) { $this->iMaxCharacterLength = ( integer ) $iMaxCharacterLength; $this->___bLabelSet = isset( $sReadMoreLabel ); $this->sReadMoreLabel = $sReadMoreLabel; add_action( 'init', array( $this, 'replyToSetUp' ) ); add_filter( 'wp_trim_excerpt', array( $this, 'replyToCheckReadMoreTruncation' ), 10, 2 ); add_filter( 'excerpt_more', array( $this, 'replyToCaptureMoreText' ), PHP_INT_MAX ); add_filter( 'excerpt_length', array( $this, 'replyToGetExcerptLength' ), 100 ); add_filter( 'gettext_with_context', array( $this, 'replyToForceCharacterCount' ), 10, 3 ); } public function replyToSetUp() { $this->sReadMoreLabel = $this->___bLabelSet ? $this->sReadMoreLabel : __( 'Continue reading %s' ); $this->___bIsAdmin = is_admin(); $this->sBlogEncoding = get_option( 'blog_charset', 'UTF-8' ); if ( preg_match( '/^utf-?8$/i', $this->sBlogEncoding ) || empty( $this->sBlogEncoding ) ) { $this->sBlogEncoding = 'UTF-8'; } $this->___bMBSupport = function_exists( 'mb_strlen' ); } public function replyToGetExcerptLength( $iLength ) { return $this->iMaxCharacterLength; } public function replyToForceCharacterCount( $sTranslations='', $sText='', $sContext= '' ) { if ( 'word count: words or characters?' == $sContext && 'words' == $sText ) { return 'characters'; } if ( 'Word count type. Do not translate!' == $sContext && 'words' == $sText ) { return 'characters_including_spaces'; } return $sTranslations; } public function replyToCaptureMoreText( $sReadMore ) { // If the user specifies the label, use it if ( $this->___bLabelSet ) { $this->___sReadMoreCaptured = $this->___getReadMoreLink( $sReadMore ); return $this->___sReadMoreCaptured; } // Otherwise, use the one that the system gives $this->___sReadMoreCaptured = $sReadMore; return $sReadMore; } /** * @see twentyseventeen_excerpt_more() * @param $sReadMore * * @return string */ private function ___getReadMoreLink( $sReadMore ) { if ( is_admin() || '' === $sReadMore ) { return $sReadMore; } $_iPostID = get_the_ID(); $_sReadMore = sprintf( '<p class="link-more"><a href="%1$s" class="more-link">%2$s</a></p>', esc_url( get_permalink( $_iPostID ) ), /* translators: %s: Post title. */ sprintf( $this->sReadMoreLabel . '<span class="screen-reader-text"> "%s"</span>', get_the_title( $_iPostID ) ) ); return ' … ' . $_sReadMore; } /** * Checks whether the post is truncated with the `<!--more-->` notation. * When `the_except()` is called, the read more "Continue reading..." link is not added for the cases of `<!--more-->`. * So if it exists, add the read more text that the user specifies in the constructor parameter. * * @param $sExcerpt * @param $sPostExcerpt * @return string */ public function replyToCheckReadMoreTruncation( $sExcerpt, $sPostExcerpt ) { if ( ! $sExcerpt ) { return $sExcerpt; } // If the $post->except is set, it should be used. `$sExcerpt` contains it without the 'Continue reading' link. // At the moment, it won't do anything in the admin area if ( $sPostExcerpt ) { return $this->___bIsAdmin ? $sExcerpt : $sExcerpt . $this->___getReadMoreLink( $this->sReadMoreLabel ); } // Check if the 'Continue reading' link is inserted if ( false !== strpos( $sExcerpt, $this->___sReadMoreCaptured ) ) { return $sExcerpt; } // At this point, the excerpt does not have the 'Continue reading' link. // Case 1: the content is too short (without `<!--more-->`) // Case 2: `<!--more-->` exists in the post but the 'Continue reading' link is not added // 2-a: the maximum character length is large enough to cover the entire post content // 2-b: not sure $_iExcerptLength = call_user_func_array( $this->___bMBSupport ? 'mb_strlen' : 'strlen', $this->___bMBSupport ? array( $sExcerpt, $this->sBlogEncoding ) : array( $sExcerpt ) ); if ( $_iExcerptLength <= $this->iMaxCharacterLength ) { $_oPost = get_post(); return $this->___hasCommentNotation( $_oPost ) ? $sExcerpt . $this->___sReadMoreCaptured // Case 2-a : $sExcerpt; // Case 1 } // Case 2-b return $sExcerpt . $this->___sReadMoreCaptured; } /** * @see get_the_content() * @return boolean */ private function ___hasCommentNotation( $oPost ) { $_aElements = generate_postdata( $oPost ); $_sPostContent = isset( $_aElements[ 'pages' ][ 0 ] ) ? $_aElements[ 'pages' ][ 0 ] : ''; if ( ! $_sPostContent ) { return false; } return ( boolean ) preg_match( '/<!--more(.*?)?-->/', $_sPostContent, $_aMatches ); } } new TwentySeventeenTechnotes_ExcerptHandling( 110, '続きを読む' ); |
使い方
使い方は、最後のラインにあるように、クラスをインスタンス化するだけ。その時に次の引数を渡してあげる。
- ( integer ) 抜粋文字最大数。デフォルト:
110
- ( string ) 続きを読むのリンクのラベル デフォルト:
Continue reading
これで、記事中に <!--more-->
が入っていたり excerpt フィールドで直接文字列を指定している場合はそれらの内容が反映される。ただし、<!--more-->
の場合は、抜粋文字数が <!--more-->
以前に到達してしまうと、そこで切れてしまうという制限あり。
記事コンテンツ取得に、generate_postdata()
を使ったが Query Monitor で調べても DB クエリ数は増えなかったのでパフォーマンスに大した影響はないはず。
結果
うまくいくとこんな感じになる。
RSS フィードの抜粋表示
あと、RSS フィードにも抜粋を適用するには、Dashboard -> Settings -> Reading -> For each post in a feed, include を Summary にする必要がある。
Firefox の フィードのアドオンで開いたらきちんと抜粋表示されている。
ということで、めでたしめでたし!色んなケースできちんとテストしてるわけじゃないので、想定外の動作があるかも。
おまけ
ちなみに、Twenty Seventeen の CSS では “続きを読む” の文字が改行されちゃうので、以下の CSS を追加した。
1 2 3 |
.entry-content .more-link:before { display: none; } |
余談
今回はかなり手こずった。コードのデザインがやはり英語圏の人のものなので、ホワイトスペースで単語を区切らない言語を想定して書かれていない。プログラミング全般にそうだけど、最初の設計で90%くらいその後の効率って決まってしまう。特にオープンソースでプロジェクトが大きくなってしまったものは簡単に変更できないので大変。最初の設計がいかに大事か再認識させられた。