コンテンツへスキップ

メタディスクリプションの文字数制限はなぜあるのか — HTMLエンティティ由来の「見かけ上の文字数超過」の罠

このブログでは、記事を公開するたびにSEO監査スクリプトでタイトル・メタディスクリプション・canonical・hreflangなどを確認している。その監査結果で、英語版の記事にくり返し現れる数字がある。「164 chars」だ。テーマ側では「メタディスクリプションは最大160文字に詰める」と実装しているのに、監査スクリプトは160を超える値を報告してくる。実装が壊れているのか、監査が間違っているのか——結論からいえば、どちらも壊れていない。数えている「文字列」が別物なのである。

今回はこの「見かけ上の文字数超過」を入口に、メタディスクリプションにそもそもなぜ文字数の目安があるのか、そして「文字数」という言葉がどれだけ多くの意味を持ちうるかを整理する。

メタディスクリプションとは何か

メタディスクリプションは、HTMLの <head> 内に書く、ページの要約文である。

<meta name="description" content="このページの内容を1〜2文で説明する要約文。" />

ブラウザの画面には表示されない。主な使われ方は2つで、1つは検索結果のタイトル下に表示される説明文(スニペット)の候補、もう1つはSNSやチャットツールでURLを共有したときのプレビュー文の材料である(多くのサイトは og:description にも同じ文を入れている)。

押さえておきたいのは、次の2点だ。

  • 検索順位を決める要素としては使われていないとGoogleは説明している。メタディスクリプションを工夫しても順位が上がるわけではない
  • そのまま表示される保証もない。検索エンジンは検索語句に合わせて、本文から別の箇所を抜き出してスニペットにすることがよくある

それでも書く意味があるのは、スニペットに採用されたとき、検索結果を見た人が「このページを開くかどうか」を判断する材料になるからだ。順位ではなく、クリックされるかどうかに関わる要素だと捉えるとよい。

「文字数制限」は誰が決めているのか

よく「メタディスクリプションは120文字以内」「160文字以内」といった目安を見かける。ところが、HTMLの仕様にも、Googleのドキュメントにも、メタディスクリプションの長さの上限は定められていない。1000文字書いてもHTMLとしては正しく、検索エンジンにエラー扱いされることもない。

では目安はどこから来ているのか。答えは検索結果画面の表示幅である。スニペットは画面上の限られた領域に収まるよう、長い場合は途中で切られて末尾が「…」になる。この切り詰めは文字数ではなく表示幅(ピクセル)を基準に行われるため、同じ文字数でも、文字の種類や端末によって切れる位置が変わる。

  • 日本語の全角文字は、英字1文字のおよそ2倍の幅を取る。そのため日本語は英語より少ない文字数で切れる
  • スマートフォンの検索結果はPCより表示領域が狭く、さらに短い位置で切れる傾向がある
  • 検索結果の見た目は検索エンジン側の都合で変わるため、「何文字まで表示されるか」は固定値ではない

「日本語は120文字前後、英語は150〜160文字前後」といった数字は、こうした表示幅から逆算された経験的な目安にすぎない。つまり文字数制限の正体は「規則」ではなく、「全部読んでもらうには、このくらいに収めた方がよい」という表示上の都合なのだ。

そこから導かれる書き方のコツは単純で、大事な情報を前半に置くことである。後半が切られても意味が通るように書いておけば、何文字で切られるかに一喜一憂しなくて済む。

このブログのテーマはどう生成しているか

このブログのテーマ wpmm-blog は、記事ごとのメタディスクリプションを wpmm_blog_meta_description() という関数で自動生成している。記事ページでの処理を抜き出すと次のようになる。

// 抜粋優先・なければ本文先頭から
$excerpt = has_excerpt() ? get_the_excerpt() : '';
if (!$excerpt) {
    $body = strip_shortcodes($post->post_content);
    $body = wp_strip_all_tags($body);
    $body = preg_replace('/\s+/u', ' ', $body);
    $excerpt = trim($body);
}
$desc = wp_trim_words($excerpt, 60, '…');

