feat(rss): port RSS feed from old Next.JS site to Astro

This commit is contained in:
Daniel Masterson 2024-12-16 14:44:54 +00:00
parent 19a38982b8
commit 0102dc66fd
4 changed files with 172 additions and 4 deletions

View file

@ -14,6 +14,7 @@
"@astrojs/check": "^0.9.4",
"@astrojs/cloudflare": "^12.0.1",
"@astrojs/react": "^4.1.0",
"@astrojs/rss": "^4.0.10",
"@astrojs/tailwind": "^5.1.2",
"@fontsource/bricolage-grotesque": "^5.1.0",
"@fortawesome/fontawesome-svg-core": "^6.7.1",

24
pnpm-lock.yaml generated
View file

@ -17,6 +17,9 @@ importers:
'@astrojs/react':
specifier: ^4.1.0
version: 4.1.0(@types/node@22.10.2)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(jiti@1.21.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yaml@2.6.1)
'@astrojs/rss':
specifier: ^4.0.10
version: 4.0.10
'@astrojs/tailwind':
specifier: ^5.1.2
version: 5.1.3(astro@5.0.5(@types/node@22.10.2)(jiti@1.21.6)(rollup@4.28.1)(typescript@5.7.2)(yaml@2.6.1))(tailwindcss@3.4.16)
@ -159,6 +162,9 @@ packages:
react: ^17.0.2 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0
'@astrojs/rss@4.0.10':
resolution: {integrity: sha512-2gFdHM763uUAySkdwPYrpi6dppOBJr9ddg5VbkKXctWze8d1JHgIBBY78zWIYs7KBJT58zxadsObVAVt55RDaw==}
'@astrojs/tailwind@5.1.3':
resolution: {integrity: sha512-XF7WhXRhqEHGvADqc0kDtF7Yv/g4wAWTaj91jBBTBaYnc4+MQLH94duFfFa4NlTkRG40VQd012eF3MhO3Kk+bg==}
peerDependencies:
@ -1528,6 +1534,10 @@ packages:
fast-uri@3.0.3:
resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==}
fast-xml-parser@4.5.1:
resolution: {integrity: sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==}
hasBin: true
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
@ -2532,6 +2542,9 @@ packages:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
strnum@1.0.5:
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
@ -3149,6 +3162,11 @@ snapshots:
- tsx
- yaml
'@astrojs/rss@4.0.10':
dependencies:
fast-xml-parser: 4.5.1
kleur: 4.1.5
'@astrojs/tailwind@5.1.3(astro@5.0.5(@types/node@22.10.2)(jiti@1.21.6)(rollup@4.28.1)(typescript@5.7.2)(yaml@2.6.1))(tailwindcss@3.4.16)':
dependencies:
astro: 5.0.5(@types/node@22.10.2)(jiti@1.21.6)(rollup@4.28.1)(typescript@5.7.2)(yaml@2.6.1)
@ -4502,6 +4520,10 @@ snapshots:
fast-uri@3.0.3: {}
fast-xml-parser@4.5.1:
dependencies:
strnum: 1.0.5
fastq@1.17.1:
dependencies:
reusify: 1.0.4
@ -5721,6 +5743,8 @@ snapshots:
strip-bom@3.0.0: {}
strnum@1.0.5: {}
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.5

View file

@ -29,11 +29,12 @@ import Footer from "../components/Footer.astro";
<meta property="og:color" content="#da755b3" />
<!-- twitter card -->
<meta name="twitter:card" content="summary_large_image" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=bricolage-grotesque:200,300,400,500,600,700,800|instrument-serif:400,400i" rel="stylesheet" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=bricolage-grotesque:200,300,400,500,600,700,800|instrument-serif:400,400i" rel="stylesheet" />
<link rel="alternate" type="application/rss+xml" title="Zen Browser Release Notes" href={`${Astro.url.origin}/feed.xml`} />
</head>
<body class="bg-paper font-['bricolage-grotesque'] text-dark overflow-x-hidden">
<body class="bg-paper font-['bricolage-grotesque'] text-dark overflow-x-hidden">
<slot />
<Footer />
<NavBar />

142
src/pages/feed.xml.ts Normal file
View file

