Коли я почала додавати structured data до статей у ICanUp, найпростіший варіант виглядав очевидно: зібрати BlogPosting, передати title, description, URL та ім'я автора.
Але дуже швидко стало зрозуміло, що JSON-LD не повинен бути окремою SEO-копією Post state.
Structured data мають описувати ту саму Post entity, той самий canonical URL, ту саму locale і того самого автора, яких уже бачить application.

Що я хочу отримати від BlogPosting
Для мене BlogPosting має відповідати на кілька простих питань.
Що це за матеріал? Який URL є його canonical public URL? Якою мовою показана поточна версія? Хто її написав? Де знаходиться author entity?
Якщо JSON-LD відповідає на ці питання інакше, ніж сама сторінка, SEO infrastructure вже має два джерела правди.
{ "@context": "https://schema.org", "@type": "BlogPosting", "headline": "Localized post title", "description": "Localized post description", "url": "CANONICAL_URL", "author": { "@type": "Person", "name": "Alyona Yakymiv", "url": "AUTHOR_URL" }}Author для мене - не просто string
Спочатку author легко передати як просте ім'я.
Але в ICanUp у автора вже є окрема public сторінка, професійна identity і зв'язок із зовнішніми профілями.
Тому в BlogPosting я використовую об'єкт Person, а не просто текстове поле.
{ "author": { "@type": "Person", "name": "Alyona Yakymiv", "url": "https://icanup.com.ua/author/alyona-yakymiv" }}Це створює явний зв'язок між Post і author page.
Search engine отримує не тільки ім'я, а URL сутності, яку можна пов'язати з іншими статтями та structured data типу Person.
Image і sameAs посилюють Person entity
Для author page я також заклала image і sameAs із професійними зовнішніми профілями.
Ці поля належать саме author entity. Їх не потрібно заново придумувати окремо для кожної статті.
Краще мати один author contract і повторно використовувати його в Person та BlogPosting.
{ "@type": "Person", "name": "Alyona Yakymiv", "url": "https://icanup.com.ua/author/alyona-yakymiv", "image": "AUTHOR_IMAGE_URL", "jobTitle": "Full-stack developer", "sameAs": [ "PROFESSIONAL_PROFILE_URL" ]}URL у JSON-LD не повинен жити окремо від canonical
Наступний ризик - сформувати BlogPosting URL вручну.
У multilingual application це особливо небезпечно: default locale може бути без prefix, EN мати /en, а routing rules з часом змінюватися.
URL structured data має походити з того самого public URL contract, що і canonical.
UK:https://icanup.com.ua/posts/example-slug EN:https://icanup.com.ua/en/posts/example-slugЦе той самий принцип, який я використовую для canonical і hreflang у multilingual Laravel.
SEO consumer не повинен самостійно вирішувати, як виглядає public route.
Headline і description беруться з поточної locale
Shared entity identity не означає shared Post metadata.
UK і EN можуть мати той самий Post ID та shared slug, але title, description і visible content у них різні.
Тому BlogPosting має серіалізувати metadata саме поточної localized page.
Field | Shared чи localized |
|---|---|
Post identity | Shared |
Slug | Shared |
Author entity | Shared |
Headline | Localized |
Description | Localized |
Canonical URL | Locale-aware |
Visible content | Localized |
Shared slug не означає однаковий BlogPosting
У ICanUp Post використовує shared slug для всіх locales.
Але EN BlogPosting все одно має EN headline, EN description і EN canonical URL.
Це ще один приклад того, чому я розділяю route identity та content localization.
Сам shared-slug contract я детальніше розбирала у статті про shared slug vs translated slugs.
Author залишається тією самою entity між locales
На відміну від headline або description, автор статті не стає іншою людиною після перемикання locale.
Тому Person identity залишається shared, навіть якщо професійний description на author page локалізований.
Localized presentation не повинна створювати дублікати реальної Person entity.
Окрема author page робить зв'язок явним
До окремої author page ім'я в статті було лише display value.
Після її появи Post UI, BlogPosting JSON-LD і Person structured data можуть посилатися на одну author entity.
Це також прибирає необхідність копіювати професійну інформацію автора у кожну статтю.
Person і BlogPosting повинні погоджуватися між собою
На author page structured data описують Person.
У BlogPosting поле author посилається на цю саму людину.
Якщо ім'я, URL або інші identity fields відрізняються між цими двома JSON-LD payloads, entity graph стає менш послідовним.
{ "@context": "https://schema.org", "@type": "Person", "name": "Alyona Yakymiv", "url": "https://icanup.com.ua/author/alyona-yakymiv", "image": "AUTHOR_IMAGE_URL", "sameAs": [ "PROFESSIONAL_PROFILE_URL" ]}JSON-LD має бути в initial HTML
Ще одна важлива частина implementation - SSR.
Я не хочу покладатися на те, що crawler виконає JavaScript, дочекається hydration і лише після цього побачить structured data.
BlogPosting JSON-LD має бути присутнім у server-rendered HTML одразу.
<script type="application/ld+json">{ "@context": "https://schema.org", "@type": "BlogPosting", "headline": "Localized title", "description": "Localized description", "url": "https://icanup.com.ua/en/posts/example-slug", "author": { "@type": "Person", "name": "Alyona Yakymiv", "url": "https://icanup.com.ua/author/alyona-yakymiv" }}</script>Це продовжує проблему, яку я вже зустрічала раніше: meta tags були в browser, але не в початковому HTML Inertia SSR.
Як я перевіряю structured data
curl -s \ https://icanup.com.ua/en/posts/example-slug \ | grep -A 30 'application/ld+json'Browser inspector корисний, але для цього test case мене цікавить саме response HTML.
Так я перевіряю, що structured data не з'являються лише після client-side rendering.
Я не хочу окремого набору SEO values для JSON-LD
Найгірший варіант для мене - мати title для page, meta title для SEO, ще один headline для JSON-LD і окремий вручну сформований URL.
Чим більше незалежних copies одного public state, тим легше вони розходяться.
Structured data повинні збиратися з уже нормалізованих Post, author і routing data.
Що я б не робила
- Не передавала б автора тільки як plain string, якщо вже існує author entity.
- Не hardcode canonical URL у JSON-LD.
- Не використовувала б UK headline для EN page.
- Не створювала б окремий slug contract тільки для structured data.
- Не дублювала б author profile data в кожній статті.
- Не покладалася б тільки на client-side insertion JSON-LD.
- Не дозволяла б Person і BlogPosting використовувати різні author URLs.
Що я перевіряю тестами
- BlogPosting JSON-LD присутній у initial HTML статті.
- Author має type
Person. - Author містить правильні name та URL.
- Author page містить власний Person JSON-LD.
- Localized page використовує localized headline і description.
- Structured URL відповідає public canonical URL поточної locale.
- UK і EN не змішують localized metadata.
- Author link зі статті веде на ту саму author entity.
Для мене хороший BlogPosting JSON-LD не додає нові факти про статтю. Він послідовно описує факти, які application уже вважає правдою.
Що я залишила собі на майбутнє
- Використовувати Person object замість author string, коли author entity уже існує.
- Тримати author identity shared між localized pages.
- Брати headline і description з поточної locale.
- Брати public URL з того самого routing contract, що і canonical.
- Не створювати окремі SEO copies даних тільки для JSON-LD.
- Рендерити structured data в initial SSR HTML.
- Перевіряти Person і BlogPosting як один пов'язаний entity graph.
Висновок
У ICanUp BlogPosting JSON-LD став не окремою SEO feature, а ще одним consumer уже існуючих application contracts.
Localized Post metadata приходять із поточної мовної версії, URL узгоджується з canonical routing, а author представлений як реальна Person entity з окремою public page.
І все це має бути присутнім у server-rendered HTML ще до hydration.
Чим менше окремої logic знає JSON-LD builder, тим менший ризик, що structured data почнуть описувати іншу сторінку, ніж та, яку реально бачить користувач.