// 最大 160 文字に詰める(バイト数ではなく文字数で)
if (mb_strlen($desc, 'UTF-8') > 160) {
    $desc = mb_substr($desc, 0, 158, 'UTF-8') . '…';
}

出力する側はこうだ。

echo '<meta name="description" content="' . esc_attr($desc) . '" />';

流れとしては「本文からタグを取り除く → 60語で切る → それでも160文字を超えたら158文字+『…』に切る → 属性値としてエスケープして出力」の4段階である。

ここで1つ目の興味深い点がある。wp_trim_words() の「60語」は、サイトの言語によって意味が変わる。英語のように単語をスペースで区切る言語では文字通り60単語だが、日本語のようにスペースで区切らない言語では、WordPressの翻訳ファイル側で「単語ではなく文字で数える」設定になっており、60文字で切られる。実際、このブログの日本語記事のメタディスクリプションは、ほぼすべて「60文字+『…』=61文字」になっている。一方、英語記事は60単語だと160文字を超えることが多いため、次の段の160文字の上限に引っかかって「158文字+『…』=159文字」に揃う。

同じ関数を通しているのに、日本語は61文字、英語は159文字と、長さの決まり方がまったく違うわけだ。

164文字の正体 — esc_attr() がアポストロフィを変換している

では、159文字のはずの英語版がなぜ164文字と報告されるのか。前回公開した robots.txt の記事(英語版)の実際の出力を見てみる。

<meta name="description" content="When a search engine crawler visits a site, it usually doesn&#039;t jump straight to your articles. It first checks robots.txt to learn where it may go, and it rea…" />

doesn't のアポストロフィ(')が、&#039; という6文字に置き換わっている。これがHTMLエンティティ(文字参照)である。

HTMLの属性値の中に " や ' や < がそのまま入っていると、属性の終わりやタグの始まりと区別がつかなくなる。そのためWordPressの esc_attr() は、出力直前にこうした記号を安全な表記に置き換える(エスケープの考え方は、以前の esc_html()/esc_attr() の記事 で詳しく扱った)。

元の文字 エンティティ 文字数の増え方
& &amp; +4
< &lt; +3
> &gt; +3
" &quot; +5
' &#039; +5

アポストロフィ1つで5文字増える。159文字+5文字=164文字。監査スクリプトはHTMLソースから content="..." の中身を正規表現で取り出し、そのエスケープされたままの文字列の長さを数えているため、164と報告していたのだ。

一方、検索エンジンやブラウザはHTMLを解釈する時点でエンティティを元の文字に戻す(デコードする)。&#039; は画面上では ' の1文字として扱われる。つまり、読み手の目に触れるのは159文字であり、表示上の問題は起きていない。英語は doesn't や it's のような省略形(アポストロフィを含む縮約表現)が多いため、日本語記事ではほとんど見かけないこのズレが、英語記事では毎回のように現れる。

もう1段深い罠 — 上限を数える時点で、すでにエンティティが混ざっていることがある

話はこれで終わらない。同じく英語版の esc_html()/esc_attr() の記事のメタディスクリプションは、監査では同じ164文字なのに、デコードすると156文字しかない。159文字にすら届いていないのだ。

content="... Instead you see echo esc_html( $title ); or &lt;a h…"

末尾に &lt; がある。これは esc_attr() が付けたものではない。記事本文にコード例として <a href=...> を載せるため、本文のHTMLの時点ですでに &lt; と書かれていたものだ。wp_strip_all_tags() はタグは取り除くが、エンティティはテキストとしてそのまま残す。その結果、160文字の上限を判定する mb_strlen() は、&lt; を4文字として数えていた。

数え方を整理するとこうなる。

  1. 上限判定の時点:&lt;(4文字)と '(1文字)を含めて158文字+「…」=159文字
  2. esc_attr() 後:' が &#039; になり +5 → 164文字(監査が見る値)。なお esc_attr() は既存のエンティティを二重にエスケープしないため、&lt; はそのまま
  3. デコード後:&#039; → '(−5)、&lt; → <(−3)→ 156文字(読み手が見る値)

