HugoでSearch Consoleに追加された「ウェブに関する主な指標レポート」(CLS)の問題に対応する

Google Search Consoleに追加された新たな指標値

Google Search Consoleに新たな指標が追加されました。すべて、ウェブに関する主な指標レポートに記載の通りです。必ずしも早急な対処が必要なものではありません。ただし、これらの指標に対応することで、必然的にUI/UXの向上が期待できます。

  • LCP (Largest Contentful Paint)
  • FID (Fist Input Delay)
  • CLS (Cumulative Layout Shift)

LCP (Largest Contentful Paint)

LCPは、「ビューポート内に表示される最大の画像またはテキストブロックのレンダリング時間」を表し、Largest Contentful Paint (LCP)に具体例が掲載されています。ビューポートは、「モバイルデバイスでコンテンツを表示する幅」を示します。ビューポートは、<meta>タグで明示的に指定可能です。

例えば、いわゆるレスポンシブなデザインを採用しているページでは、下記の指定をすることが多いでしょう。「コンテンツをモバイルデバイス(Safari等のブラウザ)の幅に合わせてレンダリングし、等倍で表示する」ことを表しています。ユーザーの操作による拡大・縮小を制限するものではないことに注意してください。ブラウザが、レンダリング時に描画する幅を示しています。

<meta name="viewport" content="width=device-width,initial-scale=1">

「2.5秒」以内に、「最大の画像」または「最大のテキストブロック」が表示されると良い、と(Googleは)定義しています。必ずしもすべての要素が表示されるまでの時間を示しているわけではないことに注意してください。表示されるページのレイアウトが決定されるまでの時間と捉えることもできます。ページが完全に表示されるまでの時間から、考え方がシフトしていることが分かりますね。

FID (Fist Input Delay)

FIDは、定義を直訳すると「ユーザーが最初にページと対話した時(リンクをクリックした時、ボタンをタップした時、JavaScriptを使ったカスタムコントロールを使用した時など)から、ブラウザが実際にその対話に応答してイベントハンドラの処理を開始できるようになるまでの時間」です。

リンクやボタンをタップしても何の応答もない、もしくは応答がないように感じるページに遭遇したことはありませんか。ボタンをタップしたのかどうかもわからない、イライラしますよね。そのようなユーザーのイライラを抑えるために、イベント(クリックやタップ)を発火したタイミングから**「100ミリ秒」以内**に何らかの応答をせよ、ということです。

CLS (Cumulative Layout Shift)

CLSは、「読み込みフェーズにおけるページレイアウトの移動量を示します」とあります。First Input Delay (FID)では、「ページの全ライフサイクルの間に発生する予期せぬレイアウトシフトのすべてについて、個々のレイアウトシフトスコアの合計を測定」とあります。同ページに掲載されているYouTubeの動画を見るのが、理解への近道です。

要は、ページ読み込み中の「ぐらつき」の累積を表します。

ショッピングサイトの決済画面を思い出してください。カートに入れた商品を確認する「確認画面」、カートに入れた商品を注文する「注文画面」があったとします。後者の画面では、最後に「注文」ボタンを押しますよね。誤った注文をキャンセルしたい場合、「キャンセル」ボタンを押すかもしれません。「確認画面」から「注文画面」へ遷移後、注文を取り消すために「キャンセル」ボタンを押したいとします。しかし、ページの読み込みが完全に終わっておらず、突如「キャンセル」ボタンの上に広告等が挿入されることがあります。その結果、「表示のズレ」が発生し、ボタンではなく誤って広告をクリックした、という経験はありませんか。もしくは、「キャンセル」しようとしたのに、誤って「注文」してしまったというケースも考えられるかもしれません。

これが、ページの「ぐらつき」です。細かな計算方法は割愛しますが、CLSはこの「ぐらつき」をなくしましょうという指標です。LCP、FIDが「時間」を指標としていたのに対して、CLSのみレイアウトに関するものです。

デスクトップ(パソコン)にのみCLSの警告が表示された

前置きが長くなりましたが、本記事の本題にようやく突入できました。ある日、Google Search Consoleをのぞくと、**「CLSに関する問題: 0.25超 (パソコン)」**と表示されていました。モバイルに関する指標は、すべて良好でした。CLSは、前述の通り「ページ読み込み時のレイアウトのぐらつき」に関する問題です。パソコンとモバイルの違いを検討した結果、問題は<img>タグで表示される画像にあることが分かりました。

原因はタグに"width”、“height"属性を指定していなかったこと

詳細は、【2020年夏】imgタグにはwidthとheight属性を書くのがいいらしい | Rriverの記事が大変分かりやすく勉強になりました。ブラウザが賢くなったのです。Can I useによると、Chrome、Firefox、Edge (Chromium)など主要ブラウザで対応していることが分かります。

また、当サイトはモバイルよりもデスクトップからの訪問者が圧倒的に多いです。必然的にブラウザシェアの高いChromeからのアクセスが多くなります。MacやiPhoneなど、Apple系のコンテンツが多めのため、賢くなる前(?)のSafariからのアクセスが多かったのでしょう。その結果、「賢い」デスクトップと、「賢くなる前」のモバイルで明暗が別れたのです。

HugoでIMGタグに自動的に"width”、“height"属性を付与する

