changelog rss

This commit is contained in:
Francesco Sansalvadore committed 2026-04-15 18:16:06 +02:00
1 parent 1984fc9718
commit 4eac9853bc
17 files changed
+1447 -35

No files matched your search

+4
View File
@@ -22,3 +22,7 @@ NEXT_PUBLIC_SUPABASE_URL=https://xguihxuzqibwxjnimxev.supabase.co
NEXT_PUBLIC_UI_LIBRARY_URL=http://localhost:3004
NEXT_PUBLIC_URL=http://localhost:3000
SUPABASE_COM_SERVICE_ROLE_KEY=secret
# GITHUB_CHANGELOG_APP_ID=
# GITHUB_CHANGELOG_APP_INSTALLATION_ID=
# GITHUB_CHANGELOG_APP_PRIVATE_KEY=
@@ -0,0 +1,20 @@
import { Rss } from 'lucide-react'
import Link from 'next/link'
import { Button, cn } from 'ui'
type Props = {
className?: string
}
export function ChangelogRssButton({ className }: Props) {
return (
<Button
asChild
type="text"
className={cn(className)}
icon={<Rss className="h-4 w-4" strokeWidth={2} aria-hidden />}
>
<Link href="/changelog-rss.xml">Changelog RSS</Link>
</Button>
)
}
@@ -1,5 +1,6 @@
'use client'
import { ChangelogRssButton } from '~/components/Changelog/ChangelogRssButton'
import type { ChangelogTimelineIndexItem } from '~/lib/changelog-github'
import { githubChangelogLabelFilterUrl } from '~/lib/changelog.utils'
import mdxComponents from '~/lib/mdx/mdxComponents'
@@ -313,7 +314,10 @@ export function ChangelogV5Explorer({ items }: Props) {
<>
<div className="pb-4 container mx-auto max-w-xl">
<h1 className="h1">Changelog</h1>
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<ChangelogRssButton />
</div>
</div>
<section className="relative flex w-full flex-col">
@@ -324,7 +328,7 @@ export function ChangelogV5Explorer({ items }: Props) {
<div className="flex flex-col gap-6 pb-12">
<div
className={cn(
'sticky top-16 z-20 md:-mx-4 md:w-[calc(100%+4rem)] flex flex-col gap-1 border-default border-b md:px-4 py-3',
'sticky top-16 z-20 md:-mx-4 md:w-[calc(100%+2rem)] flex flex-col gap-1 border-default border-b md:px-4 py-3',
'bg-background/95 backdrop-blur-sm supports-[backdrop-filter]:bg-background/95'
)}
>
@@ -7,9 +7,9 @@ import dayjs from 'dayjs'
import { ChevronsUpDown } from 'lucide-react'
import type { MDXRemoteSerializeResult } from 'next-mdx-remote'
import { MDXRemote } from 'next-mdx-remote'
import Link from 'next/link'
import { parseAsInteger, useQueryState } from 'nuqs'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ChangelogRssButton } from '~/components/Changelog/ChangelogRssButton'
import {
Badge,
cn,
@@ -174,7 +174,10 @@ export function ChangelogV6Explorer({ items }: Props) {
<>
<div className="pb-4">
<h1 className="h1">Changelog</h1>
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<div className="flex flex-wrap items-center gap-2 justify-between">
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<ChangelogRssButton />
</div>
</div>
{/* Sticky combobox navigator */}
+4
View File
@@ -124,6 +124,10 @@ const footerData = [
text: 'RSS',
url: '/rss.xml',
},
{
text: 'Changelog RSS',
url: '/changelog-rss.xml',
},
],
},
{
+67
View File
@@ -0,0 +1,67 @@
/**
* Pure ESM changelog RSS document builder (used by generateStaticContent.mjs and re-exported from rss.tsx).
* @typedef {{ title: string; url: string; sortDate: string }} ChangelogRssItemInput
*/
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc.js'
import advancedFormat from 'dayjs/plugin/advancedFormat.js'
dayjs.extend(utc)
dayjs.extend(advancedFormat)
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)
.utcOffset(0, true)
.startOf('day')
.format('ddd, DD MMM YYYY HH:mm:ss [-0700]')
}
/** @param {ChangelogRssItemInput[]} entries */
export function generateChangelogRssXml(entries) {
const visible = entries.filter((e) => !e.title.includes('[d]'))
const sorted = [...visible].sort(
(a, b) => dayjs(b.sortDate).valueOf() - dayjs(a.sortDate).valueOf()
)
const lastBuildDate = sorted[0]?.sortDate
? formatRssPubDate(sorted[0].sortDate)
: formatRssPubDate(dayjs().toISOString())
const itemsXml = sorted
.map((e) => {
const encodedTitle = xmlEncodeRss(e.title)
const encodedLink = xmlEncodeRss(e.url)
const pubDate = formatRssPubDate(e.sortDate)
return `<item>
<guid>${encodedLink}</guid>
<title>${encodedTitle}</title>
<link>${encodedLink}</link>
<pubDate>${pubDate}</pubDate>
</item>`
})
.join('\n')
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"/>
${itemsXml}
</channel>
</rss>
`
}
+3
View File
@@ -1,6 +1,9 @@
// hackery to fix Terry accidentally deleting
// a bunch of releases and their associted discussions in Dec 2023
// checks if titles match and grabs this original createdAt timestamp
//
// When editing entries with `createdAt`, also sync `scripts/data/changelog-deleted-discussions.json`
// (title + createdAt only) — used by generateStaticContent.mjs for changelog-rss.xml.
export const deletedDiscussions = [
{
title: 'Platform updates: October 2023',
+24 -15
View File
@@ -5,23 +5,22 @@ var advancedFormat = require('dayjs/plugin/advancedFormat')
dayjs.extend(utc)
dayjs.extend(advancedFormat)
const generateRssItem = (post: any): string => {
const xmlEncode = (str: string) => {
if (str === undefined || str === null) {
return ''
}
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
export function xmlEncodeRss(str: string | undefined | null): string {
if (str === undefined || str === null) {
return ''
}
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
const encodedTitle = xmlEncode(post.title)
const encodedPath = xmlEncode(post.path)
const encodedDescription = xmlEncode(post.description)
const generateRssItem = (post: any): string => {
const encodedTitle = xmlEncodeRss(post.title)
const encodedPath = xmlEncodeRss(post.path)
const encodedDescription = xmlEncodeRss(post.description)
const formattedDate = dayjs(post.date)
.utcOffset(0, true)
.startOf('day')
@@ -37,9 +36,19 @@ const generateRssItem = (post: any): string => {
`
}
export type ChangelogRssItemInput = {
title: string
url: string
sortDate: string
}
/** Implemented in `./changelog-rss.mjs` (used by `scripts/generateStaticContent.mjs`). */
export { generateChangelogRssXml } from './changelog-rss.mjs'
// This utility generates RSS feeds for specialized content:
// 1. Customer stories RSS feed (customers-rss.xml) - used by pages/customers.tsx via getStaticProps
// 2. Author-specific PlanetPG RSS feeds (planetpg-{authorID}-rss.xml) - filtered feeds for individual authors
// 3. Changelog RSS (changelog-rss.xml) — built in generateStaticContent.mjs via ./changelog-rss.mjs
//
// Note: The main blog RSS feed (rss.xml) containing all blog posts is generated separately
// in generateStaticContent.mjs during the build process. This file is NOT used for the main blog feed.
+5 -1
View File
@@ -3,6 +3,7 @@ import { createAppAuth } from '@octokit/auth-app'
import { Octokit } from '@octokit/core'
import { paginateGraphql } from '@octokit/plugin-paginate-graphql'
import { Octokit as OctokitRest } from '@octokit/rest'
import { ChangelogRssButton } from '~/components/Changelog/ChangelogRssButton'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
import { discussionDisplayDate, githubChangelogLabelFilterUrl } from '~/lib/changelog.utils'
@@ -385,7 +386,10 @@ function ChangelogV2Page({ changelogIndex, changelog, pageInfo, restPage }: Chan
>
<div className="pb-4">
<h1 className="h1">Changelog</h1>
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<ChangelogRssButton />
</div>
</div>
<section
+5 -1
View File
@@ -1,3 +1,4 @@
import { ChangelogRssButton } from '~/components/Changelog/ChangelogRssButton'
import { ChangelogV3TimelineList } from '~/components/Changelog/ChangelogV3TimelineList'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
@@ -126,7 +127,10 @@ function ChangelogV3ModalContent({ changelogIndex }: PageProps) {
<div className="container mx-auto max-w-3xl flex flex-col gap-6 px-4 py-10 sm:px-16 xl:px-20">
<div>
<h1 className="h1">Changelog</h1>
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<ChangelogRssButton />
</div>
</div>
<section aria-label="Changelog entries">
+5 -2
View File
@@ -1,3 +1,4 @@
import { ChangelogRssButton } from '~/components/Changelog/ChangelogRssButton'
import { ChangelogV3TimelineList } from '~/components/Changelog/ChangelogV3TimelineList'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
@@ -5,7 +6,6 @@ import type { ChangelogTimelineIndexItem } from '~/lib/changelog-github'
import { getChangelogTimelineSortedIndex } from '~/lib/changelog-github'
import type { GetServerSideProps } from 'next'
import { NextSeo } from 'next-seo'
import Link from 'next/link'
type PageProps = {
changelogIndex: ChangelogTimelineIndexItem[]
@@ -40,7 +40,10 @@ function ChangelogV3Page({ changelogIndex }: PageProps) {
<div className="container mx-auto max-w-3xl flex flex-col gap-6 px-4 py-10 sm:px-16 xl:px-20">
<div>
<h1 className="h1">Changelog</h1>
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<ChangelogRssButton />
</div>
</div>
<section aria-label="Changelog entries">
+12 -8
View File
@@ -1,4 +1,5 @@
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/outline'
import { ChangelogRssButton } from '~/components/Changelog/ChangelogRssButton'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
import {
@@ -113,14 +114,17 @@ function ChangelogV3DetailPage({
/>
<DefaultLayout>
<div className="container mx-auto max-w-3xl flex flex-col gap-4 px-4 py-10 sm:px-16 xl:px-20">
<nav
aria-label="Breadcrumb"
className="text-foreground-lighter flex flex-wrap items-center gap-x-2 gap-y-1 text-sm"
>
<Link href="/changelog-v3" className="text-foreground-lighter hover:underline">
Changelog
</Link>
</nav>
<div className="flex flex-wrap items-center justify-between gap-4">
<nav
aria-label="Breadcrumb"
className="text-foreground-lighter flex flex-wrap items-center gap-x-2 gap-y-1 text-sm"
>
<Link href="/changelog-v3" className="text-foreground-lighter hover:underline">
Changelog
</Link>
</nav>
<ChangelogRssButton />
</div>
<header className="border-default flex flex-col gap-2 border-b pb-6">
<h1 className="h1 text-2xl sm:text-3xl">{title}</h1>
+5 -2
View File
@@ -1,3 +1,4 @@
import { ChangelogRssButton } from '~/components/Changelog/ChangelogRssButton'
import { ChangelogMonthTabExplorer } from '~/components/Changelog/ChangelogMonthTabExplorer'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
@@ -5,7 +6,6 @@ import type { ChangelogTimelineIndexItem } from '~/lib/changelog-github'
import { getChangelogTimelineSortedIndex } from '~/lib/changelog-github'
import type { GetServerSideProps } from 'next'
import { NextSeo } from 'next-seo'
import Link from 'next/link'
import { NuqsAdapter } from 'nuqs/adapters/next/pages'
type PageProps = {
@@ -40,7 +40,10 @@ export default function ChangelogV4MonthTabsPage({ changelogIndex }: PageProps)
<div className="container mx-auto max-w-5xl flex flex-col gap-6 px-4 py-10 sm:px-16 xl:px-20">
<div>
<h1 className="h1">Changelog</h1>
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<ChangelogRssButton />
</div>
</div>
<ChangelogMonthTabExplorer items={changelogIndex} />
+5 -1
View File
@@ -3,6 +3,7 @@ import { createAppAuth } from '@octokit/auth-app'
import { Octokit } from '@octokit/core'
import { paginateGraphql } from '@octokit/plugin-paginate-graphql'
import { Octokit as OctokitRest } from '@octokit/rest'
import { ChangelogRssButton } from '~/components/Changelog/ChangelogRssButton'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
import {
@@ -311,7 +312,10 @@ function ChangelogPage({ changelog, pageInfo, restPage }: ChangelogPageProps) {
>
<div className="pb-4">
<h1 className="h1">Changelog</h1>
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-foreground-lighter text-lg">New updates and product improvements</p>
<ChangelogRssButton />
</div>
</div>
{/* Content */}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,21 @@
[
{ "title": "Platform updates: October 2023", "createdAt": "2023-11-06T16:25:12Z" },
{ "title": "Platform update September 2023", "createdAt": "2023-10-06T09:23:56Z" },
{ "title": "Platform updates: August 2023", "createdAt": "2023-09-08T13:00:39Z" },
{ "title": "Platform updates June 2023", "createdAt": "2023-07-07T16:09:32Z" },
{ "title": "Platform updates: May 2023", "createdAt": "2023-06-09T16:40:16Z" },
{ "title": "Platform updates: April 2023", "createdAt": "2023-05-10T18:40:02Z" },
{ "title": "Platform Update February 2023", "createdAt": "2023-03-09T12:06:01Z" },
{ "title": "Platform update January 2023", "createdAt": "2023-02-08T18:29:41Z" },
{ "title": "Platform Updates November 2022", "createdAt": "2022-12-08T12:00:48Z" },
{ "title": "Platform updates: October 2022", "createdAt": "2022-11-02T16:07:47Z" },
{ "title": "Platform Update September 2022", "createdAt": "2022-10-07T10:18:21Z" },
{ "title": "Platform updates: 30 Nov 2021", "createdAt": "2021-11-30T10:06:55Z" },
{ "title": "October Beta 2021", "createdAt": "2021-11-08T13:14:35Z" },
{ "title": "September Beta 2021", "createdAt": "2021-10-04T18:10:57Z" },
{ "title": "August Beta 2021", "createdAt": "2021-09-13T09:47:18Z" },
{ "title": "July Beta 2021", "createdAt": "2021-08-12T13:46:14Z" },
{ "title": "June Beta 2021", "createdAt": "2021-07-04T13:23:35Z" },
{ "title": "May Beta 2021", "createdAt": "2021-06-10T11:53:14Z" },
{ "title": "April Beta 2021", "createdAt": "2021-05-05T05:17:27Z" }
]
+148 -1
View File
@@ -8,6 +8,60 @@ import advancedFormat from 'dayjs/plugin/advancedFormat.js'
import utc from 'dayjs/plugin/utc.js'
import matter from 'gray-matter'
/**
* Plain `node` does not read `.env` / `.env.local` (Next.js loads those when you run `next`).
* Minimal parser: no extra dependency; `.env` first, then `.env.local` overrides.
*/
function loadLocalEnvFiles(rootDir) {
const parseValue = (raw) => {
const val = raw.trim()
if (val.startsWith('"') && val.endsWith('"')) {
return val
.slice(1, -1)
.replace(/\\n/g, '\n')
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\')
}
if (val.startsWith("'") && val.endsWith("'")) {
return val
.slice(1, -1)
.replace(/\\n/g, '\n')
.replace(/\\'/g, "'")
.replace(/\\\\/g, '\\')
}
return val
}
const applyLine = (line, override) => {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
const eq = trimmed.indexOf('=')
if (eq === -1) return
const key = trimmed.slice(0, eq).trim().replace(/^export\s+/i, '')
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return
const value = parseValue(trimmed.slice(eq + 1))
if (override || process.env[key] === undefined) {
process.env[key] = value
}
}
for (const name of ['.env', '.env.local']) {
try {
const fp = path.join(rootDir, name)
const raw = fsSync.readFileSync(fp, 'utf8')
const override = name === '.env.local'
for (const line of raw.split(/\r?\n/)) {
applyLine(line, override)
}
} catch {
/* file missing */
}
}
}
const wwwRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
loadLocalEnvFiles(wwwRoot)
dayjs.extend(utc)
dayjs.extend(advancedFormat)
@@ -263,7 +317,7 @@ await fs.writeFile(
console.log(`✅ Generated static content with ${latestBlogPosts.length} blog posts`)
// Generate RSS feed
// Generate blog and changelog RSS feed
try {
const allBlogPosts = await getAllBlogPosts()
@@ -328,3 +382,96 @@ try {
} catch (error) {
console.warn('Error generating RSS feed:', error)
}
// Changelog RSS → public/changelog-rss.xml (same GitHub App + category as lib/changelog-github.ts)
try {
const appId = process.env.GITHUB_CHANGELOG_APP_ID
const installationId = process.env.GITHUB_CHANGELOG_APP_INSTALLATION_ID
const privateKey = process.env.GITHUB_CHANGELOG_APP_PRIVATE_KEY
if (!appId || !installationId || !privateKey) {
console.warn('Skipping changelog RSS: missing GITHUB_CHANGELOG_APP_* env vars')
} else {
const { createAppAuth } = await import('@octokit/auth-app')
const { Octokit } = await import('@octokit/core')
const { paginateGraphql } = await import('@octokit/plugin-paginate-graphql')
const { generateChangelogRssXml } = await import('../lib/changelog-rss.mjs')
const rewritesPath = path.join(__dirname, 'data/changelog-deleted-discussions.json')
const rewrites = JSON.parse(await fs.readFile(rewritesPath, 'utf8'))
const discussionDisplayDate = (item) => {
const dateRewrite = rewrites.find(
(r) => item.title && r.title && item.title.includes(r.title)
)
return dateRewrite ? dateRewrite.createdAt : item.createdAt
}
const CHANGELOG_CATEGORY_ID = 'DIC_kwDODMpXOc4CAFUr'
const changelogQuery = `
query changelogDiscussionMetadata($cursor: String, $owner: String!, $repo: String!, $categoryId: ID!) {
repository(owner: $owner, name: $repo) {
discussions(
first: 100
after: $cursor
categoryId: $categoryId
orderBy: { field: CREATED_AT, direction: DESC }
) {
nodes {
number
title
createdAt
url
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`
const ExtendedOctokit = Octokit.plugin(paginateGraphql)
const octokit = new ExtendedOctokit({
authStrategy: createAppAuth,
auth: {
appId,
installationId,
privateKey: privateKey.replace(/\\n/g, '\n'),
},
})
const collected = []
let cursor = null
let hasNextPage = true
while (hasNextPage) {
const {
repository: {
discussions: { nodes, pageInfo },
},
} = await octokit.graphql(changelogQuery, {
owner: 'supabase',
repo: 'supabase',
categoryId: CHANGELOG_CATEGORY_ID,
cursor,
})
collected.push(...nodes)
hasNextPage = pageInfo.hasNextPage
cursor = pageInfo.endCursor
}
const entries = collected.map((item) => ({
title: item.title,
url: item.url,
sortDate: discussionDisplayDate({ title: item.title, createdAt: item.createdAt }),
}))
const changelogXml = generateChangelogRssXml(entries)
const changelogRssPath = path.join(__dirname, '../public/changelog-rss.xml')
await fs.writeFile(changelogRssPath, changelogXml.trim(), 'utf8')
const visibleCount = entries.filter((e) => !e.title.includes('[d]')).length
console.log(`✅ Generated changelog RSS with ${visibleCount} entries`)
}
} catch (error) {
console.warn('Error generating changelog RSS:', error)
}