メインコンテンツへスキップ
#Web制作 約7分で読めます

descriptionの文字数を管理し始めたら、検索結果の「…」が消えた話と、JSの文字数が3通りある話

検索結果でメタディスクリプションが切れていたのを機に文字数管理を始めた記録。String.length・コードポイント・Intl.Segmenterで「文字数」がズレる罠をコード例で整理し、ブラウザ完結の自作カウントツールも紹介。

ある日スマホで自分のブログをエゴサ的に検索してみたら、検索結果に並んだ自分の記事のdescriptionが、ことごとく「…」で切れていた。しかも切れている位置が悪い。「LCPが4.6秒から1.8秒に改善した手順を…」みたいに、一番言いたい結論の直前でぶった切られている。

書いた本人としては160字フルに使って丁寧に説明したつもりだったのだが、スマホの検索結果はそんなに表示してくれない。そこから文字数をちゃんと管理するようになった、という話。

スマホは120字、PCは160字。ただし正確には「字数」ですらない

調べてわかったのは、メタディスクリプションの表示はスマホで約120字、PCで約160字が目安ということ。「約」なのは、Googleが実際には文字数ではなくピクセル幅で切っているからだ。全角と半角、漢字とひらがなで幅が違うので、同じ字数でも切れたり切れなかったりする。

なので厳密な上限を追いかけても意味がない。実務的には、

  • 最初の120字に要点と結論を入れる(スマホで切れても意味が通る)
  • 全体は160字以内に収める(PCではフルで見せる)

という2段構えにした。タイトルも同様で、30字前後を超えると末尾が切れる。要は「切れても成立する順番で書く」が本体で、文字数はその検算にすぎない。

このブログはzodで160字を強制している

うちのブログ(Astro製)では、content collectionsのスキーマでdescriptionの上限を機械的に縛っている。

// src/content.config.ts
schema: z.object({
  title: z.string(),
  description: z.string().max(160),
  // ...
})

これでdescriptionが161字あるとビルドが落ちる。「気をつける」ではなく「通らない」にしたのは正解で、導入してから書き忘れも超過も一度も起きていない。

ただ、これを入れたときにひとつ引っかかったことがある。zodの.max(160)が数えている「文字数」とは何なのか、という問題だ。

JavaScriptの「文字数」は3通りある

答えを先に書くと、z.string().max()は内部でString.prototype.lengthを使っている。そしてJSのlengthは文字数ではなくUTF-16コードユニット数だ。ここがズレる。

const s = "𩸽の刺身";   // 𩸽(ほっけ)はサロゲートペア

s.length          // 5 ← 𩸽が2カウントされる
[...s].length     // 4 ← コードポイント単位。直感に近い

𩸽𠮷(つちよし)のようなBMP外の漢字は、UTF-16では2つのコードユニット(サロゲートペア)で表現されるので、lengthだと1文字で2を消費する。スプレッド構文[...s]はコードポイント単位で分割するので、こちらは4になる。

さらにもう一段ある。結合文字だ。macOSからコピーしたファイル名などで実際に混入するやつで、見た目は1文字なのにコードポイントが2つある。

const ha = "ば";  // 「ば」を「は+結合濁点」で表現(NFD正規化。macOS由来で混入しがち)

ha.length          // 2
[...ha].length     // 2 ← コードポイント単位でも2
[...new Intl.Segmenter("ja", { granularity: "grapheme" }).segment(ha)].length
                   // 1 ← 人間の目で見た「1文字」

人間の感覚に一致するのは最後のIntl.Segmenter(書記素クラスタ単位)だけ。つまりJSには「文字数」の定義が少なくとも3層ある。

数え方単位”𩸽の刺身""ば”
str.lengthUTF-16コードユニット52
[...str].lengthコードポイント42
Intl.Segmenter書記素(見た目の1文字)41

descriptionくらいの日本語文なら3つの結果はほぼ一致するので実害は薄い。ただ、SNS投稿の上限判定や、DBの文字数制限バリデーションをlengthで書いていると、特殊な漢字や結合文字を含む入力で「見た目より多く数えられて弾かれる」事故が起きる。フォームのバリデーションを書く人は、自分がどの層で数えているかは意識しておいたほうがいい。

毎回コンソールで数えるのが面倒なので、ツールにした

で、実際の運用の話。descriptionを書くたびにDevToolsのコンソールで[...s].lengthを打つのはさすがに面倒くさかったので、ブラウザで動く文字数カウントツールを自分で作った。

文字数カウントツール

貼り付けた瞬間に、総文字数・空白除く・行数・UTF-8バイト数・原稿用紙換算あたりが一気に出る。カウントはコードポイント単位([...str]相当)なので、lengthのサロゲートペア問題は踏まない。

個人的に一番使っているのが残り文字数ゲージで、「メタディスクリプション スマホ目安120字 / PC目安160字」のバーが並んでいる。descriptionを貼ると、スマホのバーがどこで振り切れるかが一目でわかるので、「120字までに結論、160字で切る」の2段構えがそのまま画面上で確認できる。X(全角換算で140字)やInstagram用のゲージも同じ画面にある。

あとは文字種の内訳(ひらがな・カタカナ・漢字・英数字の比率)も出るようにした。これは半分おまけだが、descriptionが漢字だらけで硬すぎるときに気づけるので、意外と使っている。入力内容はlocalStorageに保存されるだけでサーバーには一切送信しないので、公開前の下書きを貼っても平気だ。

管理し始めて変わったこと

既存記事のdescriptionを「120字までに結論」ルールで20本ほど書き直した。効果として一番はっきりしているのは、単純にスマホの検索結果で「…」で切れる記事がなくなったこと。切れていた頃は結論の手前で途切れて何の記事かわからなかったのが、今は2行でちゃんと完結して見える。

CTRへの影響は正直まだ断言できる段階ではない。ただ、Search Consoleで見る限りリライトした記事群の表示回数あたりのクリックは悪くなってはいないし、少なくとも「一番言いたいことが表示されない」という機会損失は確実に消えた。

descriptionは検索結果に出る唯一の「自分で書ける広告文」なのに、切れていたら書いていないのと同じ。文字数管理はその最低ラインの話で、カウントツールにゲージを付けてからは考えることが減った。数える作業は道具に任せて、人間は120字に何を入れるかだけ考えればいい。

#メタディスクリプション #JavaScript #Unicode #SEO
シェア