Files
supabase/apps/www/lib/changelog-rss.mjs
Francesco Sansalvadore 7c20cc574c feat(www): new changelog sync (#47880)
## What kind of change does this PR introduce?

Sync changelog from private supabase/changelog.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Changelog entries now come from the structured changelog repository.
* Added filters for change type, product stage, and self-hosted impact.
* Updated badge UI for affected products and change types with filter
links.
* Changelog detail sidebar now shows lifecycle stage, sunset dates, and
self-hosted impact (when available).

* **Improvements**
  * Product category discovery and filtering now use affected products.
  * Discussion links show only when legacy discussion data is present.
* RSS feeds and generated changelog markdown now use the updated
metadata.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-23 11:16:39 +02:00

116 lines
3.6 KiB
JavaScript

/**
* Pure ESM changelog RSS document builder (used by generateStaticContent.mjs and re-exported from rss.tsx).
* @typedef {{ slug: string; title: string; sortDate: string; affectedProducts?: string[] }} ChangelogRssItemInput
*/
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc.js'
dayjs.extend(utc)
function xmlEncodeRss(str) {
if (str === undefined || str === null) return ''
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
function formatRssPubDate(isoOrDate) {
return `${dayjs(isoOrDate).utc().startOf('day').format('ddd, DD MMM YYYY HH:mm:ss')} +0000`
}
function buildItemsXml(sorted) {
return sorted
.map((e) => {
const encodedTitle = xmlEncodeRss(e.title)
const canonicalUrl = `https://supabase.com/changelog/${e.slug}`
const encodedCanonical = xmlEncodeRss(canonicalUrl)
const pubDate = formatRssPubDate(e.sortDate)
return ` <item>
<guid isPermaLink="true">${encodedCanonical}</guid>
<title>${encodedTitle}</title>
<link>${encodedCanonical}</link>
<pubDate>${pubDate}</pubDate>
</item>`
})
.join('\n')
}
/**
* Converts a display label into a lowercase, URL-safe filename slug.
* e.g. "Edge Functions" → "edge-functions", "Data APIs" → "data-apis"
* @param {string} label
*/
export function labelToFileSlug(label) {
return label
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}
/** @param {ChangelogRssItemInput[]} entries */
export function generateChangelogRssXml(entries) {
const sorted = [...entries].sort(
(a, b) => dayjs(b.sortDate).valueOf() - dayjs(a.sortDate).valueOf()
)
const lastBuildDate = sorted[0]?.sortDate
? formatRssPubDate(sorted[0].sortDate)
: formatRssPubDate(dayjs().toISOString())
return `
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Supabase Changelog</title>
<link>https://supabase.com/changelog</link>
<description>Product updates and improvements from Supabase</description>
<language>en</language>
<lastBuildDate>${lastBuildDate}</lastBuildDate>
<atom:link href="https://supabase.com/changelog-rss.xml" rel="self" type="application/rss+xml"/>
${buildItemsXml(sorted)}
</channel>
</rss>
`
}
/**
* Generates a tag-filtered RSS feed.
* @param {ChangelogRssItemInput[]} allEntries - all entries, with `affected_products` display strings
* @param {{ displayLabel: string }} tag
*/
export function generateChangelogTagRssXml(allEntries, tag) {
const { displayLabel } = tag
const fileSlug = labelToFileSlug(displayLabel)
const filtered = allEntries.filter((e) =>
(e.affectedProducts ?? []).some(
(product) => product.toLowerCase() === displayLabel.toLowerCase()
)
)
const sorted = [...filtered].sort(
(a, b) => dayjs(b.sortDate).valueOf() - dayjs(a.sortDate).valueOf()
)
const lastBuildDate = sorted[0]?.sortDate
? formatRssPubDate(sorted[0].sortDate)
: formatRssPubDate(dayjs().toISOString())
const feedUrl = `https://supabase.com/changelog-rss/${fileSlug}.xml`
return `
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Supabase Changelog · ${displayLabel}</title>
<link>https://supabase.com/changelog</link>
<description>${displayLabel} updates and improvements from Supabase</description>
<language>en</language>
<lastBuildDate>${lastBuildDate}</lastBuildDate>
<atom:link href="${feedUrl}" rel="self" type="application/rss+xml"/>
${buildItemsXml(sorted)}
</channel>
</rss>
`
}