mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-04-23 03:01:03 -04:00
feat: fix blog page
This commit is contained in:
@@ -7,100 +7,139 @@ import { Markdown } from "@/components/ui/markdown"
|
||||
import { getArticles, getArticleById } from "@/lib/blog"
|
||||
import { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const articles = await getArticles()
|
||||
return articles.map(({ id }) => ({
|
||||
slug: id,
|
||||
}))
|
||||
try {
|
||||
const articles = await getArticles()
|
||||
return articles.map(({ id }) => ({
|
||||
slug: id,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error("Error generating static params for blog articles:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: any): Promise<Metadata> {
|
||||
const post = await getArticleById(params.slug)
|
||||
try {
|
||||
const post = await getArticleById(params.slug)
|
||||
|
||||
const imageUrl =
|
||||
(post?.image ?? "")?.length > 0
|
||||
? `/articles/${post?.id}/${post?.image}`
|
||||
: "/og-image.png"
|
||||
if (!post) {
|
||||
return {
|
||||
title: "Article Not Found",
|
||||
description: "The requested article could not be found.",
|
||||
}
|
||||
}
|
||||
|
||||
const metadata: Metadata = {
|
||||
title: post?.title,
|
||||
description: post?.tldr,
|
||||
openGraph: {
|
||||
images: [{ url: imageUrl, width: 1200, height: 630 }],
|
||||
},
|
||||
}
|
||||
const imageUrl =
|
||||
(post?.image ?? "")?.length > 0
|
||||
? `/articles/${post?.id}/${post?.image}`
|
||||
: "/og-image.png"
|
||||
|
||||
// Add canonical URL if post has canonical property
|
||||
if (post && "canonical" in post) {
|
||||
metadata.alternates = {
|
||||
canonical: post.canonical as string,
|
||||
const metadata: Metadata = {
|
||||
title: post?.title,
|
||||
description: post?.tldr,
|
||||
openGraph: {
|
||||
images: [{ url: imageUrl, width: 1200, height: 630 }],
|
||||
},
|
||||
}
|
||||
|
||||
// Add canonical URL if post has canonical property
|
||||
if (post && "canonical" in post) {
|
||||
metadata.alternates = {
|
||||
canonical: post.canonical as string,
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
} catch (error) {
|
||||
console.error(`Error generating metadata for slug ${params.slug}:`, error)
|
||||
return {
|
||||
title: "Blog Article",
|
||||
description: "An error occurred while loading this article.",
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
export default function BlogArticle({ params }: any) {
|
||||
const slug = params.slug
|
||||
const post = getArticleById(slug)
|
||||
export default async function BlogArticle({ params }: any) {
|
||||
try {
|
||||
const slug = params.slug
|
||||
const post = await getArticleById(slug)
|
||||
|
||||
if (!post) return null
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-center background-gradient z-0">
|
||||
<div className="w-full bg-cover-gradient border-b border-tuatara-300">
|
||||
<AppContent className="flex flex-col gap-8 py-10 max-w-[978px]">
|
||||
<Label.PageTitle label={post?.title} />
|
||||
{post?.date || post?.tldr ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{post?.date && (
|
||||
<div
|
||||
className={blogArticleCardTagCardVariants({
|
||||
variant: "secondary",
|
||||
})}
|
||||
>
|
||||
{new Date(post?.date).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{post?.canonical && (
|
||||
<div className="text-sm italic text-gray-500 mt-1">
|
||||
This post was originally posted in{" "}
|
||||
<a
|
||||
href={post.canonical}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer canonical"
|
||||
className="text-primary hover:underline"
|
||||
if (!post) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-center background-gradient z-0">
|
||||
<div className="w-full bg-cover-gradient border-b border-tuatara-300">
|
||||
<AppContent className="flex flex-col gap-8 py-10 max-w-[978px]">
|
||||
<Label.PageTitle label={post?.title} />
|
||||
{post?.date || post?.tldr ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{post?.date && (
|
||||
<div
|
||||
className={blogArticleCardTagCardVariants({
|
||||
variant: "secondary",
|
||||
})}
|
||||
>
|
||||
{new URL(post.canonical).hostname.replace(/^www\./, "")}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{post?.tldr && <Markdown>{post?.tldr}</Markdown>}
|
||||
</div>
|
||||
) : null}
|
||||
{(post?.tags ?? [])?.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm italic text-tuatara-950">Tags:</span>
|
||||
<div className="flex gap-2">
|
||||
{post?.tags?.map((tag) => (
|
||||
<Link key={tag} href={`/${params.lang}/blog?tag=${tag}`}>
|
||||
<Button size="xs">{tag}</Button>
|
||||
</Link>
|
||||
))}
|
||||
{new Date(post?.date).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{post?.canonical && (
|
||||
<div className="text-sm italic text-gray-500 mt-1">
|
||||
This post was originally posted in{" "}
|
||||
<a
|
||||
href={post.canonical}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer canonical"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{new URL(post.canonical).hostname.replace(/^www\./, "")}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{post?.tldr && <Markdown>{post?.tldr}</Markdown>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AppContent>
|
||||
) : null}
|
||||
{(post?.tags ?? [])?.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm italic text-tuatara-950">Tags:</span>
|
||||
<div className="flex gap-2">
|
||||
{post?.tags?.map((tag) => (
|
||||
<Link key={tag} href={`/${params.lang}/blog?tag=${tag}`}>
|
||||
<Button size="xs">{tag}</Button>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AppContent>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-10 md:pt-16 pb-32">
|
||||
<BlogContent post={post} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-10 md:pt-16 pb-32">
|
||||
<BlogContent post={post} />
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error rendering blog article ${params.slug}:`, error)
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20">
|
||||
<h1 className="text-2xl font-bold mb-4">Error Loading Article</h1>
|
||||
<p className="text-gray-600 mb-8">
|
||||
There was a problem loading this article. Please try again later.
|
||||
</p>
|
||||
<Link href={`/${params.lang}/blog`}>
|
||||
<Button>Return to Blog</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useTranslation } from "@/app/i18n"
|
||||
import { BlogArticles } from "@/components/blog/blog-articles"
|
||||
import { AppContent } from "@/components/ui/app-content"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { getArticles } from "@/lib/blog"
|
||||
import { Article, getArticles } from "@/lib/blog"
|
||||
import { Metadata } from "next"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -18,11 +18,14 @@ interface BlogPageProps {
|
||||
const BlogPage = async ({ params: { lang }, searchParams }: BlogPageProps) => {
|
||||
const { t } = await useTranslation(lang, "blog-page")
|
||||
|
||||
// Get the tag from searchParams
|
||||
const tag = searchParams?.tag as string | undefined
|
||||
|
||||
// Fetch articles, filtering by tag if present
|
||||
const articles = await getArticles({ tag })
|
||||
let articles: Article[] = []
|
||||
try {
|
||||
articles = await getArticles({ tag })
|
||||
} catch (error) {
|
||||
console.error("Error fetching blog articles:", error)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
|
||||
54
lib/blog.ts
54
lib/blog.ts
@@ -23,20 +23,43 @@ const articlesDirectory = path.join(process.cwd(), "articles")
|
||||
// Get all articles from /articles
|
||||
export function getArticles(options?: { limit?: number; tag?: string }) {
|
||||
const { limit = 1000, tag } = options ?? {}
|
||||
// Get file names under /articles
|
||||
const fileNames = fs.readdirSync(articlesDirectory)
|
||||
|
||||
if (!fs.existsSync(articlesDirectory)) {
|
||||
console.error(`Articles directory not found at ${articlesDirectory}`)
|
||||
return []
|
||||
}
|
||||
|
||||
let fileNames = []
|
||||
try {
|
||||
fileNames = fs.readdirSync(articlesDirectory)
|
||||
} catch (error) {
|
||||
console.error(`Error reading articles directory: ${error}`)
|
||||
return []
|
||||
}
|
||||
|
||||
const allArticlesData = fileNames.map((fileName: string) => {
|
||||
const id = fileName.replace(/\.md$/, "")
|
||||
if (id.toLowerCase() === "readme") {
|
||||
return null
|
||||
}
|
||||
|
||||
// Read markdown file as string
|
||||
const fullPath = path.join(articlesDirectory, fileName)
|
||||
const fileContents = fs.readFileSync(fullPath, "utf8")
|
||||
|
||||
let fileContents
|
||||
try {
|
||||
fileContents = fs.readFileSync(fullPath, "utf8")
|
||||
} catch (error) {
|
||||
console.error(`Error reading file ${fileName}: ${error}`)
|
||||
return {
|
||||
id,
|
||||
title: `Error reading ${id}`,
|
||||
content: "This article could not be read due to an error.",
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
tags: [],
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Use matter with options to handle multiline strings
|
||||
const matterResult = matter(fileContents, {
|
||||
engines: {
|
||||
yaml: {
|
||||
@@ -65,9 +88,11 @@ export function getArticles(options?: { limit?: number; tag?: string }) {
|
||||
|
||||
return {
|
||||
id,
|
||||
title: matterResult.data.title || `Article ${id}`,
|
||||
content: matterResult.content || "",
|
||||
date: matterResult.data.date || new Date().toISOString().split("T")[0],
|
||||
...matterResult.data,
|
||||
tags: tags, // Assign the combined and normalized tags array
|
||||
content: matterResult.content,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${fileName}:`, error)
|
||||
@@ -97,6 +122,9 @@ export function getArticles(options?: { limit?: number; tag?: string }) {
|
||||
const dateA = new Date(a.date)
|
||||
const dateB = new Date(b.date)
|
||||
|
||||
if (isNaN(dateA.getTime())) return 1
|
||||
if (isNaN(dateB.getTime())) return -1
|
||||
|
||||
// Sort in descending order (newest first)
|
||||
return dateB.getTime() - dateA.getTime()
|
||||
})
|
||||
@@ -105,11 +133,17 @@ export function getArticles(options?: { limit?: number; tag?: string }) {
|
||||
}
|
||||
|
||||
export function getArticleById(slug?: string) {
|
||||
// Note: This might need adjustment if you expect getArticleById to also have tags
|
||||
// Currently relies on the base getArticles() which fetches all tags
|
||||
const articles = getArticles() // Fetch all articles to find the one by ID
|
||||
if (!slug) return null
|
||||
|
||||
return articles.find((article) => article.id === slug)
|
||||
try {
|
||||
// Note: This might need adjustment if you expect getArticleById to also have tags
|
||||
// Currently relies on the base getArticles() which fetches all tags
|
||||
const articles = getArticles() // Fetch all articles to find the one by ID
|
||||
return articles.find((article) => article.id === slug)
|
||||
} catch (error) {
|
||||
console.error(`Error in getArticleById for slug ${slug}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const lib = { getArticles, getArticleById }
|
||||
|
||||
Reference in New Issue
Block a user