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> = {
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 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 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 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
href={href}
className="text-primary hover:text-primary/80 transition-colors"
className="text-blue-500 hover:text-blue-500/80 transition-colors"
target="_blank"
rel="noopener noreferrer"
{...props}
@ -117,7 +120,7 @@ export default async function BlogPostPage({ params }: Props) {
};
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
href="/blog"
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")}
</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">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center mb-6">

View File

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