feat(www): add JSON-LD structured data to homepage, products, and blog (#45451)

This commit is contained in:
Pamela Chia authored and GitHub committed 2026-05-01 23:55:32 +09:00
1 parent 50ccd39b31
commit e2480538ad
9 files changed
+297 -30

No files matched your search

+40 -2
View File
@@ -2,11 +2,27 @@ import type { Metadata } from 'next'
import { draftMode } from 'next/headers'
import BlogPostClient from './BlogPostClient'
import { getAbsoluteBlogSocialImage } from '@/lib/blog-images'
import authors from '@/lib/authors.json'
import {
BLOG_PLACEHOLDER_IMAGE,
getAbsoluteBlogSocialImage,
toAbsoluteBlogImageUrl,
} from '@/lib/blog-images'
import { SITE_ORIGIN } from '@/lib/constants'
import { blogPostingSchema, serializeJsonLd } from '@/lib/json-ld'
import { getAllPostSlugs, getPostdata, getSortedPosts } from '@/lib/posts'
import type { Blog, BlogData, PostReturnType } from '@/types/post'
function resolveBlogAuthors(authorField: string | undefined) {
return (authorField ?? '')
.split(',')
.map((id) => id.trim())
.filter(Boolean)
.map((id) => authors.find((a) => a.author_id === id))
.filter((a): a is (typeof authors)[number] => Boolean(a))
.map((a) => ({ name: a.author, url: a.author_url }))
}
export const revalidate = 30
type MatterReturn = {
@@ -138,6 +154,28 @@ export default async function BlogPostPage({ params }: { params: Promise<Params>
isDraftMode: isDraft,
}
return <BlogPostClient {...props} />
const frontmatter = parsedContent.data
const blogAuthors = resolveBlogAuthors(frontmatter.author)
const imageUrl =
getAbsoluteBlogSocialImage(frontmatter, SITE_ORIGIN) ??
toAbsoluteBlogImageUrl(BLOG_PLACEHOLDER_IMAGE, SITE_ORIGIN)
const blogJsonLd = blogPostingSchema({
url: `${SITE_ORIGIN}/blog/${slug}`,
headline: frontmatter.title,
description: frontmatter.description,
image: imageUrl,
datePublished: frontmatter.date,
authors: blogAuthors.length > 0 ? blogAuthors : [{ name: 'Supabase' }],
})
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: serializeJsonLd(blogJsonLd) }}
/>
<BlogPostClient {...props} />
</>
)
} catch {}
}
+122
View File
@@ -0,0 +1,122 @@
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 },
}
}
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,
},
},
}
}
+17
View File
@@ -2,6 +2,7 @@ import { useBreakpoint } from 'common'
import { ArrowUpRight, Briefcase, Eye, Link as IconLink, Shield } from 'lucide-react'
import { NextSeo } from 'next-seo'
import dynamic from 'next/dynamic'
import Head from 'next/head'
import NextImage from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
@@ -18,6 +19,7 @@ import AuthProviders from '@/data/auth.json'
import MainProducts from '@/data/MainProducts'
import ApiExamples from '@/data/products/auth/auth-api-examples'
import AuthSqlRulesExamples from '@/data/products/auth/auth-sql-rules-examples'
import { serializeJsonLd, softwareApplicationSchema } from '@/lib/json-ld'
const SplitCodeBlockCarousel = dynamic(
() => import('~/components/Carousels/SplitCodeBlockCarousel')
@@ -54,6 +56,21 @@ function AuthPage() {
],
}}
/>
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: serializeJsonLd(
softwareApplicationSchema({
name: 'Supabase Auth',
description: meta_description,
url: 'https://supabase.com/auth',
image: `https://supabase.com${basePath}/images/product/auth/auth-og.jpg`,
})
),
}}
/>
</Head>
<DefaultLayout>
<ProductsNav activePage={PRODUCT_NAMES.AUTHENTICATION} />
<ProductHeader
+17
View File
@@ -4,6 +4,7 @@ import 'swiper/css'
import { ArrowUpRight, X } from 'lucide-react'
import { NextSeo } from 'next-seo'
import dynamic from 'next/dynamic'
import Head from 'next/head'
import NextImage from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
@@ -23,6 +24,7 @@ import ExtensionsExamplesData from '@/data/products/database/extensions-examples
import HighlightsCards from '@/data/products/database/highlight-cards'
import SqlViewCarouselData from '@/data/products/database/sql-view-carousel.json'
import TableViewCarouselData from '@/data/products/database/table-view-carousel.json'
import { serializeJsonLd, softwareApplicationSchema } from '@/lib/json-ld'
const NewFeatureCard = dynamic(() => import('~/components/NewFeatureCard'))
const ImageCarousel = dynamic(() => import('~/components/Carousels/ImageCarousel'))
@@ -72,6 +74,21 @@ function Database() {
],
}}
/>
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: serializeJsonLd(
softwareApplicationSchema({
name: 'Supabase Database',
description: meta_description,
url: 'https://supabase.com/database',
image: `https://supabase.com${basePath}/images/product/database/database-og.jpg`,
})
),
}}
/>
</Head>
<DefaultLayout>
<ProductsNav activePage={PRODUCT_NAMES.DATABASE} />
<ProductHeader
+27 -11
View File
@@ -1,18 +1,19 @@
import React from 'react'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { NextSeo } from 'next-seo'
import { useBreakpoint } from 'common'
import { PRODUCT_NAMES, PRODUCT_SHORTNAMES } from 'shared-data/products'
import page from '~/data/products/functions/page'
import DefaultLayout from '~/components/Layouts/Default'
import SectionContainer from '~/components/Layouts/SectionContainer'
import ProductHeader from '~/components/Sections/ProductHeader2'
import HighlightColumns from '~/components/Sections/HighlightColumns'
import ProductsNav from '~/components/Products/ProductsNav'
import HighlightColumns from '~/components/Sections/HighlightColumns'
import ProductHeader from '~/components/Sections/ProductHeader2'
import page from '~/data/products/functions/page'
import { useBreakpoint } from 'common'
import { NextSeo } from 'next-seo'
import dynamic from 'next/dynamic'
import Head from 'next/head'
import Image from 'next/image'
import { useRouter } from 'next/router'
import React from 'react'
import { PRODUCT_NAMES, PRODUCT_SHORTNAMES } from 'shared-data/products'
import { serializeJsonLd, softwareApplicationSchema } from '@/lib/json-ld'
const ExamplesCarousel = dynamic(() => import('~/components/Examples/ExamplesCarousel'))
const GlobalPresenceSection = dynamic(
@@ -47,6 +48,21 @@ function EdgeFunctions() {
],
}}
/>
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: serializeJsonLd(
softwareApplicationSchema({
name: 'Supabase Edge Functions',
description: pageData.metaDescription,
url: 'https://supabase.com/edge-functions',
image: `https://supabase.com${basePath}/images/product/functions/functions-og.jpg`,
})
),
}}
/>
</Head>
<DefaultLayout>
<ProductsNav activePage={PRODUCT_NAMES.FUNCTIONS} />
<ProductHeader
+7
View File
@@ -2,8 +2,10 @@ import Hero from '~/components/Hero/Hero'
import Layout from '~/components/Layouts/Default'
import Logos from '~/components/logos'
import getContent from '~/data/home/content'
import { organizationSchema, serializeJsonLd, websiteSchema } from '~/lib/json-ld'
import { NextSeo } from 'next-seo'
import dynamic from 'next/dynamic'
import Head from 'next/head'
const Products = dynamic(() => import('~/components/Products/index'))
const HeroFrameworks = dynamic(() => import('~/components/Hero/HeroFrameworks'))
@@ -16,12 +18,17 @@ const CTABanner = dynamic(() => import('components/CTABanner/index'))
// When updating page content, also update public/llms/homepage.txt
const HOMEPAGE_JSON_LD = serializeJsonLd([organizationSchema(), websiteSchema()])
const Index = () => {
const content = getContent()
return (
<>
<NextSeo canonical="https://supabase.com/" />
<Head>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: HOMEPAGE_JSON_LD }} />
</Head>
<Layout>
<Hero />
<Logos />
+22 -5
View File
@@ -1,14 +1,16 @@
import 'swiper/css'
import DefaultLayout from '~/components/Layouts/Default'
import ModulesNav from '~/components/Modules/ModulesNav'
import vectorPageData from '~/data/products/modules/vector'
import { useBreakpoint } from 'common'
import { NextSeo } from 'next-seo'
import dynamic from 'next/dynamic'
import DefaultLayout from '~/components/Layouts/Default'
import { useBreakpoint } from 'common'
import vectorPageData from '~/data/products/modules/vector'
import ModulesNav from '~/components/Modules/ModulesNav'
import Head from 'next/head'
import { PRODUCT_MODULES_NAMES, PRODUCT_MODULES_SHORTNAMES } from 'shared-data/products'
import { serializeJsonLd, softwareApplicationSchema } from '@/lib/json-ld'
const ProductModulesHeader = dynamic(() => import('~/components/Sections/ProductModulesHeader'))
const HighlightCards = dynamic(() => import('~/components/Sections/HighlightCards'))
const FeaturesSection = dynamic(() => import('~/components/Sections/FeaturesSection'))
@@ -45,6 +47,21 @@ function VectorPage() {
],
}}
/>
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: serializeJsonLd(
softwareApplicationSchema({
name: 'Supabase Vector',
description: meta_description,
url: 'https://supabase.com/modules/vector',
image: meta_image,
})
),
}}
/>
</Head>
<DefaultLayout className="bg-alternative!" stickyNavbar={false}>
<ModulesNav activePage={PRODUCT_MODULES_NAMES.VECTOR} docsUrl={pageData.docsUrl} />
<ProductModulesHeader {...pageData.heroSection} />
+28 -12
View File
@@ -1,25 +1,26 @@
import 'swiper/css'
import RealtimeStyles from '~/styles/realtime.module.css'
import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Grid, Layers, Menu } from 'lucide-react'
import Image from 'next/image'
import { Button } from 'ui'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
import SectionContainer from '~/components/Layouts/SectionContainer'
import ProductsNav from '~/components/Products/ProductsNav'
import RealtimeShowcase from '~/components/Realtime/realtime-showcase'
import APISection from '~/components/Sections/APISection'
import ProductHeader from '~/components/Sections/ProductHeader'
import RealtimeShowcase from '~/components/Realtime/realtime-showcase'
import ApiExamples from 'data/products/realtime/api-examples'
import MainProducts from '~/data/MainProducts'
import RealtimeStyles from '~/styles/realtime.module.css'
import ApiExamples from 'data/products/realtime/api-examples'
import { Grid, Layers, Menu } from 'lucide-react'
import { NextSeo } from 'next-seo'
import dynamic from 'next/dynamic'
import Head from 'next/head'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { PRODUCT_NAMES } from 'shared-data/products'
import { Button } from 'ui'
import { serializeJsonLd, softwareApplicationSchema } from '@/lib/json-ld'
const SingleQuote = dynamic(() => import('~/components/Sections/SingleQuote'))
@@ -67,6 +68,21 @@ function RealtimePage() {
],
}}
/>
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: serializeJsonLd(
softwareApplicationSchema({
name: 'Supabase Realtime',
description: meta_description,
url: 'https://supabase.com/realtime',
image: `https://supabase.com${basePath}/images/realtime/og.jpg`,
})
),
}}
/>
</Head>
<DefaultLayout>
<ProductsNav activePage={PRODUCT_NAMES.REALTIME} />
<ProductHeader
+17
View File
@@ -1,6 +1,7 @@
import { ArrowUpRight, Shuffle, Wifi, X } from 'lucide-react'
import { NextSeo } from 'next-seo'
import dynamic from 'next/dynamic'
import Head from 'next/head'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { PRODUCT_NAMES } from 'shared-data/products'
@@ -18,6 +19,7 @@ import Solutions from '@/data/MainProducts'
import ApiExamples from '@/data/products/storage/api-examples'
import DashboardViewData from '@/data/products/storage/dashboard-carousel.json'
import StoragePermissionsData from '@/data/products/storage/permissions-examples'
import { serializeJsonLd, softwareApplicationSchema } from '@/lib/json-ld'
const APISection = dynamic(() => import('~/components/Sections/APISection'))
const SingleQuote = dynamic(() => import('~/components/Sections/SingleQuote'))
@@ -53,6 +55,21 @@ function StoragePage() {
],
}}
/>
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: serializeJsonLd(
softwareApplicationSchema({
name: 'Supabase Storage',
description: meta_description,
url: 'https://supabase.com/storage',
image: `https://supabase.com${basePath}/images/product/storage/storage-og.jpg`,
})
),
}}
/>
</Head>
<DefaultLayout>
<ProductsNav activePage={PRODUCT_NAMES.STORAGE} />
<ProductHeader