mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## Summary
- Adds `breadcrumbListSchema(items)` helper to `apps/www/lib/json-ld.ts`
and a hand-curated `apps/www/lib/breadcrumbs.ts` route map.
- Wires inline `<script type="application/ld+json">` BreadcrumbList
blocks into 18 marketing surfaces: blog (index + slug), customers (index
+ slug), events (index + slug), 5 product pages (database, auth,
storage, edge-functions, realtime), 3 modules (vector, cron, queues),
pricing, careers, company, features.
- Pages router callers wrap the script in `<Head>`; app router callers
place it directly in JSX. Dynamic surfaces append a leaf at render time
using the page's title (`frontmatter.title` for blog, `meta_title ??
title` for customers, `event.meta_title ?? event.title` for events).
- Modules sit at `Home > {Name}` since no `/modules` index page exists;
products sit at `Home > {Product}` (no shared products parent). Absolute
`https://supabase.com` URLs match the existing `CANONICAL_ORIGIN`
convention so anchors stay stable across Vercel previews.
Linear:
[GROWTH-822](https://linear.app/supabase/issue/GROWTH-822/add-breadcrumblist-json-ld-to-www-marketing-surfaces)
(sub-issue under
[GROWTH-724](https://linear.app/supabase/issue/GROWTH-724)).
> **Note on branch name:** the branch is
`pamela/growth-820-www-breadcrumb-jsonld`; the actual Linear issue is
GROWTH-822. The branch was named before the sub-issue was created.
Ignore the `820` in the branch.
Explicitly deferred (separate PRs / low SEO ROI): `/launch-week/*`,
`/solutions/*`, `/partners/*`, `/alternatives/*`, `/changelog`,
`/legal/dpa`, `/aws-reinvent-2025`, `/wrapped`, `/contribute/*`,
`/brand-assets`, `/ga`, `/ga-week`, `/state-of-startups*`, and the
homepage (Organization + WebSite already cover homepage entity signals;
single-item BreadcrumbList is ignored by Google).
## Test plan
- [x] On the Vercel preview, `curl -s https://<preview>/database | grep
'"BreadcrumbList"'` returns the script block with `Home > Database`.
- [x] `curl -s https://<preview>/blog/<recent-slug> | grep
'"BreadcrumbList"'` returns `Home > Blog > {post title}`.
- [x] `curl -s https://<preview>/customers/<slug> | grep
'"BreadcrumbList"'` returns `Home > Customer Stories > {customer
title}`.
- [x] `curl -s https://<preview>/events/<slug> | grep
'"BreadcrumbList"'` returns `Home > Events > {event title}`.
- [x] `curl -s https://<preview>/modules/vector | grep
'"BreadcrumbList"'` returns `Home > Vector`.
141 lines
3.4 KiB
TypeScript
141 lines
3.4 KiB
TypeScript
import { DEFAULT_META_DESCRIPTION, SITE_NAME } from './constants'
|
|
|
|
const CANONICAL_ORIGIN = 'https://supabase.com'
|
|
const ORG_ID = `${CANONICAL_ORIGIN}/#organization`
|
|
const SITE_ID = `${CANONICAL_ORIGIN}/#website`
|
|
|
|
const ORG_SAMEAS = [
|
|
'https://github.com/supabase',
|
|
'https://twitter.com/supabase',
|
|
'https://www.linkedin.com/company/supabase',
|
|
'https://youtube.com/c/supabase',
|
|
'https://discord.supabase.com/',
|
|
'https://www.tiktok.com/@supabase.com',
|
|
'https://www.instagram.com/supabasecom',
|
|
]
|
|
|
|
const ORG_LOGO_URL = `${CANONICAL_ORIGIN}/images/og/supabase-og.png`
|
|
|
|
type JsonLdSchema = Record<string, unknown> | Record<string, unknown>[]
|
|
|
|
export function serializeJsonLd(schema: JsonLdSchema): string {
|
|
return JSON.stringify(schema)
|
|
.replace(/</g, '\\u003c')
|
|
.replace(/>/g, '\\u003e')
|
|
.replace(/&/g, '\\u0026')
|
|
}
|
|
|
|
interface OrganizationSchemaInput {
|
|
description?: string
|
|
}
|
|
|
|
export function organizationSchema(input: OrganizationSchemaInput = {}) {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Organization',
|
|
'@id': ORG_ID,
|
|
name: SITE_NAME,
|
|
url: CANONICAL_ORIGIN,
|
|
logo: {
|
|
'@type': 'ImageObject',
|
|
url: ORG_LOGO_URL,
|
|
},
|
|
description: input.description ?? DEFAULT_META_DESCRIPTION,
|
|
sameAs: ORG_SAMEAS,
|
|
}
|
|
}
|
|
|
|
interface WebSiteSchemaInput {
|
|
description?: string
|
|
}
|
|
|
|
export function websiteSchema(input: WebSiteSchemaInput = {}) {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'WebSite',
|
|
'@id': SITE_ID,
|
|
url: CANONICAL_ORIGIN,
|
|
name: SITE_NAME,
|
|
description: input.description ?? DEFAULT_META_DESCRIPTION,
|
|
publisher: { '@id': ORG_ID },
|
|
}
|
|
}
|
|
|
|
interface SoftwareApplicationSchemaInput {
|
|
name: string
|
|
description: string
|
|
url: string
|
|
image: string
|
|
applicationCategory?: string
|
|
}
|
|
|
|
export function softwareApplicationSchema(input: SoftwareApplicationSchemaInput) {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'SoftwareApplication',
|
|
name: input.name,
|
|
description: input.description,
|
|
url: input.url,
|
|
image: input.image,
|
|
applicationCategory: input.applicationCategory ?? 'DeveloperApplication',
|
|
operatingSystem: 'Cross-platform',
|
|
publisher: { '@id': ORG_ID },
|
|
}
|
|
}
|
|
|
|
export interface BreadcrumbItem {
|
|
name: string
|
|
url: string
|
|
}
|
|
|
|
export function breadcrumbListSchema(items: BreadcrumbItem[]) {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'BreadcrumbList',
|
|
itemListElement: items.map((item, index) => ({
|
|
'@type': 'ListItem',
|
|
position: index + 1,
|
|
name: item.name,
|
|
item: item.url,
|
|
})),
|
|
}
|
|
}
|
|
|
|
interface BlogPostingSchemaInput {
|
|
url: string
|
|
headline: string
|
|
description?: string
|
|
image: string
|
|
datePublished: string
|
|
authors: Array<{ name: string; url?: string }>
|
|
}
|
|
|
|
export function blogPostingSchema(input: BlogPostingSchemaInput) {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'BlogPosting',
|
|
mainEntityOfPage: {
|
|
'@type': 'WebPage',
|
|
'@id': input.url,
|
|
},
|
|
headline: input.headline,
|
|
description: input.description,
|
|
image: input.image,
|
|
datePublished: input.datePublished,
|
|
author: input.authors.map((a) => ({
|
|
'@type': 'Person',
|
|
name: a.name,
|
|
...(a.url ? { url: a.url } : {}),
|
|
})),
|
|
publisher: {
|
|
'@type': 'Organization',
|
|
'@id': ORG_ID,
|
|
name: SITE_NAME,
|
|
logo: {
|
|
'@type': 'ImageObject',
|
|
url: ORG_LOGO_URL,
|
|
},
|
|
},
|
|
}
|
|
}
|