Коли в ICanUp з'явилися breadcrumbs, я могла б окремо побудувати ще одну hierarchy спеціально для JSON-LD.
Але тоді visible navigation і structured data могли б з часом почати описувати різні шляхи.
Тому для мене BreadcrumbList - це структуроване представлення тієї самої ієрархії навігації, яку вже бачить користувач.

Що BreadcrumbList описує насправді
BreadcrumbList не просто повторює текст breadcrumb labels.
Він описує впорядкований шлях сторінки в середині ієрархії сайту через список ListItem.
Кожен item має position, name і, якщо елемент navigable, public URL.
{ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Blog", "item": "https://icanup.com.ua/" }, { "@type": "ListItem", "position": 2, "name": "Laravel", "item": "https://icanup.com.ua/categories/laravel" }, { "@type": "ListItem", "position": 3, "name": "Current post" } ]}Видима ієрархія для мене первинна
Breadcrumbs спочатку мають сенс для людини.
Вони показують, де поточна сторінка знаходиться відносно Blog, Category або інших parent pages.
JSON-LD не повинен вигадувати parent, якого немає у видимій навігації.
Тип сторінки | Видимий шлях |
|---|---|
Category | Blog > Laravel |
Post | Blog > Laravel > Current post |
Static page | Blog > About |
Post отримує category у ієрархії
Для технічної post категорія є важливою частиною контексту.
Тому breadcrumb для Post може показувати не тільки Blog і title, а й category між ними.
У результаті ICanUp отримує ієрархію, яка одночасно корисна для навігації і структурованих даних.
Blog | +-- Laravel | +-- Laravel BreadcrumbList JSON-LD у реальному блозіПозиція генерується з position, а не з ID бази даних
У Schema.org кожен ListItem отримує sequential position.
Це position у breadcrumb path, а не primary key category, menu item або іншої database entity.
Перший item має position 1, другий - 2, третій - 3 незалежно від їхніх database IDs.
$items = []; foreach ($breadcrumbs as $index => $breadcrumb) { $items[] = [ '@type' => 'ListItem', 'position' => $index + 1, 'name' => $breadcrumb['label'], ];}Поточна сторінка не обов'язково має URL елемента
У visible breadcrumbs поточна сторінка зазвичай показана останньою і не є clickable.
Я не хочу створювати fake navigation behavior тільки заради schema.
Тому для ненавігаційного елемента name і position можуть залишитися, а item не додається.
{ "@type": "ListItem", "position": 3, "name": "Current post"}Localized URLs мають бути такими самими, як canonical routes
Multilingual blog додає ще одну умову: breadcrumb URL залежить від locale.
У default UK locale public URL не має locale prefix, а EN використовує /en.
BreadcrumbList не повинен мати власну логіку для локалізації URL.
UK:https://icanup.com.ua/categories/laravel EN:https://icanup.com.ua/en/categories/laravelЦе той самий URL контракт, який я використовую для canonical і hreflang у multilingual Laravel.
Structured data не повинні окремо вирішувати, коли додавати locale prefix.
Генерація маршруту залишається джерелом істини
Я вже проходила через проблему дублювання localized routing logic.
Тому навігаційний URL, канонічний, hreflang та інші споживачі SEO мають спиратися на маршрутизацію додатків, а не вручну об’єднані рядки.
Один route контракт зменшує ризик, що visible link і JSON-LD item почнуть відрізнятися.
Сам підхід до logical route names я окремо описувала у статті про localized URLs і logical routing у Laravel.
Вкладені категорії роблять BreadcrumbList цікавішим
Якщо category tree стає глибшим, breadcrumb path теж може стати довшим.
Наприклад, замість одного Laravel level може з'явитися parent category і child category.
У такій ситуації structured data повинні повторити реальний упорядкований ланцюг предків, а не тільки поточну category.
Blog | +-- Development | +-- Laravel | +-- Current postLabels теж мають бути localized
URL - не єдина localized частина breadcrumbs.
Visible label category або page title також має відповідати поточній locale.
Інакше EN page може отримати правильний EN URL, але UK breadcrumb name всередині JSON-LD.
Part | UK page | EN page |
|---|---|---|
Category label | Laravel | Laravel |
Post title | UK localized title | EN localized title |
Category URL | /categories/laravel | /en/categories/laravel |
BreadcrumbList і BlogPosting можуть жити на одній сторінці
Post page не обмежена одним JSON-LD type.
BlogPosting описує Post entity, тоді як BreadcrumbList описує її місце в ієрархії сайту.
Ці schemas доповнюють одна одну, а не конкурують за роль головного structured object.
[ { "@type": "BlogPosting", "headline": "Localized post title" }, { "@type": "BreadcrumbList", "itemListElement": [] }]Я не дублюю ієрархію навігації на рівні SEO
Найризикованішим рішенням було б окремо зібрати visible breadcrumbs у frontend і вдруге вручну описати ієрархію в SEO service.
Тоді зміна category tree або page structure вимагала б синхронного оновлення двох реалізацій.
Structured data краще будувати з уже нормалізованої breadcrumb collection.
Що я вважаю breadcrumb item контрактом
Field | Навіщо потрібне |
|---|---|
label | Visible і schema name |
url | Public localized URL для navigable item |
navigable | Чи має element item URL |
order | Позиція в середині ієрархії |
Static pages мають простіший path
Не кожна сторінка потребує ієрархії категорій.
Для static page breadcrumb може бути коротким: Blog і поточна page.
Це ще одна причина не hardcode Post-specific hierarchy всередині BreadcrumbList builder.
Blog > AboutJSON-LD має бути в initial HTML
BreadcrumbList є SEO structured data, тому я перевіряю його не тільки після browser hydration.
Schema повинна бути присутня в page source, який отримує сканер.
Це той самий SSR принцип, який у мене вже з'явився після кейсу, коли meta tags були в browser, але не в initial HTML.
Як я перевіряю BreadcrumbList
curl -s \ https://icanup.com.ua/en/posts/example-slug \ | grep -A 40 '"BreadcrumbList"'Після raw HTML я також дивлюся сам JSON-LD payload.
Мене цікавить не тільки валідність синтаксису, а й те, чи схема збігається з реальною видимою ієрархією breadcrumb.
Що я перевіряю тестами
BreadcrumbList JSON-LD присутній у page source.
Visible breadcrumbs і структурована ієрархія мають однаковий порядок.
Positions починаються з 1 і йдуть послідовно.
Navigable items мають localized public URL.
Current non-clickable page не отримує fake item URL.
Post ієрархія містить правильну category.
Вкладені предки категорії зберігають правильний порядок.
UK і EN використовують відповідні localized labels та URLs.
Static pages не отримують Post-specific ієрархію.
Що я б не робила
Не створювала б окремий breadcrumb tree тільки для SEO.
Не використовувала б database ID як ListItem position.
Не харколила locale prefixes у JSON-LD builder.
Не додавала б item URL до non-navigable current page механічно.
Не використовувала б UK labels усередині EN schema.
Не пропускала б parent categories, які реально присутні у visible hierarchy.
Не покладалася лише на клієнтську вставку JSON-LD.
Для мене BreadcrumbList хороший тоді, коли він не придумує ієрархію для пошукової системи, а точно описує навігацію, яка програма вже показує користувачу.
Що я залишила собі на майбутнє
- Будувати structured breadcrumbs із тієї самої normalized collection, що і visible UI.
- Генерувати position із ordered path.
- Використовувати application routing для item URLs.
- Локалізувати і URL, і breadcrumb labels.
- Не додавати item property там, де breadcrumb не navigable.
- Підтримувати category ancestors як реальний hierarchy chain.
- Перевіряти BreadcrumbList у initial server-rendered HTML.
Висновок
У ICanUp BreadcrumbList для мене не є окремою SEO navigation system.
Visible breadcrumbs уже знають ієрархію, localized labels і public links. Structured data лише переводять цей contract у Schema.org BreadcrumbList.
Positions будуються з ordered path, navigable items використовують localized URLs, а поточна non-clickable page не отримує фейкову семантику навігації.
У результаті користувач і search engine бачать одну й ту саму site hierarchy, просто в різних representations.



