mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
I added content dates to `sitemap_www.xml` and `dateModified` to blog
JSON-LD so crawlers can compare freshness with page metadata. Both use
`updated` when present, otherwise the publication date.
**Changed:**
- **Consistent dates:** I use the same frontmatter parser for the blog
page and sitemap. It preserves authored dates across quoting and
timezones and rejects JavaScript frontmatter.
- **Invalid dates stop the build:** I reject impossible calendar days,
out-of-range times and offsets, malformed dates, and `updated` before
publication. Content changes run the generator in CI.
- **Authoring:** I documented optional `updated` for substantive
revisions. Events, static pages, and `/evals` omit `<lastmod>`.
**Note:** Changelog dates come from RSS. Existing sitemap omissions for
nonnumeric changelog slugs (GROWTH-1212) and app-router pages
(GROWTH-1214) remain separate.
## To test
On the preview:
- [x] Open `/sitemap_www.xml`: blog, alternatives, customer stories, and
included changelog entries should carry `YYYY-MM-DD` lastmod values.
Verified on the 2092513 preview: all 425 blog, 3 alternatives, 43
customer story, and 207 changelog entries carry a `YYYY-MM-DD` lastmod,
zero malformed values. Production currently emits no lastmod at all.
- [x] Inspect `/blog/supabase-is-now-available-in-gemini-enterprise`:
BlogPosting `dateModified` should be `2026-09-09`, matching its sitemap
entry. Verified: one BlogPosting block, `datePublished` and
`dateModified` both `2026-09-09`, sitemap lastmod `2026-09-09`.
- [x] Find the `/company` and event entries in the sitemap: neither
should carry lastmod. Verified: `/company` and all 13 `/events/` entries
have no lastmod.
- [x] Added: `/evals` and the `/changelog` index carry no lastmod
either.
- [x] Added: the blog page renders with no new console errors. The only
console error is a `/docs?_rsc=` prefetch 404: the preview host serves
404 for `/docs` itself, unrelated to this change.
## Linear
- fixes GROWTH-1206
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Blog posts now support optional updated dates for substantive
revisions.
- Sitemap entries include accurate modification dates for blog,
alternatives, customers, and changelog content.
- Blog structured data now includes the post’s modification date.
- **Bug Fixes**
- Improved validation prevents invalid or inconsistent content dates
from generating incorrect sitemap data.
- Changelog sitemap links are deduplicated and assigned their published
dates.
- **Documentation**
- Added guidance for specifying publication and update dates in blog
post metadata.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
188 lines
4.6 KiB
TypeScript
188 lines
4.6 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,
|
|
legalName: 'Supabase Pte. Ltd.',
|
|
contactPoint: {
|
|
'@type': 'ContactPoint',
|
|
contactType: 'customer support',
|
|
url: `${CANONICAL_ORIGIN}/support`,
|
|
},
|
|
address: {
|
|
'@type': 'PostalAddress',
|
|
streetAddress: '65 Chulia Street #38-02/03, OCBC Centre',
|
|
addressLocality: 'Singapore',
|
|
postalCode: '049513',
|
|
addressCountry: 'SG',
|
|
},
|
|
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 ServiceSchemaInput {
|
|
name: string
|
|
description: string
|
|
url: string
|
|
serviceType: string
|
|
offerings: Array<{ name: string; url: string }>
|
|
}
|
|
|
|
export function serviceSchema(input: ServiceSchemaInput) {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Service',
|
|
name: input.name,
|
|
description: input.description,
|
|
url: input.url,
|
|
serviceType: input.serviceType,
|
|
provider: { '@id': ORG_ID },
|
|
hasOfferCatalog: {
|
|
'@type': 'OfferCatalog',
|
|
name: `${input.name} products`,
|
|
itemListElement: input.offerings.map((offering) => ({
|
|
'@type': 'Offer',
|
|
itemOffered: {
|
|
'@type': 'Service',
|
|
name: offering.name,
|
|
url: offering.url,
|
|
},
|
|
})),
|
|
},
|
|
}
|
|
}
|
|
|
|
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
|
|
dateModified: 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,
|
|
dateModified: input.dateModified,
|
|
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,
|
|
},
|
|
},
|
|
}
|
|
}
|