mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-04-23 03:01:03 -04:00
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:
@@ -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
36
app/api/rss/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
122
lib/rss.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
28
yarn.lock
28
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user