Files
supabase/apps/www/app/blog/page.tsx
Pamela Chia c26b64a033 feat(www): emit BreadcrumbList JSON-LD on marketing surfaces (#45478)
## 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`.
2026-05-05 23:57:53 +08:00

68 lines
2.4 KiB
TypeScript

import FeaturedThumb from 'components/Blog/FeaturedThumb'
import DefaultLayout from 'components/Layouts/Default'
import type { Metadata } from 'next'
import type PostTypes from 'types/post'
import BlogClient from './BlogClient'
import { breadcrumbs } from '@/lib/breadcrumbs'
import { breadcrumbListSchema, serializeJsonLd } from '@/lib/json-ld'
import { getSortedPosts } from '@/lib/posts'
export const revalidate = 30
export const metadata: Metadata = {
title: 'Supabase Blog: the Postgres development platform',
description: 'Get all your Supabase News on the Supabase blog.',
openGraph: {
title: 'Supabase Blog: the Postgres development platform',
description: 'Get all your Supabase News on the Supabase blog.',
url: 'https://supabase.com/blog',
images: [{ url: 'https://supabase.com/images/og/supabase-og.png' }],
},
}
const INITIAL_POSTS_LIMIT = 25
export default async function BlogPage() {
const staticPostsData = getSortedPosts({ directory: '_blog', runner: '** BLOG PAGE **' })
const allPosts = [...staticPostsData].sort((a, b) => {
const dateA = new Date(a.date || a.formattedDate).getTime()
const dateB = new Date(b.date || b.formattedDate).getTime()
return dateB - dateA
})
const initialPosts = allPosts.slice(0, INITIAL_POSTS_LIMIT)
const featuredPost = initialPosts[0]
// Featured post is rendered as the hero above the list, so exclude it from
// the list to avoid showing it twice. BlogClient compensates the API offset
// when scrolling for more posts.
const listPosts = initialPosts.slice(1)
const totalListPosts = Math.max(0, allPosts.length - 1)
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: serializeJsonLd(breadcrumbListSchema(breadcrumbs.blogIndex)),
}}
/>
<DefaultLayout>
<h1 className="sr-only">Supabase blog</h1>
<div className="container relative mx-auto px-4 py-4 md:py-8 xl:py-10 sm:px-16 xl:px-20">
{featuredPost && (
<FeaturedThumb key={featuredPost.slug} {...(featuredPost as PostTypes)} />
)}
</div>
<div className="border-default border-t">
<div className="container mx-auto px-4 py-4 md:py-8 xl:py-10 sm:px-16 xl:px-20">
<BlogClient initialBlogs={listPosts} totalPosts={totalListPosts} />
</div>
</div>
</DefaultLayout>
</>
)
}