Skip to content

Commit 8ac5aa9

Browse files
authored
feat: add rss feed + remove “related “projects for newsletter (#429)
- Remove “related “projects for newsletter: #423 - Add RSS feed: #411
1 parent e91d3a6 commit 8ac5aa9

File tree

9 files changed

+247
-7
lines changed

9 files changed

+247
-7
lines changed

app/[lang]/blog/[slug]/page.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ export default function BlogArticle({ params }: any) {
6161

6262
const imageAsCover = true
6363

64+
const isNewsletter =
65+
post?.title?.toLowerCase().includes("newsletter") ||
66+
post?.tags?.includes("newsletter") ||
67+
post?.tldr?.toLowerCase()?.includes("newsletter")
68+
6469
return (
6570
<div className="flex flex-col">
6671
<div className="flex items-start justify-center z-0 relative">
@@ -150,7 +155,11 @@ export default function BlogArticle({ params }: any) {
150155
</div>
151156
</div>
152157
<div className="pt-10 md:pt-16 pb-32">
153-
<BlogContent post={post} lang={params.lang} />
158+
<BlogContent
159+
post={post}
160+
lang={params.lang}
161+
isNewsletter={isNewsletter}
162+
/>
154163
</div>
155164
</div>
156165
)

app/api/rss/route.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { NextResponse } from "next/server"
2+
import { generateRssFeed } from "@/lib/rss"
3+
4+
export const dynamic = "force-dynamic"
5+
export const revalidate = 3600 // Revalidate every hour
6+
7+
export async function GET(request: Request) {
8+
try {
9+
const { searchParams } = new URL(request.url)
10+
const lang = searchParams.get("lang") || "en"
11+
12+
console.log("Generating RSS feed for language:", lang)
13+
const feed = await generateRssFeed(lang)
14+
console.log("RSS feed generated successfully")
15+
16+
return new NextResponse(feed, {
17+
headers: {
18+
"Content-Type": "application/xml",
19+
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=1800",
20+
},
21+
})
22+
} catch (error) {
23+
console.error("Error generating RSS feed:", error)
24+
if (error instanceof Error) {
25+
console.error("Error details:", {
26+
message: error.message,
27+
stack: error.stack,
28+
name: error.name,
29+
})
30+
}
31+
return new NextResponse(
32+
`Error generating RSS feed: ${error instanceof Error ? error.message : "Unknown error"}`,
33+
{ status: 500 }
34+
)
35+
}
36+
}

app/layout.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ export const metadata: Metadata = {
5959
},
6060
],
6161
},
62+
alternates: {
63+
types: {
64+
"application/rss+xml": [
65+
{
66+
url: "/api/rss",
67+
title: "RSS Feed for Privacy & Scaling Explorations",
68+
},
69+
],
70+
},
71+
},
6272
}
6373

6474
interface RootLayoutProps {

components/blog/blog-content.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ArticleListCard } from "./article-list-card"
1010
interface BlogContentProps {
1111
post: Article
1212
lang: LocaleTypes
13+
isNewsletter?: boolean
1314
}
1415

1516
interface BlogImageProps {
@@ -36,7 +37,11 @@ export function BlogImage({ image, alt, description }: BlogImageProps) {
3637
)
3738
}
3839

