mirror of
https://github.com/Dokploy/website
synced 2025-06-26 18:16:01 +00:00
feat: improve blog page design and add RSS feed support
This commit is contained in:
parent
120982b85e
commit
d03d30aa76
@ -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">
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
46
apps/website/app/rss.xml/route.ts
Normal file
46
apps/website/app/rss.xml/route.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user