「160文字以内に詰めたつもり」の処理が、実際には「エンティティを含んだ160文字」を数えていたわけで、コード例の多い技術記事ほど、表示される文は想定より短くなる。今のところ目安の範囲内に収まっているので実害はないが、「どの時点の文字列を数えているか」を意識していないと、上限処理そのものが意図とずれうることを示す実例である。

「文字数」には少なくとも4つの意味がある

ここまでの話を一般化すると、同じ1つの文に対して、「長さ」は少なくとも4通りに測れる。前述の robots.txt の記事(英語版・日本語版)で実際に計測した値が次の表である。

測り方 英語版 日本語版
エスケープ後の文字数(HTMLソース上) 164 61
デコード後の文字数(読み手が見る文字) 159 61
UTF-8のバイト数(デコード後) 161 177
表示幅 端末・フォント次第 端末・フォント次第

日本語版は61文字なのに177バイトある。UTF-8では日本語の多くの文字が1文字3バイトで表現されるためだ(文字コードとバイトの関係は UTF-8とcp932の記事 でも取り上げた)。英語版の「…」も1文字だが3バイトなので、159文字が161バイトになっている。

Pythonで確認すると、この違いがはっきり見える。

import html

raw = "it usually doesn&#039;t jump straight"   # HTMLソース上の値
text = html.unescape(raw)                      # 読み手が見る値

print(len(raw))                  # 37 … エスケープ後の文字数
print(len(text))                 # 32 … デコード後の文字数
print(len(text.encode("utf-8"))) # 32 … UTF-8バイト数(英数字のみなので同じ)

プログラミング言語によっても「長さ」の定義は違う。PHPの strlen() はバイト数、mb_strlen() は文字数を返す。JavaScriptの .length はUTF-16の単位で数えるため、一部の絵文字は1文字でも2と数えられる。テーマのコメントに「バイト数ではなく文字数で」とわざわざ書いてあるのは、substr() のようなバイト単位の切り詰めを日本語に使うと、3バイトの文字の途中で切れて文字化けした不完全なバイト列を出力してしまうからだ。

監査ではどう数えるべきか

目的が「読み手に何文字見えるか」を確認することなら、デコードしてから数えるのが正しい。先ほどの例なら html.unescape() を通してから len() を取ればよい。

このブログの監査スクリプトは現状エスケープ後の文字数を表示しているため、英語版で160を少し超える値が出るのは「既知の許容差分」として扱っている。仕組みが分かっていれば、164という数字を見ても慌てずに済む。逆にいえば、仕組みを知らずに「160を超えているから直そう」と本文を削り始めると、存在しない問題を追いかけることになる。数字を見たら、まずそれがどの段階の、どの単位の数字かを確かめるのが先だ。

よくある落とし穴

  • HTMLソースの文字数をそのまま「表示文字数」とみなす: エンティティの分だけ多く見える。アポストロフィ1つで+5文字
  • エスケープ済みの文字列に対して上限を判定する: 表示される文は想定より短くなる。本文にコード例を含む技術記事で起きやすい
  • バイト単位の関数で日本語を切り詰める: 文字の途中で切れて文字化けする。PHPなら mb_substr() を使う
  • 単語の途中で切られることを想定しない: 文字数で機械的に切ると it rea… のように単語が途中で途切れる。気になる場合は、抜粋を手書きで用意するのが確実
  • 文字数を守れば必ずその通り表示されると思い込む: 検索エンジンは検索語句に応じてスニペットを差し替える。メタディスクリプションは「候補」である
  • 言語ごとの数え方の違いを見落とす: wp_trim_words() は日本語では文字数、英語では単語数で切る。同じ設定値でも結果の長さはまったく違う

まとめ

メタディスクリプションの「文字数制限」は、仕様上の上限ではなく、検索結果の表示幅から逆算された目安である。だから大切なのは文字数をぴったり守ることより、切られても意味が通るよう要点を前半に置くことだ。そして「文字数」を扱うときは、エスケープ前か後か、文字かバイトか、どの時点の文字列か——を区別する必要がある。監査結果の164という数字は、壊れた実装のサインではなく、HTMLが記号を安全に運ぶための表記がそのまま数えられていた結果だった。数字の意味を確かめてから判断する、という当たり前の手順が、ここでも一番の近道になる。