Skip to content

Commit f727beb

Browse files
committed
refactor: rewrite feed generation using jpmonette/feed library
Signed-off-by: Sefa Eyeoglu <[email protected]>
1 parent 1b49853 commit f727beb

File tree

4 files changed

+59
-120
lines changed

4 files changed

+59
-120
lines changed

astro.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import sitemap from "@astrojs/sitemap";
66
import mdx from "@astrojs/mdx";
77

88
export default defineConfig({
9-
site: "https://prismlauncher.org",
9+
site: process.env.DEPLOY_URL || "https://prismlauncher.org",
1010

1111
vite: {
1212
plugins: [tailwindcss()],

bun.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@tailwindcss/vite": "4.1.11",
1111
"astro": "5.12.3",
1212
"astro-icon": "1.1.5",
13+
"feed": "5.1.0",
1314
"rehype-raw": "7.0.0",
1415
"rehype-stringify": "10.0.1",
1516
"remark": "15.0.1",
@@ -505,6 +506,8 @@
505506

506507
"fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
507508

509+
"feed": ["[email protected]", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-qGNhgYygnefSkAHHrNHqC7p3R8J0/xQDS/cYUud8er/qD9EFGWyCdUDfULHTJQN1d3H3WprzVwMc9MfB4J50Wg=="],
510+
508511
"flattie": ["[email protected]", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
509512

510513
"follow-redirects": ["[email protected]", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
@@ -1047,6 +1050,8 @@
10471050

10481051
"wrappy": ["[email protected]", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
10491052

1053+
"xml-js": ["[email protected]", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="],
1054+
10501055
"xxhash-wasm": ["[email protected]", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
10511056

10521057
"yallist": ["[email protected]", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@tailwindcss/vite": "4.1.11",
1717
"astro": "5.12.3",
1818
"astro-icon": "1.1.5",
19+
"feed": "5.1.0",
1920
"rehype-raw": "7.0.0",
2021
"rehype-stringify": "10.0.1",
2122
"remark": "15.0.1",

src/pages/feed/[feedName].xml.ts

Lines changed: 52 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -3,141 +3,74 @@ import { remark } from "remark";
33
import remarkRehype from "remark-rehype";
44
import rehypeRaw from "rehype-raw";
55
import rehypeStringify from "rehype-stringify";
6-
import { getCollection, type CollectionEntry } from "astro:content";
6+
import { getCollection } from "astro:content";
7+
import { Feed } from "feed";
78

8-
interface Post {
9-
title: string;
10-
url: string;
11-
date: string;
12-
description: string;
13-
content: string;
14-
}
15-
16-
const FEED_CONFIG = {
17-
title: "Prism Launcher",
18-
subtitle:
19-
"An Open Source Minecraft launcher with the ability to manage multiple instances, accounts and mods. Focused on user freedom and free redistributability.",
20-
21-
} as const;
9+
const DEFAULT_URL = new URL("https://prismlauncher.org");
2210

2311
const processor = remark()
2412
.use(remarkRehype, { allowDangerousHtml: true })
2513
.use(rehypeRaw)
2614
.use(rehypeStringify);
2715

28-
const escapeXml = (text: string): string =>
29-
text
30-
.replace(/&/g, "&amp;")
31-
.replace(/</g, "&lt;")
32-
.replace(/>/g, "&gt;")
33-
.replace(/"/g, "&quot;");
16+
export const GET: APIRoute = async ({
17+
site = DEFAULT_URL,
18+
url,
19+
params: { feedName },
20+
}) => {
21+
if (feedName !== "feed" && feedName !== "short") {
22+
return new Response(null, {
23+
status: 404,
24+
});
25+
}
26+
27+
const feed = new Feed({
28+
title: "Prism Launcher",
29+
description:
30+
"An Open Source Minecraft launcher with the ability to manage multiple instances, accounts and mods. Focused on user freedom and free redistributability.",
31+
id: site.toString(),
32+
feed: url.toString(),
33+
copyright: "AGPL-3.0",
34+
language: "en",
35+
image: `${site.toString()}/img/favicon.png`,
36+
});
3437

35-
async function processPost(
36-
post: CollectionEntry<"news">,
37-
siteUrl: string,
38-
): Promise<Post> {
39-
const slug = post.data.slug || post.slug;
38+
const posts = await getCollection("news", ({ data }) => !data.draft).then(
39+
(posts) =>
40+
posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()),
41+
);
4042

41-
try {
43+
for (const post of posts) {
44+
const slug = post.data.slug || post.slug;
45+
const link = new URL(`/news/${slug}`, site).toString();
46+
47+
// TODO: use Astro's .render() in the future
4248
const content = String(await processor.process(post.body))
43-
.replace(/href="\/([^"]*)"/g, `href="${siteUrl}$1"`)
44-
.replace(/src="\/([^"]*)"/g, `src="${siteUrl}$1"`)
45-
.replace(/href="#([^"]+)"/g, `href="${siteUrl}news/${slug}/#$1"`);
49+
.replace(/href="\/([^"]*)"/g, `href="${site}$1"`)
50+
.replace(/src="\/([^"]*)"/g, `src="${site}$1"`)
51+
.replace(/href="#([^"]+)"/g, `href="${link}/#$1"`);
4652

47-
return {
53+
feed.addItem({
4854
title: post.data.title,
49-
url: `${siteUrl}news/${slug}/`,
50-
date: post.data.date.toISOString(),
55+
id: link,
56+
link,
57+
date: post.data.date,
5158
description: post.data.description,
52-
content,
53-
};
54-
} catch (error) {
55-
console.warn(`Failed to process post "${post.data.title}":`, error);
56-
return {
57-
title: post.data.title,
58-
url: `${siteUrl}news/${slug}/`,
59-
date: post.data.date.toISOString(),
60-
description: post.data.description || "No description available",
61-
content: `<p>${escapeXml(post.data.description || "No description available")}</p>`,
62-
};
63-
}
64-
}
65-
66-
function generateFeed(
67-
entries: Post[],
68-
siteUrl: string,
69-
updated: string,
70-
short: boolean,
71-
): string {
72-
return `<?xml version="1.0" encoding="utf-8"?>
73-
<feed xmlns="http://www.w3.org/2005/Atom">
74-
<title>${FEED_CONFIG.title}</title>
75-
<subtitle>${FEED_CONFIG.subtitle}</subtitle>
76-
<link href="${siteUrl}feed/feed.xml" rel="self"/>
77-
<link href="${siteUrl}"/>
78-
<updated>${updated}</updated>
79-
<id>${siteUrl}</id>
80-
<author>
81-
<name>${FEED_CONFIG.title}</name>
82-
<email>${FEED_CONFIG.email}</email>
83-
</author>
84-
${entries
85-
.map((entry) => {
86-
let content = short
87-
? `<content type="string">${escapeXml(entry.description)}</content>`
88-
: `<content type="html">${escapeXml(entry.content)}</content>`;
89-
return ` <entry>
90-
<title>${escapeXml(entry.title)}</title>
91-
<link href="${entry.url}"/>
92-
<updated>${entry.date}</updated>
93-
<id>${entry.url}</id>
94-
${content}
95-
</entry>`;
96-
})
97-
.join("\n")}
98-
</feed>`;
99-
}
100-
101-
export const GET: APIRoute = async ({ site, params: { feedName } }) => {
102-
if (feedName !== "feed" && feedName !== "short") {
103-
return new Response(null, {
104-
status: 404,
59+
content: feedName === "short" ? undefined : content,
60+
author: [
61+
{
62+
name: "Prism Launcher Team",
63+
},
64+
],
10565
});
10666
}
107-
try {
108-
const posts = (await getCollection("news", ({ data }) => !data.draft)).sort(
109-
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
110-
);
111-
const siteUrl =
112-
(site?.toString() || "https://prismlauncher.org/").replace(/\/$/, "") +
113-
"/";
11467

115-
return new Response(
116-
generateFeed(
117-
await Promise.all(posts.map((post) => processPost(post, siteUrl))),
118-
siteUrl,
119-
(posts[0]?.data.date || new Date()).toISOString(),
120-
feedName === "short",
121-
),
122-
{
123-
headers: {
124-
"Content-Type": "application/atom+xml; charset=utf-8",
125-
"Cache-Control": "public, max-age=3600",
126-
},
127-
},
128-
);
129-
} catch (error) {
130-
console.error("Feed generation failed:", error);
131-
return new Response(
132-
import.meta.env.DEV
133-
? `Feed error: ${error instanceof Error ? error.message : "Unknown error"}`
134-
: "Internal Server Error",
135-
{
136-
status: 500,
137-
headers: { "Content-Type": "text/plain" },
138-
},
139-
);
140-
}
68+
return new Response(feed.atom1(), {
69+
headers: {
70+
"Content-Type": "application/atom+xml; charset=utf-8",
71+
"Cache-Control": "public, max-age=3600",
72+
},
73+
});
14174
};
14275

14376
export const getStaticPaths = () => {

0 commit comments

Comments
 (0)