mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(www): add JSON-LD structured data to homepage, products, and blog (#45451)
This commit is contained in:
1 parent
50ccd39b31
commit
e2480538ad
9 files changed
+297
-30
No files matched your search
@@ -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 {}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user