feat: improve blog page design and add RSS feed support

This commit is contained in:
Mauricio Siu
2025-02-27 23:59:14 -06:00
parent 120982b85e
commit d03d30aa76
3 changed files with 103 additions and 44 deletions

View File

@@ -74,21 +74,24 @@ export default async function BlogPostPage({ params }: Props) {
const components: Partial<Components> = { const components: Partial<Components> = {
h1: ({ node, ...props }) => ( h1: ({ node, ...props }) => (
<h1 className="text-3xl font-bold mt-8 mb-4" {...props} /> <h1 className="text-3xl text-primary font-bold mt-8 mb-4" {...props} />
), ),
h2: ({ node, ...props }) => ( h2: ({ node, ...props }) => (
<h2 className="text-2xl font-bold mt-6 mb-3" {...props} /> <h2 className="text-2xl text-primary/90 font-bold mt-6 mb-3" {...props} />
), ),
h3: ({ node, ...props }) => ( h3: ({ node, ...props }) => (
<h3 className="text-xl font-bold mt-4 mb-2" {...props} /> <h3 className="text-xl text-primary/90 font-bold mt-4 mb-2" {...props} />
), ),
p: ({ node, ...props }) => ( p: ({ node, ...props }) => (
<p className="text-base leading-relaxed mb-4" {...props} /> <p
className="text-base text-muted-foreground leading-relaxed mb-4"
{...props}
/>
), ),
a: ({ node, href, ...props }) => ( a: ({ node, href, ...props }) => (
<a <a
href={href} href={href}
className="text-primary hover:text-primary/80 transition-colors" className="text-blue-500 hover:text-blue-500/80 transition-colors"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
{...props} {...props}
@@ -117,7 +120,7 @@ export default async function BlogPostPage({ params }: Props) {
}; };
return ( return (
<article className="container mx-auto px-4 py-12 max-w-4xl"> <article className="container mx-auto px-4 py-12 max-w-5xl">
<Link <Link
href="/blog" href="/blog"
className="inline-flex items-center mb-8 text-primary hover:text-primary/80 transition-colors" className="inline-flex items-center mb-8 text-primary hover:text-primary/80 transition-colors"
@@ -137,7 +140,7 @@ export default async function BlogPostPage({ params }: Props) {
{t("backToBlog")} {t("backToBlog")}
</Link> </Link>
<div className="bg-card rounded-lg p-8 shadow-lg border border-border"> <div className=" rounded-lg p-8 shadow-lg border border-border">
<header className="mb-8"> <header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1> <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center mb-6"> <div className="flex items-center mb-6">

View File

@@ -1,6 +1,6 @@
import { getPosts, getTags } from "@/lib/ghost"; import { getPosts, getTags } from "@/lib/ghost";
import type { Post } from "@/lib/ghost"; import type { Post } from "@/lib/ghost";
import { Search } from "lucide-react"; import { RssIcon } from "lucide-react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import Image from "next/image"; import Image from "next/image";
@@ -48,13 +48,20 @@ export default async function BlogPage({
}); });
return ( return (
<div className="container mx-auto px-4 py-12 flex flex-col gap-4"> <div className="container mx-auto px-4 py-12 max-w-5xl">
<div className="flex flex-col gap-2"> <div className="flex items-center justify-between mb-8">
<h1 className="text-4xl font-bold">{t("title")}</h1> <div>
<div className="flex items-center gap-2"> <p className="text-sm text-muted-foreground uppercase tracking-wider mb-2">
<Search className="size-5" /> BLOG
Welcome to the blog </p>
<h1 className="text-4xl font-bold">Stories behind the magic</h1>
</div> </div>
<Link
href="/rss.xml"
className="text-muted-foreground hover:text-foreground"
>
<RssIcon className="h-5 w-5" />
</Link>
</div> </div>
<SearchAndFilter <SearchAndFilter
@@ -66,14 +73,13 @@ export default async function BlogPage({
/> />
{filteredPosts.length === 0 ? ( {filteredPosts.length === 0 ? (
<div className="text-center py-12 min-h-[35vh] items-center justify-center flex"> <div className="text-center py-12">
<p className="text-xl flex items-center gap-2"> <p className="text-xl text-muted-foreground">
{search || selectedTag ? t("noResults") : t("noPosts")} {search || selectedTag ? t("noResults") : t("noPosts")}
<Search className="size-5" />
</p> </p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div className="space-y-8">
{filteredPosts.map((post: Post) => ( {filteredPosts.map((post: Post) => (
<BlogPostCard key={post.id} post={post} locale={locale} /> <BlogPostCard key={post.id} post={post} locale={locale} />
))} ))}
@@ -91,47 +97,51 @@ function BlogPostCard({ post, locale }: { post: Post; locale: string }) {
}); });
return ( return (
<Link href={`/blog/${post.slug}`} className="group"> <Link
<div className="bg-card rounded-lg overflow-hidden h-fit shadow-lg transition-all duration-300 hover:shadow-xl border border-border"> href={`/blog/${post.slug}`}
className="group block hover:bg-muted p-4 rounded-lg"
>
<article className="flex gap-6 items-start">
{post.feature_image && ( {post.feature_image && (
<div className="relative h-48 w-full"> <div className="relative h-32 w-48 shrink-0">
<Image <Image
src={post.feature_image} src={post.feature_image}
alt={post.title} alt={post.title}
fill fill
className="object-cover" className="object-cover rounded-lg"
/> />
</div> </div>
)} )}
<div className="p-6"> <div className="flex-1 min-w-0">
<h2 className="text-xl font-semibold mb-2 transition-colors"> <h2 className="text-xl font-semibold mb-2 group-hover:text-primary">
{post.title} {post.title}
</h2> </h2>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-muted-foreground line-clamp-2 mb-4">
{formattedDate} {post.reading_time} min read
</p>
<p className=" text-primary/80 line-clamp-3 mb-4">
{post.custom_excerpt || post.excerpt} {post.custom_excerpt || post.excerpt}
</p> </p>
<div className="flex items-center"> <div className="flex items-center text-sm text-muted-foreground">
{post.primary_author?.profile_image && ( <div className="flex items-center">
<div className="relative h-10 w-10 rounded-full overflow-hidden mr-3"> {post.primary_author?.profile_image && (
<Image <div className="relative h-6 w-6 rounded-full overflow-hidden mr-2">
src={post.primary_author.profile_image} <Image
alt={post.primary_author.name} src={post.primary_author.profile_image}
fill alt={post.primary_author.name}
className="object-cover" fill
/> className="object-cover"
</div> />
)} </div>
<div> )}
<p className="font-medium"> <span>{post.primary_author?.name || "Unknown Author"}</span>
{post.primary_author?.name || "Unknown Author"}
</p>
</div> </div>
<span className="mx-2">in</span>
<span>{post.primary_tag?.name || "General"}</span>
<span className="mx-2"></span>
<span>{post.reading_time} min read</span>
<span className="mx-2"></span>
<span>{formattedDate}</span>
</div> </div>
</div> </div>
</div> </article>
</Link> </Link>
); );
} }

View File

@@ -0,0 +1,46 @@
import { getPosts } from "@/lib/ghost";
import { NextResponse } from "next/server";
export async function GET() {
const posts = await getPosts();
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Dokploy Blog</title>
<link>https://dokploy.com/blog</link>
<description>Stories behind the magic</description>
<language>en</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${posts
.map(
(post) => `
<item>
<title><![CDATA[${post.title}]]></title>
<link>https://dokploy.com/blog/${post.slug}</link>
<guid>https://dokploy.com/blog/${post.slug}</guid>
<description><![CDATA[${post.excerpt}]]></description>
<pubDate>${new Date(post.published_at).toUTCString()}</pubDate>
${
post.feature_image
? `<enclosure url="${post.feature_image}" type="image/jpeg" />`
: ""
}
${
post.primary_author
? `<dc:creator><![CDATA[${post.primary_author.name}]]></dc:creator>`
: ""
}
</item>`,
)
.join("\n")}
</channel>
</rss>`;
return new NextResponse(rss, {
headers: {
"Content-Type": "application/xml",
"Cache-Control": "s-maxage=3600, stale-while-revalidate",
},
});
}