一部だけ足したつもりが、丸ごと置き換わっていた
1行足したつもりでした。実際には、そのページの共有画像が消えていました。
しかも消えたのは、その日に触ったページだけではありません。調べたら、以前から9ページで同じことが起きていました。画面には出ず、動作も壊れず、誰も気づいていませんでした。
Next.js のページ情報(メタデータ)は、親と子で「項目ごとに混ざる」のではなく「かたまりごとに入れ替わる」という話です。気づいた経緯と、同じ間違いを繰り返さないための置き方を書きます。
正規URLを足しに行っただけだった
きっかけは検索エンジン側の指摘でした。ブログ一覧に絞り込みが付いているため、同じ中身が複数のURLで見える状態になっていて、56件が「重複しています」と報告されていました。
対処として、各ページに「このページの代表URLはこれです」という指定を足しました。Next.js では alternates というかたまりの中に書きます。
// 記事一覧ページ
export const metadata = {
alternates: {
canonical: "/blog",
},
};
足したのは1項目です。ところが公開後に確認すると、そのページからRSSフィードの場所が消えていました。
親に書いてあったものが、丸ごと消えた
フィードの場所は、全ページ共通の設定として親の側に書いてありました。
// 全ページ共通の側
alternates: {
types: {
"application/rss+xml": [{ url: "/feed.xml", title: "..." }],
},
},
期待していたのは、親の types はそのまま残り、子で足した canonical が加わることです。実際は違いました。
子のページが alternates を書くと、親の alternates はまるごと捨てられます。中の項目どうしは比べられません。
つまり canonical だけを書いた瞬間、そのページの alternates は「canonical しか無いもの」になります。フィードの場所は、書き足したのではなく、上書きで消えたことになります。
購読アプリはブログ一覧を最初に見に来ます。そこから場所が消えるのは困ります。
同じ形が、すでに別の場所で起きていた
この仕組みが分かった時点で、嫌な予感がしました。同じ書き方をしているかたまりが、もう1つあるからです。SNSで共有されたときの見え方を決める openGraph です。
共有画像は親の側に1枚だけ置いてあり、各ページはそこに乗るはずでした。ところが個別のページには、こう書いてありました。
export const metadata = {
openGraph: {
title: "...",
description: "...",
},
};
画像の指定はありません。画像を消すつもりも、当然ありません。それでも alternates と同じ理屈で、親の openGraph はまるごと捨てられます。画像だけが残るということは起こりません。
公開中のページを1つずつ取り寄せて、実際のHTMLを見ました。
| ページ | og:title |
og:image |
|---|---|---|
| トップ | あり | あり |
| ブログ一覧 | あり | 無し |
| 用語集 | あり | 無し |
| プロフィール | あり | 無し |
| 各記事ページ ほか | あり | 無し |
合計9ページで画像が出ていませんでした。og:title は書いてあるので、確認する側から見ると「設定してある」ように見えます。抜けているのは、書いていない項目のほうでした。
なぜ長いあいだ気づかなかったのか
この不具合には、気づくきっかけがほとんどありません。
画面には何も出ません。ページは普通に表示され、リンクも動きます。ビルドも通ります。書き間違いではないので、警告も出ません。「書いていないものが消える」ので、書いたものを見ているかぎり異常が見えません。
この設定が効くのは、リンクを他のサービスに貼ったときです。共有されて初めて、画像の無い地味なリンクとして出ます。
つまり自分のサイトを見ているかぎり永久に分かりません。今回も、別件を調べていて仕組みのほうから逆算しただけで、症状から見つけたわけではありません。
書き写すのではなく、1か所から読む
直し方は単純で、上書きするページでは、消えるものを毎回書き添えます。
ただし各ページに同じ内容を書き写すと、いずれ書き忘れます。今回消えていた9ページは、まさに書き忘れの集まりでした。同じ直し方をすれば、同じことが起こります。
そこで、共通の値を1か所に置いて、そこから読む形にしました。
export const FEED_ALTERNATE_TYPES = { /* ... */ };
export function alternatesFor(canonicalPath) {
return {
canonical: canonicalPath,
types: FEED_ALTERNATE_TYPES,
};
}
各ページは alternatesFor("/blog") と書きます。「canonical を足す」と書けば、消えるはずのものが自動で付いてきます。共有画像のほうも同じ形にして、既定の画像を1か所に置き、上書きするページはそれを混ぜて返すようにしました。
「かたまりごと入れ替わる」という挙動そのものは、こちら側では変えられません。変えられるのは、上書きが必要なときに安全な形しか書けないようにしておくことです。
注意して書き写す運用は、9ページぶん失敗した実績があります。
今回の学び
まず、設定を足すときは「入れ替わる単位」を先に確かめるということ。項目ごとに混ざると思い込んでいたので、1項目を足す作業のつもりでいました。実際にはかたまりの差し替えでした。足し算だと思っていた操作が、代入だったわけです。
次に、1件見つけたら、同じ形を探しに行くこと。今回いちばん役に立ったのは、直したことではなく「同じ書き方をしている場所は他にないか」と考えたことでした。目に見える不具合は1つでしたが、同じ形をした見えない不具合が9つありました。
そして、自分の画面に出ない設定は、外から取り寄せて確かめること。共有時の見え方も、検索エンジン向けの指定も、開発中の画面には出てきません。公開中のHTMLを1ページずつ取り寄せて初めて、書いてあるつもりのものが無いと分かりました。書いたコードではなく、出ている結果を見る必要がありました。
関連記事

そのページは何だと名乗っているか
用語集の分類ページを調べていて、妙な指定に気づきました。このページは「記事」だと名乗っていました。 中身は用語が並んだ一覧です。公開日もなければ、書いた人もいません。それでも、SNSや検索エンジンに向けては「これは1本の […]

推測させるのをやめる
sitemap.xml・robots.txt・構造化データ。バラバラの施策に見えて、やっていることは同じだった。これまで相手に推測させていたことを、こちらから明示的に伝える。そして実装中に見つけた、一覧用のクエリを流用すると21本目から静かに漏れ始めるという落とし穴。

見た目で区別が付いているものは、機械には区別が付いていない
見た目で伝わっている区別が、機械には伝わっていなかった話です。何が起きていたか、どう直したか、そして「見えるから伝わっている」という思い込みについて書きます。