Skip to content

Commit 5ce163f

Browse files
authored
feat(docs): add BreadcrumbList JSON-LD to guide pages (supabase#45477)
## Summary Emits `BreadcrumbList` JSON-LD on every `/docs/guides/*` page served by `GuideTemplate`. Search engines and AI crawlers get an explicit hierarchical signal for the docs site (the marketing site already shipped JSON-LD via supabase#45451). The chain prepends `Docs > Guides` to the existing resolver output, so a page like `/docs/guides/auth/passwords` produces a 5-level chain with the leaf URL set per Google's spec. ## Changes - New `apps/docs/lib/breadcrumbs.ts`: pure pathname → chain resolver, server-safe. Extracted from the existing client `useBreadcrumbs` hook so the same logic runs in both contexts. - New `apps/docs/lib/json-ld.ts`: `serializeJsonLd` + `breadcrumbListSchema` mirroring `apps/www/lib/json-ld.ts`. - `Breadcrumbs.tsx` (visual) now delegates to the shared resolver — single source of truth for visual + SEO chains. - `GuideTemplate` takes a required `pathname` prop and emits `<script type="application/ld+json">` next to `<Breadcrumbs />`. Skipped when the chain is empty (e.g., page not in nav menu). Middle items without URLs (e.g., the "Auth" section root) omit `item`, matching the visual breadcrumb. - 8 explicit-prop callers updated; `[[...slug]]` callers already spread `data` (which carries `pathname`). ## Scope **Out of scope:** - `/docs/reference/*` (SDK reference) — no breadcrumbs rendered today, would need separate traversal over spec JSON. - `/guides/troubleshooting/*` — uses its own template, not `GuideTemplate`. - `TechArticle` per-page schema — high maintenance for marginal value. ## Testing (Vercel preview) ```bash curl -s https://<preview>/docs/guides/auth/passwords | grep -oE '<script type="application/ld\+json"[^>]*>[^<]+</script>' ``` Expect a script tag with the chain `Docs > Guides > Auth > Flows (How-tos) > Password-based`, leaf URL `https://supabase.com/docs/guides/auth/passwords`. - [x] `/docs/guides/auth/passwords` — 5-item chain, leaf URL present - [x] `/docs/guides/getting-started/features` — 4-item chain, all items have URLs - [x] `/docs/guides/getting-started/ai-prompts/<slug>` — special-case chain (`Getting started > AI Tools > Prompts > <slug>`), leaf URL falls back to pathname - [x] `/docs/guides/database/database-advisors` (explicit-prop caller) — chain renders - [x] Visual breadcrumb on the same pages still renders correctly - [ ] Validate output through [Google Rich Results Test](https://search.google.com/test/rich-results) on a deployed preview URL - [x] `/docs/guides/troubleshooting/<slug>` — no JSON-LD emitted (different template, intentional) ## Linear - fixes GROWTH-820 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added JSON-LD breadcrumb markup to guide pages to improve search/discovery. * **Improvements** * Centralized breadcrumb generation for consistent, accurate breadcrumbs across guides. * Multiple guide pages updated to ensure breadcrumbs and page context display correctly. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 8a22c4a commit 5ce163f

12 files changed

Lines changed: 177 additions & 82 deletions

File tree

apps/docs/app/guides/database/database-advisors/page.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
import { capitalize } from 'lodash-es'
2-
import rehypeSlug from 'rehype-slug'
3-
import { Heading } from 'ui'
4-
import { Admonition } from 'ui-patterns'
5-
61
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
72
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
83
import { MDXRemoteBase } from '~/features/docs/MdxBase'
9-
import { OCTOKIT_RETRY_OPTIONS, getGitHubFileContents, octokit } from '~/lib/octokit'
104
import { TabPanel, Tabs } from '~/features/ui/Tabs'
11-
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
5+
import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform'
126
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
137
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
148
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
9+
import { getGitHubFileContents, octokit, OCTOKIT_RETRY_OPTIONS } from '~/lib/octokit'
1510
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
11+
import { capitalize } from 'lodash-es'
12+
import rehypeSlug from 'rehype-slug'
13+
import { Heading } from 'ui'
14+
import { Admonition } from 'ui-patterns'
1615

1716
// We fetch these docs at build time from an external repo
1817
const org = 'supabase'
@@ -64,7 +63,7 @@ const DatabaseAdvisorDocs = async () => {
6463
} as SerializeOptions
6564

6665
return (
67-
<GuideTemplate meta={meta} editLink={editLink}>
66+
<GuideTemplate meta={meta} editLink={editLink} pathname="/guides/database/database-advisors">
6867
<MDXRemoteBase source={markdownIntro} />
6968
<Heading tag="h2">Available checks</Heading>
7069

apps/docs/app/guides/deployment/terraform/reference/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,11 @@ const TerraformReferencePage = async () => {
352352
const editLink = newEditLink('supabase/terraform-provider-supabase')
353353

354354
return (
355-
<GuideTemplate meta={meta} editLink={editLink}>
355+
<GuideTemplate
356+
meta={meta}
357+
editLink={editLink}
358+
pathname="/guides/deployment/terraform/reference"
359+
>
356360
The Terraform Provider provides access to{' '}
357361
<Link
358362
href="https://developer.hashicorp.com/terraform/language/resources"

apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
12
import { source } from 'common-tags'
23
import { notFound } from 'next/navigation'
3-
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
4+
45
import {
56
generateAiPromptMetadata,
67
generateAiPromptsStaticParams,
@@ -50,6 +51,7 @@ export default async function AiPromptsPage(props: { params: Promise<{ slug: str
5051
meta={{ title: `AI Prompt: ${heading}` }}
5152
content={content}
5253
editLink={newEditLink(`supabase/supabase/blob/master/examples/prompts/${slug}.md`)}
54+
pathname={`/guides/getting-started/ai-prompts/${slug}`}
5355
/>
5456
)
5557
}

apps/docs/app/guides/local-development/cli/config/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const Config = () => {
3636
const editLink = newEditLink('supabase/supabase/blob/master/apps/docs/spec/cli_v1_config.yaml')
3737

3838
return (
39-
<GuideTemplate meta={meta} editLink={editLink}>
39+
<GuideTemplate meta={meta} editLink={editLink} pathname="/guides/local-development/cli/config">
4040
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
4141
<div>{content}</div>
4242
</GuideTemplate>

apps/docs/app/guides/self-hosting/analytics/config/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Param from '~/components/Params'
2-
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
32
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
3+
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
44
import { MDXRemoteBase } from '~/features/docs/MdxBase'
55
import specAnalyticsV0 from '~/spec/analytics_v0_config.yaml' with { type: 'yml' }
66

@@ -23,6 +23,7 @@ const AnalyticsConfigPage = async () => {
2323
editLink={newEditLink(
2424
'supabase/supabase/blob/master/apps/docs/app/guides/self-hosting/analytics/config/page.tsx'
2525
)}
26+
pathname="/guides/self-hosting/analytics/config"
2627
>
2728
<MDXRemoteBase source={descriptionMdx} />
2829

apps/docs/app/guides/self-hosting/auth/config/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Param from '~/components/Params'
2-
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
32
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
3+
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
44
import { MDXRemoteBase } from '~/features/docs/MdxBase'
55
import specAuthV1 from '~/spec/gotrue_v1_config.yaml' with { type: 'yml' }
66

@@ -23,6 +23,7 @@ const AuthConfigPage = async () => {
2323
editLink={newEditLink(
2424
'supabase/supabase/blob/master/apps/docs/app/guides/self-hosting/auth/config/page.tsx'
2525
)}
26+
pathname="/guides/self-hosting/auth/config"
2627
>
2728
<MDXRemoteBase source={descriptionMdx} />
2829

apps/docs/app/guides/self-hosting/realtime/config/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Param from '~/components/Params'
2-
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
32
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
3+
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
44
import { MDXRemoteBase } from '~/features/docs/MdxBase'
55
import specRealtimeV0 from '~/spec/realtime_v0_config.yaml' with { type: 'yml' }
66

@@ -23,6 +23,7 @@ const RealtimeConfigPage = async () => {
2323
editLink={newEditLink(
2424
'supabase/supabase/blob/master/apps/docs/app/guides/self-hosting/realtime/config/page.tsx'
2525
)}
26+
pathname="/guides/self-hosting/realtime/config"
2627
>
2728
<MDXRemoteBase source={descriptionMdx} />
2829

apps/docs/app/guides/self-hosting/storage/config/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Param from '~/components/Params'
2-
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
32
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
3+
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
44
import { MDXRemoteBase } from '~/features/docs/MdxBase'
55
import specStorageV0 from '~/spec/storage_v0_config.yaml' with { type: 'yml' }
66

@@ -23,6 +23,7 @@ const StorageConfigPage = async () => {
2323
editLink={newEditLink(
2424
'supabase/supabase/blob/master/apps/docs/app/guides/self-hosting/storage/config/page.tsx'
2525
)}
26+
pathname="/guides/self-hosting/storage/config"
2627
>
2728
<MDXRemoteBase source={descriptionMdx} />
2829

apps/docs/components/Breadcrumbs.tsx

Lines changed: 6 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
'use client'
22

3+
import { resolveBreadcrumbs } from '~/lib/breadcrumbs'
4+
import { useBreakpoint } from 'common'
35
import Link from 'next/link'
46
import { usePathname, useSearchParams } from 'next/navigation'
57
import React, { Fragment, Suspense } from 'react'
6-
7-
import { useBreakpoint } from 'common'
88
import {
99
Breadcrumb_Shadcn_ as Breadcrumb,
10-
BreadcrumbList_Shadcn_ as BreadcrumbList,
10+
BreadcrumbEllipsis_Shadcn_ as BreadcrumbEllipsis,
1111
BreadcrumbItem_Shadcn_ as BreadcrumbItem,
1212
BreadcrumbLink_Shadcn_ as BreadcrumbLink,
13-
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
13+
BreadcrumbList_Shadcn_ as BreadcrumbList,
1414
BreadcrumbPage_Shadcn_ as BreadcrumbPage,
15-
BreadcrumbEllipsis_Shadcn_ as BreadcrumbEllipsis,
15+
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
1616
Button,
1717
cn,
1818
Drawer,
@@ -26,9 +26,6 @@ import {
2626
DropdownMenuTrigger,
2727
} from 'ui'
2828

29-
import * as NavItems from '~/components/Navigation/NavigationMenu/NavigationMenu.constants'
30-
import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils'
31-
3229
interface BreadcrumbsProps extends React.HTMLAttributes<HTMLDivElement> {
3330
minLength?: number
3431
forceDisplayOnMobile?: boolean
@@ -159,56 +156,5 @@ const BreadcrumbsInternal = ({
159156

160157
function useBreadcrumbs() {
161158
const pathname = usePathname()
162-
163-
const isTroubleshootingPage = pathname.startsWith('/guides/troubleshooting')
164-
if (isTroubleshootingPage) {
165-
const breadcrumbs = [{ name: 'Troubleshooting', url: '/guides/troubleshooting' }]
166-
return breadcrumbs
167-
}
168-
169-
const isAiPromptsPage = pathname.startsWith('/guides/getting-started/ai-prompts')
170-
if (isAiPromptsPage) {
171-
const breadcrumbs = [
172-
{ name: 'Getting started', url: '/guides/getting-started' },
173-
{ name: 'AI Tools' },
174-
{ name: 'Prompts', url: '/guides/getting-started/ai-prompts' },
175-
]
176-
return breadcrumbs
177-
}
178-
179-
// TODO: Breadcrumbs currently can't infer the "AI Tools" parent for /guides/getting-started/ai-* routes,
180-
// so we special-case these paths here. Remove when Breadcrumbs can derive this hierarchy from NavigationMenu.
181-
const isAiSkillsPage = pathname.startsWith('/guides/getting-started/ai-skills')
182-
if (isAiSkillsPage) {
183-
const breadcrumbs = [
184-
{ name: 'Getting started', url: '/guides/getting-started' },
185-
{ name: 'AI Tools' },
186-
{ name: 'Agent Skills', url: '/guides/getting-started/ai-skills' },
187-
]
188-
return breadcrumbs
189-
}
190-
191-
const menuId = getMenuId(pathname)
192-
const menu = NavItems[menuId]
193-
return findMenuItemByUrl(menu, pathname, [])
194-
}
195-
196-
function findMenuItemByUrl(menu: any, targetUrl: string, parents: any[] = []) {
197-
// If the menu has items, recursively search through them
198-
if (menu.items) {
199-
for (let item of menu.items) {
200-
const result = findMenuItemByUrl(item, targetUrl, [...parents, menu])
201-
if (result) {
202-
return result
203-
}
204-
}
205-
}
206-
207-
// Check if the current menu object itself has the target URL
208-
if (menu.url === targetUrl) {
209-
return [...parents, menu]
210-
}
211-
212-
// If the URL is not found, return null
213-
return null
159+
return resolveBreadcrumbs(pathname)
214160
}

apps/docs/features/docs/GuidesMdx.template.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { ExternalLink } from 'lucide-react'
2-
import { type ReactNode } from 'react'
3-
import ReactMarkdown from 'react-markdown'
4-
5-
import { cn } from 'ui'
6-
71
import Breadcrumbs from '~/components/Breadcrumbs'
82
import GuidesSidebar from '~/components/GuidesSidebar'
93
import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
104
import { MDXRemoteBase } from '~/features/docs/MdxBase'
115
import type { WithRequired } from '~/features/helpers.types'
6+
import { resolveBreadcrumbs } from '~/lib/breadcrumbs'
127
import { type GuideFrontmatter } from '~/lib/docs'
8+
import { breadcrumbListSchema, serializeJsonLd } from '~/lib/json-ld'
139
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
10+
import { ExternalLink } from 'lucide-react'
11+
import { type ReactNode } from 'react'
12+
import ReactMarkdown from 'react-markdown'
13+
import { cn } from 'ui'
1414

1515
const EDIT_LINK_SYMBOL = Symbol('edit link')
1616
interface EditLink {
@@ -54,14 +54,27 @@ interface BaseGuideTemplateProps {
5454
children?: ReactNode
5555
editLink: EditLink
5656
mdxOptions?: SerializeOptions
57+
pathname: `/${string}`
5758
}
5859

5960
type GuideTemplateProps =
6061
| WithRequired<BaseGuideTemplateProps, 'children'>
6162
| WithRequired<BaseGuideTemplateProps, 'content'>
6263

63-
const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideTemplateProps) => {
64+
const GuideTemplate = ({
65+
meta,
66+
content,
67+
children,
68+
editLink,
69+
mdxOptions,
70+
pathname,
71+
}: GuideTemplateProps) => {
6472
const hideToc = meta?.hideToc || meta?.hide_table_of_contents
73+
const breadcrumbChain = resolveBreadcrumbs(pathname)
74+
const breadcrumbJsonLd =
75+
breadcrumbChain.length > 0
76+
? serializeJsonLd(breadcrumbListSchema({ pathname, chain: breadcrumbChain }))
77+
: null
6578

6679
return (
6780
<TocAnchorsProvider>
@@ -74,6 +87,12 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
7487
'col-span-12 md:col-span-9'
7588
)}
7689
>
90+
{breadcrumbJsonLd && (
91+
<script
92+
type="application/ld+json"
93+
dangerouslySetInnerHTML={{ __html: breadcrumbJsonLd }}
94+
/>
95+
)}
7796
<Breadcrumbs className="mb-2" />
7897
<article
7998
// Used to get headings for the table of contents

0 commit comments

Comments
 (0)