From ebb0c8084eaba81f21e537db5aa2591d8dc21efc Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Nov 2026 11:59:17 +0100 Subject: [PATCH] feat: breadcrumbs component - (UI-SPEC L1): the resolve answer's trail as nav > ol with server-rendered links and the last item as aria-current text; nothing with fewer than two items, with page meta seo.breadcrumbs false or with the option seo.breadcrumbs false; props items, label, hideCurrent, locale - one trail per page: the first rendered instance claims it (seoClaimBreadcrumbs, kept per path) - seoSitePath: same-origin CMS URLs become paths, non-http(s) URLs are never links; useSeoLabels takes an optional locale - L1 default styles with CSS-generated separators --- .playground/app/app.vue | 3 ++ .playground/app/pages/[...slug].vue | 4 +- app/assets/css/seo-base.css | 34 +++++++++++- app/components/Seo/Breadcrumbs.vue | 80 +++++++++++++++++++++++++++++ app/composables/useSeoLabels.ts | 14 +++-- app/utils/breadcrumbClaim.ts | 27 ++++++++++ app/utils/sitePath.ts | 26 ++++++++++ testing/expect-seo.txt | 7 +++ tests/breadcrumbClaim.test.ts | 33 ++++++++++++ 9 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 app/components/Seo/Breadcrumbs.vue create mode 100644 app/utils/breadcrumbClaim.ts create mode 100644 app/utils/sitePath.ts create mode 100644 tests/breadcrumbClaim.test.ts diff --git a/.playground/app/app.vue b/.playground/app/app.vue index 8f62b8b..4ef9b10 100644 --- a/.playground/app/app.vue +++ b/.playground/app/app.vue @@ -1,3 +1,6 @@ diff --git a/.playground/app/pages/[...slug].vue b/.playground/app/pages/[...slug].vue index bf6a46f..a85a9d4 100644 --- a/.playground/app/pages/[...slug].vue +++ b/.playground/app/pages/[...slug].vue @@ -3,7 +3,8 @@ // a 404 for anything else, as a registry page would. It sets its own title // so the smoke can see the CMS title win, adds a layer node to the one // JSON-LD graph (merged by @id with the CMS's nodes) and prints a label of -// the page's content locale. +// the page's content locale. It also renders breadcrumbs, as a layer page +// does, next to the instance in app.vue: only one trail renders. const route = useRoute() const answer = useSeoAnswer() if (answer.value?.action !== 'ok') throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true }) @@ -17,6 +18,7 @@ const slug = computed(() => ([] as string[]).concat(route.params.slug ?? []).joi