mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(docs): always emit BreadcrumbList item field (#45744)
## Summary Eliminates the Google Search Console "Missing field 'item' (in 'itemListElement')" critical error on 230 `/docs/guides/*` pages. The schema was emitting `ListItem`s without an `item` field for intermediate category nodes that lack a URL in the docs nav. Per [Google's spec](https://developers.google.com/search/docs/appearance/structured-data/breadcrumb), `item` is required on every BreadcrumbList position except the last leaf — so url-less items are filtered out instead. Also fixes a smaller quality gap surfaced during preview verification: the `auth` section root in `NavigationMenu.constants.ts` was missing a `url`, so auth trails were dropping the "Auth" breadcrumb level (`Docs > Guides > JSON Web Tokens (JWT) > Overview` instead of `Docs > Guides > Auth > JSON Web Tokens (JWT) > Overview`). Every other section root already has a `url`; auth was the lone outlier. ## Testing Tested locally via vitest (`pnpm --filter docs exec vitest run lib/json-ld.test.ts`): - [x] All-urls chain: every `itemListElement` has string `item` and `name` - [x] Leaf-url-mismatch: leaf uses `pathname` even when the chain leaf URL differs - [x] All-url-less chain: returns `null` - [x] Empty chain: returns `null` Tested on the preview deploy against 7 representative GSC-flagged paths: - [x] `/docs/guides/getting-started/ai-prompts` — 4 positions, 0 missing - [x] `/docs/guides/getting-started/ai-skills` — 4 positions, 0 missing - [x] `/docs/guides/auth/jwts` — 4 positions, 0 missing (after auth fix: includes "Auth") - [x] `/docs/guides/auth/social-login/auth-google` — 4 positions, 0 missing (after auth fix: includes "Auth") - [x] `/docs/guides/database/postgres-js` — 4 positions, 0 missing - [x] `/docs/guides/storage/quickstart` — 4 positions, 0 missing - [x] `/docs/guides/platform/migrating-within-supabase/dashboard-restore` — 5 positions, 0 missing Post-merge: - [ ] validator.schema.org against deployed URL: 0 errors - [ ] GSC "Validate fix" on the breadcrumb issue (1-2 week re-crawl window) ## Linear - fixes GROWTH-835 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved breadcrumb validation to filter incomplete entries and avoid broken documentation links. * Restored root link for the Auth navigation section so the Auth menu item now navigates to /guides/auth. * **Tests** * Added comprehensive tests covering breadcrumb generation and edge cases. * **Refactor** * Streamlined breadcrumb JSON‑LD schema generation for clearer output and maintainability. [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45744) <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
dc0cc42d14
commit
5a5099adba
4 files changed
+95
-14
No files matched your search
@@ -684,6 +684,7 @@ export const PhoneLoginsItems = [
|
||||
export const auth: NavMenuConstant = {
|
||||
icon: 'auth',
|
||||
title: 'Auth',
|
||||
url: '/guides/auth',
|
||||
items: [
|
||||
{
|
||||
name: 'Overview',
|
||||
|
||||
@@ -71,10 +71,8 @@ const GuideTemplate = ({
|
||||
}: GuideTemplateProps) => {
|
||||
const hideToc = meta?.hideToc || meta?.hide_table_of_contents
|
||||
const breadcrumbChain = resolveBreadcrumbs(pathname)
|
||||
const breadcrumbJsonLd =
|
||||
breadcrumbChain.length > 0
|
||||
? serializeJsonLd(breadcrumbListSchema({ pathname, chain: breadcrumbChain }))
|
||||
: null
|
||||
const breadcrumbSchema = breadcrumbListSchema({ pathname, chain: breadcrumbChain })
|
||||
const breadcrumbJsonLd = breadcrumbSchema ? serializeJsonLd(breadcrumbSchema) : null
|
||||
|
||||
return (
|
||||
<TocAnchorsProvider>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { breadcrumbListSchema } from './json-ld'
|
||||
|
||||
describe('breadcrumbListSchema', () => {
|
||||
let warn: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
it('emits item and name on every position when all chain items have urls', () => {
|
||||
const result = breadcrumbListSchema({
|
||||
pathname: '/guides/auth/jwts',
|
||||
chain: [
|
||||
{ name: 'Authentication', url: '/guides/auth' },
|
||||
{ name: 'JWTs', url: '/guides/auth/jwts' },
|
||||
],
|
||||
})
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.itemListElement).toHaveLength(4)
|
||||
for (const entry of result!.itemListElement) {
|
||||
expect(typeof entry.item).toBe('string')
|
||||
expect(entry.item).toMatch(/^https?:\/\//)
|
||||
expect(typeof entry.name).toBe('string')
|
||||
}
|
||||
})
|
||||
|
||||
it('uses pathname for the leaf url even when chain leaf url differs', () => {
|
||||
const result = breadcrumbListSchema({
|
||||
pathname: '/guides/database/postgres-js',
|
||||
chain: [{ name: 'Postgres.js', url: '/guides/database/postgres-js-old' }],
|
||||
})
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
const leaf = result!.itemListElement.at(-1)
|
||||
expect(leaf?.item).toMatch(/\/guides\/database\/postgres-js$/)
|
||||
})
|
||||
|
||||
it('returns null when every chain item is url-less', () => {
|
||||
const result = breadcrumbListSchema({
|
||||
pathname: '/guides/some-broken-route',
|
||||
chain: [{ name: 'Category A' }, { name: 'Category B' }],
|
||||
})
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null on an empty chain', () => {
|
||||
const result = breadcrumbListSchema({ pathname: '/guides', chain: [] })
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
+33
-10
@@ -10,31 +10,54 @@ export function serializeJsonLd(schema: JsonLdSchema): string {
|
||||
.replace(/&/g, '\\u0026')
|
||||
}
|
||||
|
||||
const DOCS_ROOT: BreadcrumbItem = { name: 'Docs', url: '' }
|
||||
const GUIDES_ROOT: BreadcrumbItem = { name: 'Guides', url: '/guides' }
|
||||
type ValidCrumb = BreadcrumbItem & { url: string }
|
||||
|
||||
const DOCS_ROOT: ValidCrumb = { name: 'Docs', url: '' }
|
||||
const GUIDES_ROOT: ValidCrumb = { name: 'Guides', url: '/guides' }
|
||||
|
||||
interface BreadcrumbListSchemaInput {
|
||||
pathname: string
|
||||
chain: BreadcrumbItem[]
|
||||
}
|
||||
|
||||
const warnedPaths = new Set<string>()
|
||||
|
||||
function isValidCrumb(crumb: BreadcrumbItem): crumb is ValidCrumb {
|
||||
return crumb.url !== undefined && Boolean(crumb.title ?? crumb.name)
|
||||
}
|
||||
|
||||
export function breadcrumbListSchema({ pathname, chain }: BreadcrumbListSchemaInput) {
|
||||
const fullChain: BreadcrumbItem[] = [DOCS_ROOT, GUIDES_ROOT, ...chain]
|
||||
const filteredChain = chain.filter(isValidCrumb)
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
filteredChain.length !== chain.length &&
|
||||
!warnedPaths.has(pathname)
|
||||
) {
|
||||
warnedPaths.add(pathname)
|
||||
const dropped = chain
|
||||
.filter((crumb) => !isValidCrumb(crumb))
|
||||
.map((crumb) => crumb.title ?? crumb.name ?? '<unnamed>')
|
||||
console.warn(
|
||||
`[json-ld] Dropping breadcrumb items missing url or name from ${pathname}: ${dropped.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
if (filteredChain.length === 0) return null
|
||||
|
||||
const fullChain: ValidCrumb[] = [DOCS_ROOT, GUIDES_ROOT, ...filteredChain]
|
||||
|
||||
const itemListElement = fullChain.map((crumb, index) => {
|
||||
const isLeaf = index === fullChain.length - 1
|
||||
const name = crumb.title ?? crumb.name
|
||||
const path = isLeaf ? pathname : crumb.url
|
||||
const itemUrl = path === '' ? PROD_URL : `${PROD_URL}${path}`
|
||||
|
||||
const listItem: Record<string, unknown> = {
|
||||
return {
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name,
|
||||
name: crumb.title ?? crumb.name,
|
||||
item: itemUrl,
|
||||
}
|
||||
if (path !== undefined) {
|
||||
listItem.item = path === '' ? PROD_URL : `${PROD_URL}${path}`
|
||||
}
|
||||
return listItem
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user