それ、Hugoで簡単に解決できます。Image Functions | Hugoに、imageConfigという素晴らしい関数が記載されています。また、Hugoのバージョン0.62.0以降ですが、Configure Markupという、画像のレンダリング前にフックできる仕組みも持っています。素晴らしい。何も考えずに対応できる、はずでした。

当サイトの構成は、WordPress時代の名残を(私自身が)受けて、contentディレクトリの下にposts(記事)、uploads(画像)が、年月毎に保存されるようになっています。すべてのディレクトリを表示すると数が多くなるため、一部のみ掲載しています。

├── archetypes
├── content
│   ├── pages
│   ├── posts
│   │   ├── 2014
│   │   │   ├── 09
│   │   │   ├── ...
│   │   │   └── 12
│   │   └── 2020
│   │       ├── 04
│   │       ├── ...
│   │       └── 06
│   └── uploads
│       ├── 2014
│       │   ├── 09
│       │   ├── ...
│       │   └── 12
│       └── 2020
│           ├── 04
│           ├── ...
│           └── 06

Hugoの画像(リソース)ファイルは、Page Resourcesでのみ機能します。imageConfig関数は、画像(リソース)ファイルの情報を取得するだけで、操作はしないためPage Resourcesの一部である必要はありません。

Page resources are available for page bundles only, i.e. a directory with either a index.md, or _index.md file at its root. Resources are only attached to the lowest page they are bundled with, and simple which names does not contain index.md are not attached any resource.

Page Resourcesとは、簡潔に言えば、index.mdもしくは_index.mdがあるディレクトリ、およびその配下です。uploadsディレクトリ配下に、そのようなファイルがありません。

そこで、まずは/content/uploads直下に、以下のindex.mdを新規作成しました。Front Matterが存在するだけの、いわば空ファイルです。ただのダミーファイルのレンダリングを防止するため、headlesstrueにします。Page Bundlesを参照してください。index.mdを作成することで、uploadsフォルダをPage Resourcesとして認識でき、かつタイトルしかないHTMLファイルの生成も防ぐことができました。

---
title: Media
headless: true
---

まだまだ苦難は続きます。通常は、Hugo Markdown Render Hooks 入門 - Qiitaのように、.Page.Resourcesでレンダリング対象の記事中の画像(リソース)を簡単に取得できます。しかし、当サイトのように記事と画像が分断されている場合、この方法は使用できません。そこで、/layouts/_default/_markup/render-image.htmlを、以下のように作成しました。

{{ $image := (site.GetPage "uploads").Resources.GetMatch (strings.TrimPrefix "/uploads/" .Destination) }}
{{ with $image }}
{{ $imageConf := (imageConfig (printf "content/uploads/%s" .Name)) }}

<figure>
    <img src="{{ $.Destination | safeURL }}" alt="{{ $.PlainText }}" width="{{ $imageConf.Width }}"
        height="{{ $imageConf.Height }}">
    <figcaption>
        {{ if $.Title }}
        <p>{{ $.Title | markdownify }}</p>
        {{ else if $.Text }}
        <p>{{ $.Text | markdownify }}</p>
        {{ end }}
    </figcaption>
</figure>

{{ end }}

まず、site.GetPage "uploads"/content/uploadsディレクトリ配下をPage Resourcesとして取得します。siteは、/contentディレクトリ全体から検索することに注意してください。

.Destination<img>タグのsrc属性に指定された値(例:/uploads/2020/07/image.png)を取得します。前段で取得したPage Resourcesは、/content/uploads配下を指します。/content/uploadsの下にuploadsディレクトリは存在しません。そのまま画像(リソース)を取得しようとすると、no such file or directoryエラーです。そこで、画像(リソース)を取得する前に、.Destionationに含まれる/uploads/を意図的に削除します。削除した上で、GetMatch関数により同じ名前の画像ファイルを検索します。

この方法により、ようやく画像をPage Resourcesとして取得することができます。なお、GetMatch関数は最初に見つかったリソースファイルを返却します。次に、リソースのファイル名(.Name)を元に、imageConfig関数により画像の情報を取得します。あとは、imageConfigWidth、およびHeightプロパティを、<img>タグの属性に指定することで完成です。

imageConfig関数に直接画像ファイルのパスを指定できない理由

imageConfig関数で必要なのは、Page Resourcesではなくファイルのパスです。画像ファイルのリサイズ等、画像を変更する操作を行う場合、Page Resourcesである必要がありますが、imageConfig関数は対象外です。では、なんでこんなまどろっこしい事をする必要があるのか疑問に思われたかもしれません。imageConfigに直接.Destinationを指定すれば良いように思います。あるサイトでは十分に機能するでしょう。

Markdown Render Hooksでは、Markdownのイメージとして指定されたすべてが対象になります。つまり、外部のイメージファイル(例えば、Placehold.jpで作成されたダミーファイル)が指定されているかもしれません。もちろん、外部のファイルに対してimageConfig関数は適用できず、ビルド時にエラーとなります。そのため、imageConfig関数の前段で何らかの判断が入るはずで、その手段として今回はPage Resourcesによる取得を用いました。将来的に画像のリサイズ等を動的に実施する可能性もあったからです。(ビルド時間が膨大になるため実施しないと思いますが)

参考

comments powered by Disqus