Web

来店できない会社の営業時間をマークアップしていた

  • SEO
  • 構造化データ
  • JSON-LD
  • schema.org
  • Astro

labee.jpは全ページで LocalBusiness の構造化データを出していました。営業時間、対応エリア、価格帯、サービスカタログ。どれもページのどこにも表示していない情報です。Googleの構造化データポリシーが禁じている状態でした。

幸いペナルティはありません。Search Consoleに手動による対策の通知は届いておらず、順位の変動として見えるものもありませんでした。とはいえ気持ちのいい状態ではないので消しました。

ブログプロダクトサイト群を 1 つの pnpm workspace に集約した複数のプロダクトサイトを別リポジトリで運用していたところを、`sites/` と `packages/` の2層構成を持つ単一の pnpm workspace モノレポに集約しました。catalog で依存バージョンを揃え、共有パッケージはロジックと構造だけを渡してデザインは各サイトに残す設計、仮移植のサイトを抱えたまま動かす運用までを扱います。

技術記事に営業時間が付いていた

きっかけは構造化データの棚卸しです。出力しているJSON-LDをページと突き合わせていて、ブログ記事のページで手が止まりました。

出ていたのはこういうノードです。

{
  "@context": "https://schema.org",
  "@type": "LocalBusiness",
  "openingHoursSpecification": [{ "dayOfWeek": "Monday", "opens": "09:00", "closes": "18:00" }],
  "areaServed": ["横浜市", "八王子市", "町田市"],
  "priceRange": "¥¥",
  "hasOfferCatalog": { "@type": "OfferCatalog", "itemListElement": [] }
}

技術記事です。営業時間の話も、対応エリアの話も、料金の話も出てきません。住所だけはフッターに表示されていましたが、残りの4項目はどのページを開いても本文にありません。ブログ記事にもFAQにも同じノードを出していたので、この4項目は常に不可視でした。

Googleの構造化データに関する一般的なガイドラインには、一文でこう書かれています。

Don’t mark up content that is not visible to readers of the page.

読み返すまでもない違反でした。

気づく機会がなかった

ルールを知らなかったわけではありません。知っていて、気づいていませんでした。

構造化データを入れたときは「会社サイトなら LocalBusiness だろう」という程度の判断で、営業時間や対応エリアはテンプレートの穴を埋めるように書いています。その後、この4項目が検証の対象になることは一度もありませんでした。ビルドは通ります。リッチリザルトテストも構文としては通ります。ページを見てもJSON-LDは <script> の中なので目に入りません。誰も困らないまま出力だけが続きます。

そもそも型が合っていなかった

不可視の4項目を消せば済むかとも思いましたが、その前に LocalBusiness という型を読み直しました。schema.orgの定義です。

A particular physical business or branch of an organization. Examples of LocalBusiness include a restaurant, a particular branch of a restaurant chain, a branch of a bank, a medical practice, a club, a bowling alley, etc.

レストラン、銀行の支店、診療所、ボウリング場。客が足を運ぶ形態の事業ばかりです。

うちは受託開発が中心で、顧客が拠点を訪ねてくる営業形態ではありません。営業時間を書いたのも対応エリアを書いたのも、型が要求するから埋めただけでした。プロパティが不可視だったのは結果で、型の選択自体が実態に合っていなかったのが原因です。

@idで統合する、という通説を確かめた

LocalBusinessOrganization を両方使いたい場合の手法として、別々のJSON-LDブロックに分けて同じ @id を持たせれば1つのエンティティとして扱われる、という説明をよく見かけます。これも検討したのですが、一次資料を当たると2つの話が混ざっていました。

@id でノード同士を参照する用法そのものは公式に案内されています。冒頭で引用したのと同じガイドラインの「Multiple items on a page」に、こうあります。

If there are items that are more helpful when they are linked together (for example, a recipe and a video), use @id in both the recipe and the video items to specify that the video is about the recipe on the page.

商品バリエーションのページでも、"shippingDetails": { "@id": "#shipping_policy" } のように別ノードを @id で指す書き方が例全体で使われています。

見つからなかったのはその次の一段でした。同じ @id を持つ複数のブロックが1つのエンティティに統合される、という挙動を明記した記述は、今回読んだGoogleの公式ドキュメントにはありません。上の一文も「両方のitemに @id を使う」とあるだけで、書き方の具体は示されておらず、この用法のコード例もありません。統合の挙動自体はJSON-LDのグラフとしての性質から説明できますが、Googleが仕様として保証していると読める一次資料は確認できませんでした。

型を複数持たせるなら配列

Googleが実際に案内しているのは、1つのエンティティに複数の型を持たせたい場合は @type を配列にする方法です。ローカルビジネスのページにこうあります。

If you have multiple types, specify them as an array (additionalType isn’t supported).

同じページには、次の一文もあります。

Since LocalBusiness is a subtype of Organization, we recommend following the fields for Organization in addition to the fields required and recommended below.

1つのエンティティに複数の型を載せる話と、別のエンティティを @id で指す話は、別の問題への別の答えでした。前者の答えが配列型で、@id は後者のための道具です。今回必要だったのは前者だけでした。

Organization単体に寄せた

配列型でまとめる案も検討しましたが、採りませんでした。来店型の事業実態がない以上、型を正しく合成しても営業時間や対応エリアという不可視の情報は残ります。型の組み方を直す前に、この型を持つ理由がありませんでした。

LocalBusiness のノードを削除し、OrganizationWebSite の2つに整理しました。

{ "@context": "https://schema.org", "@type": "Organization", "name": "…" }

Organization にも社名やURL、住所、代表者は残りますが、これらはフッターや会社概要ページに実際に表示されている情報です。削除に伴って、LocalBusiness 側にだけ残っていた別名表記の誤りも、ノードごと消えることで解消しました。

出す前にページを開く

今回の教訓はルールを覚えることではありません。ルールは知っていました。

足りなかったのは、ノードを追加するときにそれが載るページを実際に開いて、書いた内容がそこに表示されているかを確かめる手順です。テンプレートの穴を埋める作業として書くと、この確認が抜けます。全ページ共通のノードなら、最も内容の薄いページ、たとえばブログ記事で見てみるのが早いはずです。そこに書いていない情報は、他のどのページでも怪しいと思っていいと思います。

出力範囲にも見直しの余地があります。Organization の構造化データについて、Googleは配置場所に関する案内でこう述べています。

We recommend placing this information on your home page, or a single page that describes your organization, for example the about us page. You don’t need to include it on every page of your site.

今回は型の整理だけに留めて、出力範囲は既存のままにしました。ブログ記事1本ずつに会社情報のノードを載せ続ける必要はなさそうです。ページの性質ごとに出すものを絞る作業は、次の宿題です。