mirror of
https://github.com/Dokploy/website
synced 2025-06-26 18:16:01 +00:00
257 lines
7.0 KiB
TypeScript
257 lines
7.0 KiB
TypeScript
import { getPost, getPosts } from "@/lib/ghost";
|
|
import type { Post } from "@/lib/ghost";
|
|
import type { Metadata, ResolvingMetadata } from "next";
|
|
import { getTranslations } from "next-intl/server";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { notFound } from "next/navigation";
|
|
import type { DetailedHTMLProps, HTMLAttributes } from "react";
|
|
import ReactMarkdown from "react-markdown";
|
|
import type { Components } from "react-markdown";
|
|
import rehypeRaw from "rehype-raw";
|
|
import remarkGfm from "remark-gfm";
|
|
import { ZoomableImage } from "./components/ZoomableImage";
|
|
|
|
type Props = {
|
|
params: { locale: string; slug: string };
|
|
};
|
|
|
|
export async function generateMetadata(
|
|
{ params }: Props,
|
|
parent: ResolvingMetadata,
|
|
): Promise<Metadata> {
|
|
const post = await getPost(params.slug);
|
|
|
|
if (!post) {
|
|
return {
|
|
title: "Post Not Found",
|
|
};
|
|
}
|
|
|
|
return {
|
|
title: post.title,
|
|
description: post.custom_excerpt || post.excerpt,
|
|
openGraph: post.feature_image
|
|
? {
|
|
images: [
|
|
{
|
|
url: post.feature_image,
|
|
width: 1200,
|
|
height: 630,
|
|
alt: post.title,
|
|
},
|
|
],
|
|
}
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
export async function generateStaticParams() {
|
|
const posts = await getPosts();
|
|
|
|
return posts.map((post) => ({
|
|
slug: post.slug,
|
|
}));
|
|
}
|
|
|
|
export default async function BlogPostPage({ params }: Props) {
|
|
const { locale, slug } = params;
|
|
const t = await getTranslations({ locale, namespace: "blog" });
|
|
const post = await getPost(slug);
|
|
const allPosts = await getPosts();
|
|
|
|
// Get related posts (excluding current post)
|
|
const relatedPosts = allPosts.filter((p) => p.id !== post?.id).slice(0, 3); // Show only 3 related posts
|
|
|
|
if (!post) {
|
|
notFound();
|
|
}
|
|
|
|
const formattedDate = new Date(post.published_at).toLocaleDateString(locale, {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
|
|
const components: Partial<Components> = {
|
|
h1: ({ node, ...props }) => (
|
|
<h1 className="text-3xl text-primary font-bold mt-8 mb-4" {...props} />
|
|
),
|
|
h2: ({ node, ...props }) => (
|
|
<h2 className="text-2xl text-primary/90 font-bold mt-6 mb-3" {...props} />
|
|
),
|
|
h3: ({ node, ...props }) => (
|
|
<h3 className="text-xl text-primary/90 font-bold mt-4 mb-2" {...props} />
|
|
),
|
|
p: ({ node, ...props }) => (
|
|
<p
|
|
className="text-base text-muted-foreground leading-relaxed mb-4"
|
|
{...props}
|
|
/>
|
|
),
|
|
a: ({ node, href, ...props }) => (
|
|
<a
|
|
href={href}
|
|
className="text-blue-500 hover:text-blue-500/80 transition-colors"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
{...props}
|
|
/>
|
|
),
|
|
ul: ({ node, ...props }) => (
|
|
<ul className="list-disc list-inside space-y-2 mb-4" {...props} />
|
|
),
|
|
ol: ({ node, ...props }) => (
|
|
<ol className="list-decimal list-inside space-y-2 mb-4" {...props} />
|
|
),
|
|
li: ({ node, ...props }) => (
|
|
<li className="text-base leading-relaxed" {...props} />
|
|
),
|
|
blockquote: ({ node, ...props }) => (
|
|
<blockquote
|
|
className="border-l-4 border-primary pl-4 py-2 my-4 bg-muted/50"
|
|
{...props}
|
|
/>
|
|
),
|
|
img: ({ node, src, alt }) => (
|
|
<div className="relative w-full h-64 my-6 rounded-lg overflow-hidden">
|
|
{src && <ZoomableImage src={src} alt={alt || ""} />}
|
|
</div>
|
|
),
|
|
};
|
|
|
|
return (
|
|
<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"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-5 w-5 mr-2"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
{t("backToBlog")}
|
|
</Link>
|
|
|
|
<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">
|
|
{post.primary_author?.profile_image && (
|
|
<div className="relative h-12 w-12 rounded-full overflow-hidden mr-4">
|
|
<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>
|
|
<p className="text-sm text-muted-foreground">
|
|
{formattedDate} • {post.reading_time} min read
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{post.feature_image && (
|
|
<div className="relative w-full h-[400px] mb-8">
|
|
<ZoomableImage
|
|
src={post.feature_image}
|
|
alt={post.title}
|
|
className="rounded-lg"
|
|
/>
|
|
</div>
|
|
)}
|
|
</header>
|
|
|
|
<div className="prose prose-lg max-w-none">
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
rehypePlugins={[rehypeRaw]}
|
|
components={components}
|
|
>
|
|
{post.html}
|
|
</ReactMarkdown>
|
|
</div>
|
|
|
|
{post.tags && post.tags.length > 0 && (
|
|
<div className="mt-12 pt-6 border-t border-border">
|
|
<h2 className="text-xl font-semibold mb-4">{t("tags")}</h2>
|
|
<div className="flex flex-wrap gap-2">
|
|
{post.tags.map((tag) => (
|
|
<Link
|
|
key={tag.id}
|
|
href={`/blog/tag/${tag.slug}`}
|
|
className="px-4 py-2 bg-muted hover:bg-muted/80 rounded-full text-sm transition-colors"
|
|
>
|
|
{tag.name}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{relatedPosts.length > 0 && (
|
|
<div className="mt-12">
|
|
<h2 className="text-2xl font-bold mb-6">{t("relatedPosts")}</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{relatedPosts.map((relatedPost) => {
|
|
const relatedPostDate = new Date(
|
|
relatedPost.published_at,
|
|
).toLocaleDateString(locale, {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
|
|
return (
|
|
<Link
|
|
key={relatedPost.id}
|
|
href={`/blog/${relatedPost.slug}`}
|
|
className="group"
|
|
>
|
|
<div className="bg-card rounded-lg overflow-hidden h-full shadow-lg transition-all duration-300 hover:shadow-xl border border-border">
|
|
{relatedPost.feature_image && (
|
|
<div className="relative h-48 w-full">
|
|
<Image
|
|
src={relatedPost.feature_image}
|
|
alt={relatedPost.title}
|
|
fill
|
|
className="object-cover"
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="p-6">
|
|
<h3 className="text-lg font-semibold mb-2 group-hover:text-primary transition-colors line-clamp-2">
|
|
{relatedPost.title}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
{relatedPostDate} • {relatedPost.reading_time} min read
|
|
</p>
|
|
<p className="text-muted-foreground line-clamp-2">
|
|
{relatedPost.excerpt}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</article>
|
|
);
|
|
}
|