feat: add rss feed + remove “related “projects for newsletter (#429)

- Remove “related “projects for newsletter: #423
- Add RSS feed: #411
This commit is contained in:
Kalidou Diagne
2025-06-04 08:36:09 +09:00
committed by GitHub
parent e91d3a67b7
commit 8ac5aa9797
9 changed files with 247 additions and 7 deletions

View File

@@ -61,6 +61,11 @@ export default function BlogArticle({ params }: any) {
const imageAsCover = true
const isNewsletter =
post?.title?.toLowerCase().includes("newsletter") ||
post?.tags?.includes("newsletter") ||
post?.tldr?.toLowerCase()?.includes("newsletter")
return (
<div className="flex flex-col">
<div className="flex items-start justify-center z-0 relative">
@@ -150,7 +155,11 @@ export default function BlogArticle({ params }: any) {
</div>
</div>
<div className="pt-10 md:pt-16 pb-32">
<BlogContent post={post} lang={params.lang} />
<BlogContent
post={post}
lang={params.lang}
isNewsletter={isNewsletter}
/>
</div>
</div>
)

36
app/api/rss/route.ts Normal file
View File

@@ -0,0 +1,36 @@
import { NextResponse } from "next/server"
import { generateRssFeed } from "@/lib/rss"
export const dynamic = "force-dynamic"
export const revalidate = 3600 // Revalidate every hour
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const lang = searchParams.get("lang") || "en"
console.log("Generating RSS feed for language:", lang)
const feed = await generateRssFeed(lang)
console.log("RSS feed generated successfully")
return new NextResponse(feed, {
headers: {
"Content-Type": "application/xml",
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=1800",
},
})
} catch (error) {
console.error("Error generating RSS feed:", error)
if (error instanceof Error) {
console.error("Error details:", {
message: error.message,
stack: error.stack,
name: error.name,
})
}
return new NextResponse(
`Error generating RSS feed: ${error instanceof Error ? error.message : "Unknown error"}`,
{ status: 500 }
)
}
}

View File

@@ -59,6 +59,16 @@ export const metadata: Metadata = {
},
],
},
alternates: {
types: {
"application/rss+xml": [
{
url: "/api/rss",
title: "RSS Feed for Privacy & Scaling Explorations",
},
],
},
},
}
interface RootLayoutProps {

View File

@@ -10,6 +10,7 @@ import { ArticleListCard } from "./article-list-card"
interface BlogContentProps {
post: Article
lang: LocaleTypes
isNewsletter?: boolean
}
interface BlogImageProps {
@@ -36,7 +37,11 @@ export function BlogImage({ image, alt, description }: BlogImageProps) {
)
}
export function BlogContent({ post, lang }: BlogContentProps) {
export function BlogContent({
post,
lang,
isNewsletter = false,
}: BlogContentProps) {
const articles = getArticles() ?? []
const articleIndex = articles.findIndex((article) => article.id === post.id)
@@ -54,10 +59,12 @@ export function BlogContent({ post, lang }: BlogContentProps) {
<Markdown>{post?.content ?? ""}</Markdown>
</div>
<BlogArticleRelatedProjects
projectsIds={post.projects ?? []}
lang={lang}
/>
{!isNewsletter && (
<BlogArticleRelatedProjects
projectsIds={post.projects ?? []}
lang={lang}
/>
)}
{moreArticles?.length > 0 && (
<div className="flex flex-col gap-8">

View File

@@ -235,6 +235,18 @@ export const Icons = {
/>
</svg>
),
rss: (props: LucideProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={props.size || 18}
height={props.size || 18}
viewBox="0 0 24 24"
fill="currentColor"
{...props}
>
<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" />
</svg>
),
readme: (props: LucideProps) => (
<svg
width={props.size}

View File

@@ -156,7 +156,6 @@ export function SiteFooter({ lang }: LangProps["params"]) {
}
/>
</Link>
<Link
href={siteConfig.links.youtube}
className="flex items-center gap-2"
@@ -172,6 +171,22 @@ export function SiteFooter({ lang }: LangProps["params"]) {
}
/>
</Link>
<Link
href="/api/rss"
className="flex items-center gap-2"
target="_blank"
rel="noreferrer"
>
<ItemLabel
label="RSS"
icon={
<div className="w-4">
<Icons.rss className="w-full" />
</div>
}
/>
</Link>
</LinksWrapper>
<LinksWrapper>
<Link

122
lib/rss.ts Normal file
View File

@@ -0,0 +1,122 @@
import { Feed } from "feed"
import fs from "fs"
import path from "path"
import matter from "gray-matter"
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://pse.dev"
function formatDate(dateString: string | undefined): Date {
if (!dateString) {
console.warn("No date provided, using current date")
return new Date()
}
try {
const [year, month, day] = dateString.split("-").map(Number)
if (!year || !month || !day || isNaN(year) || isNaN(month) || isNaN(day)) {
console.warn(`Invalid date format: ${dateString}, using current date`)
return new Date()
}
const date = new Date(year, month - 1, day)
if (isNaN(date.getTime())) {
console.warn(`Invalid date: ${dateString}, using current date`)
return new Date()
}
return date
} catch (error) {
console.warn(`Error parsing date: ${dateString}, using current date`)
return new Date()
}
}
export async function generateRssFeed(lang: string = "en") {
const feed = new Feed({
title: "Privacy + Scaling Explorations",
description:
"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.",
id: SITE_URL,
link: SITE_URL,
language: lang,
image: `${SITE_URL}/favicon.ico`,
favicon: `${SITE_URL}/favicon.ico`,
copyright: `All rights reserved ${new Date().getFullYear()}, Privacy & Scaling Explorations`,
updated: new Date(),
feedLinks: {
rss2: `${SITE_URL}/api/rss`,
},
author: {
name: "PSE Team",
link: SITE_URL,
},
})
try {
// Get all articles
const articlesDirectory = path.join(process.cwd(), "articles")
const articleFiles = fs.readdirSync(articlesDirectory)
const articles = articleFiles
.filter((file) => file.endsWith(".md") && file !== "_article-template.md")
.map((file) => {
try {
const filePath = path.join(articlesDirectory, file)
const fileContents = fs.readFileSync(filePath, "utf8")
const { data, content } = matter(fileContents)
return {
slug: file.replace(/\.md$/, ""),
frontmatter: data,
content,
}
} catch (error) {
console.warn(`Error processing article ${file}:`, error)
return null
}
})
.filter(
(article): article is NonNullable<typeof article> => article !== null
)
.sort(
(a, b) =>
formatDate(b.frontmatter.date).getTime() -
formatDate(a.frontmatter.date).getTime()
)
// Add articles to feed
articles.forEach((article) => {
try {
const url = `${SITE_URL}/articles/${article.slug}`
const pubDate = formatDate(article.frontmatter.date)
feed.addItem({
title: article.frontmatter.title || "Untitled Article",
id: url,
link: url,
description: article.frontmatter.tldr || "",
content: article.content,
author: article.frontmatter.authors?.map((author: string) => ({
name: author,
})) || [{ name: "PSE Team" }],
date: pubDate,
image: article.frontmatter.image
? `${SITE_URL}${article.frontmatter.image}`
: undefined,
category:
article.frontmatter.tags?.map((tag: string) => ({ name: tag })) ||
[],
})
} catch (error) {
console.warn(`Error adding article ${article.slug} to feed:`, error)
}
})
return feed.rss2()
} catch (error) {
console.error("Error generating RSS feed:", error)
throw error
}
}

View File

@@ -34,6 +34,7 @@
"clsx": "^1.2.1",
"discord.js": "14.4.0",
"dotenv": "^16.4.4",
"feed": "^5.1.0",
"framer-motion": "^10.12.17",
"fuse.js": "^6.6.2",
"gray-matter": "^4.0.3",

View File

@@ -4173,6 +4173,15 @@ __metadata:
languageName: node
linkType: hard
"feed@npm:^5.1.0":
version: 5.1.0
resolution: "feed@npm:5.1.0"
dependencies:
xml-js: "npm:^1.6.11"
checksum: 10/c0b7cd6e6a5d5406f871cdaf4487d273c00e35f3c04975bb9095e10cc5234ec1b797d11a37f6502476d212e1ba0854f3de7302c4750bdeb7aec3aedcd4e50426
languageName: node
linkType: hard
"file-entry-cache@npm:^8.0.0":
version: 8.0.0
resolution: "file-entry-cache@npm:8.0.0"
@@ -6857,6 +6866,7 @@ __metadata:
eslint-config-prettier: "npm:^8.8.0"
eslint-plugin-react: "npm:^7.37.4"
eslint-plugin-tailwindcss: "npm:^3.18.0"
feed: "npm:^5.1.0"
framer-motion: "npm:^10.12.17"
fuse.js: "npm:^6.6.2"
globals: "npm:^15.14.0"
@@ -8079,6 +8089,13 @@ __metadata:
languageName: node
linkType: hard
"sax@npm:^1.2.4":
version: 1.4.1
resolution: "sax@npm:1.4.1"
checksum: 10/b1c784b545019187b53a0c28edb4f6314951c971e2963a69739c6ce222bfbc767e54d320e689352daba79b7d5e06d22b5d7113b99336219d6e93718e2f99d335
languageName: node
linkType: hard
"scheduler@npm:^0.23.2":
version: 0.23.2
resolution: "scheduler@npm:0.23.2"
@@ -9713,6 +9730,17 @@ __metadata:
languageName: node
linkType: hard
"xml-js@npm:^1.6.11":
version: 1.6.11
resolution: "xml-js@npm:1.6.11"
dependencies:
sax: "npm:^1.2.4"
bin:
xml-js: ./bin/cli.js
checksum: 10/55ce342a47bf14a138a3fcea0c9e325b81484cfc1a8aac78df13b4d6ca01f20e32820572bc3e927cd9b61b9da9cdee4657cb2f304e460343d8d85d6a3659d749
languageName: node
linkType: hard
"yallist@npm:^3.0.2":
version: 3.1.1
resolution: "yallist@npm:3.1.1"