39-
export function BlogContent({ post, lang }: BlogContentProps) {
40+
export function BlogContent({
41+
post,
42+
lang,
43+
isNewsletter = false,
44+
}: BlogContentProps) {
4045
const articles = getArticles() ?? []
4146
const articleIndex = articles.findIndex((article) => article.id === post.id)
4247

@@ -54,10 +59,12 @@ export function BlogContent({ post, lang }: BlogContentProps) {
5459
<Markdown>{post?.content ?? ""}</Markdown>
5560
</div>
5661

57-
<BlogArticleRelatedProjects
58-
projectsIds={post.projects ?? []}
59-
lang={lang}
60-
/>
62+
{!isNewsletter && (
63+
<BlogArticleRelatedProjects
64+
projectsIds={post.projects ?? []}
65+
lang={lang}
66+
/>
67+
)}
6168

6269
{moreArticles?.length > 0 && (
6370
<div className="flex flex-col gap-8">

components/icons.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,18 @@ export const Icons = {
235235
/>
236236
</svg>
237237
),
238+
rss: (props: LucideProps) => (
239+
<svg
240+
xmlns="http://www.w3.org/2000/svg"
241+
width={props.size || 18}
242+
height={props.size || 18}
243+
viewBox="0 0 24 24"
244+
fill="currentColor"
245+
{...props}
246+
>
247+
<path d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20H16.83A12.73 12.73 0 0 0 4 7.17V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9H12.1A7.34 7.34 0 0 0 4 12.1V10.1z" />
248+
</svg>
249+
),
238250
readme: (props: LucideProps) => (
239251
<svg
240252
width={props.size}

components/site-footer.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ export function SiteFooter({ lang }: LangProps["params"]) {
156156
}
157157
/>
158158
</Link>
159-
160159
<Link
161160
href={siteConfig.links.youtube}
162161
className="flex items-center gap-2"
@@ -172,6 +171,22 @@ export function SiteFooter({ lang }: LangProps["params"]) {
172171
}
173172
/>
174173
</Link>
174+
175+
<Link
176+
href="/api/rss"
177+
className="flex items-center gap-2"
178+
target="_blank"
179+
rel="noreferrer"
180+
>
181+
<ItemLabel
182+
label="RSS"
183+
icon={
184+
<div className="w-4">
185+
<Icons.rss className="w-full" />
186+
</div>
187+
}
188+
/>
189+
</Link>
175190
</LinksWrapper>
176191
<LinksWrapper>
177192
<Link

lib/rss.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Feed } from "feed"
2+
import fs from "fs"
3+
import path from "path"
4+
import matter from "gray-matter"
5+
6+
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://pse.dev"
7+
8+
function formatDate(dateString: string | undefined): Date {
9+
if (!dateString) {
10+
console.warn("No date provided, using current date")
11+
return new Date()
12+
}
13+
14+
try {
15+
const [year, month, day] = dateString.split("-").map(Number)
16+
17+
if (!year || !month || !day || isNaN(year) || isNaN(month) || isNaN(day)) {
18+
console.warn(`Invalid date format: ${dateString}, using current date`)
19+
return new Date()
20+
}
21+
22+
const date = new Date(year, month - 1, day)
23+
24+
if (isNaN(date.getTime())) {
25+
console.warn(`Invalid date: ${dateString}, using current date`)
26+
return new Date()
27+
}
28+
29+
return date
30+
} catch (error) {
31+
console.warn(`Error parsing date: ${dateString}, using current date`)
32+
return new Date()
33+
}
34+
}
35+
36+
export async function generateRssFeed(lang: string = "en") {
37+
const feed = new Feed({
38+
title: "Privacy + Scaling Explorations",
39+
description:
40+
"PSE is a research and development lab with a mission of making cryptography useful for human collaboration. We build open source tooling with things like zero-knowledge proofs, multiparty computation, homomorphic encryption, Ethereum, and more.",
41+
id: SITE_URL,
42+
link: SITE_URL,
43+
language: lang,
44+
image: `${SITE_URL}/favicon.ico`,
45+
favicon: `${SITE_URL}/favicon.ico`,
46+
copyright: `All rights reserved ${new Date().getFullYear()}, Privacy & Scaling Explorations`,
47+
updated: new Date(),
48+
feedLinks: {
49+
rss2: `${SITE_URL}/api/rss`,
50+
},
51+
author: {
52+
name: "PSE Team",
53+
link: SITE_URL,
54+
},
55+
})
56+
57+
try {
58+
// Get all articles
59+
const articlesDirectory = path.join(process.cwd(), "articles")
60+
const articleFiles = fs.readdirSync(articlesDirectory)
61+
62+
const articles = articleFiles
63+
.filter((file) => file.endsWith(".md") && file !== "_article-template.md")
64+
.map((file) => {
65+
try {
66+
const filePath = path.join(articlesDirectory, file)
67+
const fileContents = fs.readFileSync(filePath, "utf8")
68+
const { data, content } = matter(fileContents)
69+
70+
return {
71+
slug: file.replace(/\.md$/, ""),
72+
frontmatter: data,
73+
content,
74+
}
75+
} catch (error) {
76+
console.warn(`Error processing article ${file}:`, error)
77+
return null
78+
}
79+
})
80+
.filter(
81+
(article): article is NonNullable<typeof article> => article !== null
82+
)
83+
.sort(
84+
(a, b) =>
85+
formatDate(b.frontmatter.date).getTime() -
86+
formatDate(a.frontmatter.date).getTime()
87+
)
88+
89+
// Add articles to feed
90+
articles.forEach((article) => {
91+
try {
92+
const url = `${SITE_URL}/articles/${article.slug}`
93+
const pubDate = formatDate(article.frontmatter.date)
94+
95+
feed.addItem({
96+
title: article.frontmatter.title || "Untitled Article",
97+
id: url,
98+
link: url,
99+
description: article.frontmatter.tldr || "",
100+
content: article.content,
101+
author: article.frontmatter.authors?.map((author: string) => ({
102+
name: author,
103+
})) || [{ name: "PSE Team" }],
104+
date: pubDate,
105+
image: article.frontmatter.image
106+
? `${SITE_URL}${article.frontmatter.image}`
107+
: undefined,
108+
category:
109+
article.frontmatter.tags?.map((tag: string) => ({ name: tag })) ||
110+
[],
111+
})
112+
} catch (error) {
113+
console.warn(`Error adding article ${article.slug} to feed:`, error)
114+
}
115+
})
116+
117+
return feed.rss2()
118+
} catch (error) {
119+
console.error("Error generating RSS feed:", error)
120+
throw error
121+
}
122+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"clsx": "^1.2.1",
3535
"discord.js": "14.4.0",
3636
"dotenv": "^16.4.4",
37+
"feed": "^5.1.0",
3738
"framer-motion": "^10.12.17",
3839
"fuse.js": "^6.6.2",
3940
"gray-matter": "^4.0.3",

yarn.lock

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4173,6 +4173,15 @@ __metadata:
41734173
languageName: node
41744174
linkType: hard
41754175

4176+
"feed@npm:^5.1.0":
4177+
version: 5.1.0
4178+
resolution: "feed@npm:5.1.0"
4179+
dependencies:
4180+
xml-js: "npm:^1.6.11"
4181+
checksum: 10/c0b7cd6e6a5d5406f871cdaf4487d273c00e35f3c04975bb9095e10cc5234ec1b797d11a37f6502476d212e1ba0854f3de7302c4750bdeb7aec3aedcd4e50426
4182+
languageName: node
4183+
linkType: hard
4184+
41764185
"file-entry-cache@npm:^8.0.0":
41774186
version: 8.0.0
41784187
resolution: "file-entry-cache@npm:8.0.0"
@@ -6857,6 +6866,7 @@ __metadata:
68576866
eslint-config-prettier: "npm:^8.8.0"
68586867
eslint-plugin-react: "npm:^7.37.4"
68596868
eslint-plugin-tailwindcss: "npm:^3.18.0"
6869+
feed: "npm:^5.1.0"
68606870
framer-motion: "npm:^10.12.17"
68616871
fuse.js: "npm:^6.6.2"
68626872
globals: "npm:^15.14.0"
@@ -8079,6 +8089,13 @@ __metadata:
80798089
languageName: node
80808090
linkType: hard
80818091

8092+
"sax@npm:^1.2.4":
8093+
version: 1.4.1
8094+
resolution: "sax@npm:1.4.1"
8095+
checksum: 10/b1c784b545019187b53a0c28edb4f6314951c971e2963a69739c6ce222bfbc767e54d320e689352daba79b7d5e06d22b5d7113b99336219d6e93718e2f99d335
8096+
languageName: node
8097+
linkType: hard
8098+
80828099
"scheduler@npm:^0.23.2":
80838100
version: 0.23.2
80848101
resolution: "scheduler@npm:0.23.2"
@@ -9713,6 +9730,17 @@ __metadata:
97139730
languageName: node
97149731
linkType: hard
97159732

9733+
"xml-js@npm:^1.6.11":
9734+
version: 1.6.11
9735+
resolution: "xml-js@npm:1.6.11"
9736+
dependencies:
9737+
sax: "npm:^1.2.4"
9738+
bin:
9739+
xml-js: ./bin/cli.js
9740+
checksum: 10/55ce342a47bf14a138a3fcea0c9e325b81484cfc1a8aac78df13b4d6ca01f20e32820572bc3e927cd9b61b9da9cdee4657cb2f304e460343d8d85d6a3659d749
9741+
languageName: node
9742+
linkType: hard
9743+
97169744
"yallist@npm:^3.0.2":
97179745
version: 3.1.1
97189746
resolution: "yallist@npm:3.1.1"

0 commit comments

Comments
 (0)