@ -0,0 +1,142 @@
import rss, { type RSSOptions } from '@astrojs/rss';
import { releaseNotes } from '../release-notes';
import type { ReleaseNote } from '../release-notes';
/** The default number of entries to include in the RSS feed. */
const RSS_ENTRY_LIMIT = 20;
/**
* Handles the GET request for the `feed.xml` endpoint.
* @returns The RSS feed for the Zen Browser release notes.
*/
export function GET(context: any) {
// Just in case the release notes array is empty for whatever reason.
const latestDate = releaseNotes.length > 0
? formatRssDate(releaseNotes[0].date)
: new Date();
const rssData: RSSOptions = {
title: "Zen Browser Release Notes",
description: "Release Notes for the Zen Browser",
site: context.url,
items: [],
customData: `
<language>en</language>
<link>https://www.zen-browser.app/release-notes</link>
<copyright>Zen Browser © ${new Date().getFullYear()} - Made with by the Zen team.</copyright>
<pubDate>${pubDate(latestDate)}</pubDate>
<image>
<url>https://www.zen-browser.app/favicon.ico</url>
<title>Zen Browser</title>
<link>https://www.zen-browser.app</link>
</image>
`
};
for (const releaseNote of releaseNotes.slice(0, RSS_ENTRY_LIMIT)) {
rssData.items.push({
title: `Release notes for version ${releaseNote.version}`,
link: `https://www.zen-browser.app/release-notes/${releaseNote.version}`,
pubDate: formatRssDate(releaseNote.date),
description: releaseNote.extra,
content: formatReleaseNote(releaseNote),
});
}
return rss(rssData);
}
/**
* Formats a date string in the format day/month/year.
*
* Note: If release notes change to ISO format, this will need to be updated.
* @param dateStr The date string to format.
* @returns The passed in date string as a Date object.
*/
function formatRssDate(dateStr: string) {
const splitDate = dateStr.split("/");
if (splitDate.length !== 3) {
throw new Error("Invalid date format");
}
const day = Number(splitDate[0]);
const month = Number(splitDate[1]) - 1;
const year = Number(splitDate[2]);
return new Date(year, month, day);
}
/**
* Formats the release note entry for use as the content of the RSS feed.
* @param releaseNote The release note to format.
* @returns The formatted release note as a HTML string.
*/
function formatReleaseNote(releaseNote: ReleaseNote) {
let content = `<p>
If you encounter any issues, please report them on <a href="https://github.com/zen-browser/desktop/issues/">the issues page</a>.
Thanks everyone for your feedback!
</p>`;
if (releaseNote.image) {
content += `<img src="https://cdn.jsdelivr.net/gh/zen-browser/www/public/releases/${releaseNote.version}.png"
alt="Release Image for version ${releaseNote.version}"
style="max-width: 30em; width: 100%; border-radius: 0.5rem;"
/>`;
}
if (releaseNote.extra) {
content += `<p>${releaseNote.extra.replace(/(\n)/g, "<br />")}</p>`;
}
content += addReleaseNoteSection("⚠️ Breaking changes", releaseNote.breakingChanges);
content += addReleaseNoteSection("✓ Fixes", releaseNote.fixes?.map(fixToReleaseNote));
content += addReleaseNoteSection("🖌 Theme Changes", releaseNote.themeChanges)
content += addReleaseNoteSection("⭐ Features", releaseNote.features);
return content;
}
function addReleaseNoteSection(title: string, items?: string[]): string {
if (!items) {
return "";
}
let content = `<h2>${title}</h2>`;
content += `<ul>`;
for (const item of items) {
if (item && item.length > 0) {
content += `<li>${item}</li>`;
}
}
content += `</ul>`;
return content;
}
function fixToReleaseNote(fix?: Exclude<ReleaseNote['fixes'], undefined>[number]) {
if (!fix || !fix.description || fix.description.length === 0) {
return "";
}
let note = fix.description;
if (fix.issue) {
note += ` (<a href="https://github.com/zen-browser/desktop/issues/${fix.issue}" target="_blank">#${fix.issue}</a>)`;
}
return note;
}
function pubDate(date?: Date) {
date ??= new Date();
const pieces = date.toString().split(' ');
const offsetTime = pieces[5].match(/[-+]\d{4}/);
const offset = (offsetTime) ? offsetTime : pieces[5];
const parts = [
pieces[0] + ',',
pieces[2],
pieces[1],
pieces[3],
pieces[4],
offset
];
return parts.join(' ');
}