mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 14:05:05 +08: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`.
247 lines
8.4 KiB
TypeScript
247 lines
8.4 KiB
TypeScript
import CTABanner from '~/components/CTABanner'
|
|
import DefaultLayout from '~/components/Layouts/Default'
|
|
import { breadcrumbs } from '~/lib/breadcrumbs'
|
|
import { SITE_ORIGIN } from '~/lib/constants'
|
|
import { breadcrumbListSchema, serializeJsonLd } from '~/lib/json-ld'
|
|
import mdxComponents from '~/lib/mdx/mdxComponents'
|
|
import { mdxSerialize } from '~/lib/mdx/mdxSerialize'
|
|
import { getAllPostSlugs, getPostdata, getSortedPosts } from '~/lib/posts'
|
|
import matter from 'gray-matter'
|
|
import { ChevronLeft, ChevronRight, ExternalLink } from 'lucide-react'
|
|
import { MDXRemote } from 'next-mdx-remote'
|
|
import { NextSeo } from 'next-seo'
|
|
import Head from 'next/head'
|
|
import Image from 'next/image'
|
|
import Link from 'next/link'
|
|
import { Button } from 'ui'
|
|
|
|
// table of contents extractor
|
|
const toc = require('markdown-toc')
|
|
|
|
export async function getStaticPaths() {
|
|
const paths = getAllPostSlugs('_customers')
|
|
return {
|
|
paths,
|
|
fallback: false,
|
|
}
|
|
}
|
|
|
|
export async function getStaticProps({ params }: any) {
|
|
const filePath = `${params.slug}`
|
|
const postContent = await getPostdata(filePath, '_customers')
|
|
const { data, content } = matter(postContent)
|
|
const mdxSource: any = await mdxSerialize(content)
|
|
|
|
const relatedPosts = getSortedPosts({
|
|
directory: '_customers',
|
|
limit: 5,
|
|
tags: mdxSource.scope.tags,
|
|
currentPostSlug: filePath,
|
|
})
|
|
|
|
const allPosts = getSortedPosts({ directory: '_customers' })
|
|
const currentIndex = allPosts
|
|
.map(function (e) {
|
|
return e.slug
|
|
})
|
|
.indexOf(filePath)
|
|
const nextPost = allPosts[currentIndex + 1]
|
|
const prevPost = allPosts[currentIndex - 1]
|
|
const payload = {
|
|
props: {
|
|
prevPost: currentIndex === 0 ? null : prevPost ? prevPost : null,
|
|
nextPost: currentIndex === allPosts.length ? null : nextPost ? nextPost : null,
|
|
relatedPosts,
|
|
blog: {
|
|
slug: `${params.slug}`,
|
|
content: mdxSource,
|
|
source: content,
|
|
...data,
|
|
toc: toc(content, { maxdepth: data.toc_depth ? data.toc_depth : 2 }),
|
|
},
|
|
},
|
|
}
|
|
return payload
|
|
}
|
|
|
|
function CaseStudyPage(props: any) {
|
|
const {
|
|
about,
|
|
company_url,
|
|
content,
|
|
date,
|
|
description,
|
|
logo,
|
|
meta_description,
|
|
meta_title,
|
|
misc,
|
|
name,
|
|
slug,
|
|
title,
|
|
} = props.blog
|
|
|
|
const ogImageUrl = encodeURI(
|
|
`${process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:54321' : 'https://obuldanrptloktxcffvn.supabase.co'}/functions/v1/og-images?site=customers&customer=${slug}&title=${meta_title ?? title}`
|
|
)
|
|
|
|
const meta = {
|
|
title: meta_title ?? `${name} | Supabase Customer Stories`,
|
|
description: meta_description ?? description,
|
|
image: ogImageUrl ?? `${SITE_ORIGIN}/images/customers/og/customer-stories.jpg`,
|
|
url: `${SITE_ORIGIN}/customers/${slug}`,
|
|
}
|
|
|
|
const breadcrumbItems = [
|
|
...breadcrumbs.customersIndex,
|
|
{ name: meta_title ?? title, url: `https://supabase.com/customers/${slug}` },
|
|
]
|
|
|
|
return (
|
|
<>
|
|
<NextSeo
|
|
title={meta.title}
|
|
openGraph={{
|
|
title: meta.title,
|
|
description: meta.description,
|
|
url: meta.url,
|
|
type: 'article',
|
|
article: {
|
|
//
|
|
// to do: add expiration and modified dates
|
|
// https://github.com/garmeeh/next-seo#article
|
|
publishedTime: date,
|
|
},
|
|
images: [
|
|
{
|
|
url: meta.image,
|
|
alt: `${meta.title} thumbnail`,
|
|
},
|
|
],
|
|
}}
|
|
/>
|
|
<Head>
|
|
<script
|
|
type="application/ld+json"
|
|
dangerouslySetInnerHTML={{
|
|
__html: serializeJsonLd(breadcrumbListSchema(breadcrumbItems)),
|
|
}}
|
|
/>
|
|
</Head>
|
|
<DefaultLayout>
|
|
<div
|
|
className="
|
|
container mx-auto p-8 sm:py-16 sm:px-16
|
|
xl:px-20
|
|
"
|
|
>
|
|
<div className="grid grid-cols-12 gap-4">
|
|
<div className="hidden xl:block col-span-12 mb-2 xl:col-span-2">
|
|
{/* Back button */}
|
|
<Link
|
|
href="/customers"
|
|
className="text-foreground-lighter hover:text-foreground flex cursor-pointer items-center text-sm transition"
|
|
>
|
|
<ChevronLeft style={{ padding: 0 }} />
|
|
Back
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="col-span-12 lg:col-span-8">
|
|
<div>
|
|
<article className="flex flex-col gap-8">
|
|
<div className="flex flex-col gap-4 sm:gap-8 max-w-xxl">
|
|
<Link
|
|
href="/customers"
|
|
className="text-brand hover:text-brand-600 sm:mb-2 mt-0"
|
|
>
|
|
Customer Stories
|
|
</Link>
|
|
<h1 className="text-foreground text-4xl font-semibold xl:text-5xl">{title}</h1>
|
|
<p className="text-foreground text-xl xl:text-2xl">{description}</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-12 prose max-w-none gap-8 lg:gap-20">
|
|
<div className="col-span-12 lg:col-span-4 lg:block xl:col-span-4">
|
|
<div className="space-y-8 lg:sticky lg:top-24 lg:mb-24">
|
|
{/* Logo */}
|
|
<div className="relative h-16 w-32 lg:mt-5">
|
|
<Image
|
|
fill
|
|
src={logo}
|
|
alt={`${title} logo`}
|
|
priority
|
|
placeholder="blur"
|
|
blurDataURL="/images/blur.png"
|
|
draggable={false}
|
|
className="
|
|
bg-no-repeat
|
|
object-left
|
|
object-contain
|
|
m-0
|
|
|
|
in-data-[theme*=dark]:brightness-200
|
|
in-data-[theme*=dark]:contrast-0
|
|
in-data-[theme*=dark]:filter
|
|
"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col space-y-2">
|
|
<span className="text-foreground-lighter">About</span>
|
|
<p>{about}</p>
|
|
{company_url && (
|
|
<span className="not-prose ">
|
|
<a
|
|
href={company_url}
|
|
className="flex cursor-pointer items-center space-x-1 transition-opacity text-foreground-lightround-ligtext-foreground-light:text-foreground-light"
|
|
target="_blank"
|
|
>
|
|
<span>{company_url}</span>
|
|
<ExternalLink size={14} />
|
|
</a>
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{misc?.map((x: any) => {
|
|
return (
|
|
<div className="flex flex-col gap-0">
|
|
<span className="text-foreground-lighter">{x.label}</span>
|
|
<span className="text-foreground-light">{x.text}</span>
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
<div>
|
|
<p>Ready to get started?</p>
|
|
<div>
|
|
<Button asChild type="default" iconRight={<ChevronRight />}>
|
|
<Link
|
|
href="https://supabase.com/contact/enterprise"
|
|
className="no-underline"
|
|
>
|
|
Contact sales
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="xm:col-span-7 col-span-12 lg:col-span-8 xl:col-span-8 ">
|
|
<MDXRemote {...content} components={mdxComponents()} />
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<CTABanner />
|
|
</DefaultLayout>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default CaseStudyPage
|