diff --git a/apps/website/app/[locale]/blog/[slug]/page.tsx b/apps/website/app/[locale]/blog/[slug]/page.tsx new file mode 100644 index 0000000..fe153bd --- /dev/null +++ b/apps/website/app/[locale]/blog/[slug]/page.tsx @@ -0,0 +1,145 @@ +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"; + +type Props = { + params: { locale: string; slug: string }; +}; + +export async function generateMetadata( + { params }: Props, + parent: ResolvingMetadata, +): Promise { + 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); + + if (!post) { + notFound(); + } + + const formattedDate = new Date(post.published_at).toLocaleDateString(locale, { + year: "numeric", + month: "long", + day: "numeric", + }); + + return ( +
+ + + + + {t("backToBlog")} + + +
+

{post.title}

+
+ {post.primary_author?.profile_image && ( +
+ {post.primary_author.name} +
+ )} +
+

+ {post.primary_author?.name || "Unknown Author"} +

+

+ {formattedDate} • {post.reading_time} min read +

+
+
+ {post.feature_image && ( +
+ {post.title} +
+ )} +
+ +
+ + {post.tags && post.tags.length > 0 && ( +
+

{t("tags")}

+
+ {post.tags.map((tag) => ( + + {tag.name} + + ))} +
+
+ )} +
+ ); +} diff --git a/apps/website/app/[locale]/blog/page.tsx b/apps/website/app/[locale]/blog/page.tsx new file mode 100644 index 0000000..51c31b1 --- /dev/null +++ b/apps/website/app/[locale]/blog/page.tsx @@ -0,0 +1,95 @@ +import { getPosts } from "@/lib/ghost"; +import type { Post } from "@/lib/ghost"; +import type { Metadata } from "next"; +import { getTranslations } from "next-intl/server"; +import Image from "next/image"; +import Link from "next/link"; + +export const metadata: Metadata = { + title: "Blog | Dokploy", + description: "Latest news, updates, and articles from Dokploy", +}; + +export const revalidate = 3600; // Revalidate the data at most every hour + +export default async function BlogPage({ + params: { locale }, +}: { + params: { locale: string }; +}) { + const t = await getTranslations({ locale, namespace: "blog" }); + const posts = await getPosts(); + + return ( +
+

{t("title")}

+ + {posts.length === 0 ? ( +
+

+ {t("noPosts")} +

+
+ ) : ( +
+ {posts.map((post: Post) => ( + + ))} +
+ )} +
+ ); +} + +function BlogPostCard({ post, locale }: { post: Post; locale: string }) { + const formattedDate = new Date(post.published_at).toLocaleDateString(locale, { + year: "numeric", + month: "long", + day: "numeric", + }); + + return ( + +
+ {post.feature_image && ( +
+ {post.title} +
+ )} +
+

+ {post.title} +

+

+ {formattedDate} • {post.reading_time} min read +

+

+ {post.custom_excerpt || post.excerpt} +

+
+ {post.primary_author?.profile_image && ( +
+ {post.primary_author.name} +
+ )} +
+

+ {post.primary_author?.name || "Unknown Author"} +

+
+
+
+
+ + ); +} diff --git a/apps/website/app/[locale]/blog/tag/[tag]/page.tsx b/apps/website/app/[locale]/blog/tag/[tag]/page.tsx new file mode 100644 index 0000000..4d66662 --- /dev/null +++ b/apps/website/app/[locale]/blog/tag/[tag]/page.tsx @@ -0,0 +1,135 @@ +import { getPostsByTag, getTags } from "@/lib/ghost"; +import type { Post } from "@/lib/ghost"; +import type { Metadata } from "next"; +import { getTranslations } from "next-intl/server"; +import Image from "next/image"; +import Link from "next/link"; +import { notFound } from "next/navigation"; + +type Props = { + params: { locale: string; tag: string }; +}; + +export async function generateMetadata({ params }: Props): Promise { + const { tag, locale } = params; + const t = await getTranslations({ locale, namespace: "blog" }); + + return { + title: `${t("tagTitle", { tag })}`, + description: t("tagDescription", { tag }), + }; +} + +export async function generateStaticParams() { + const tags = await getTags(); + + return tags.map((tag: { slug: string }) => ({ + tag: tag.slug, + })); +} + +export default async function TagPage({ params }: Props) { + const { locale, tag } = params; + const t = await getTranslations({ locale, namespace: "blog" }); + const posts = await getPostsByTag(tag); + + if (!posts || posts.length === 0) { + notFound(); + } + + // Get the tag name from the first post + const tagName = + posts[0].tags?.find((t: { slug: string }) => t.slug === tag)?.name || tag; + + return ( +
+ + + + + {t("backToBlog")} + + +
+

+ {t("postsTaggedWith")}{" "} + "{tagName}" +

+

+ {t("foundPosts", { count: posts.length })} +

+
+ +
+ {posts.map((post: Post) => ( + + ))} +
+
+ ); +} + +function BlogPostCard({ post, locale }: { post: Post; locale: string }) { + const formattedDate = new Date(post.published_at).toLocaleDateString(locale, { + year: "numeric", + month: "long", + day: "numeric", + }); + + return ( + +
+ {post.feature_image && ( +
+ {post.title} +
+ )} +
+

+ {post.title} +

+

+ {formattedDate} • {post.reading_time} min read +

+

+ {post.custom_excerpt || post.excerpt} +

+
+ {post.primary_author?.profile_image && ( +
+ {post.primary_author.name} +
+ )} +
+

+ {post.primary_author?.name || "Unknown Author"} +

+
+
+
+
+ + ); +} diff --git a/apps/website/components/Header.tsx b/apps/website/components/Header.tsx index 7663c2b..b4d9e04 100644 --- a/apps/website/components/Header.tsx +++ b/apps/website/components/Header.tsx @@ -127,6 +127,7 @@ function MobileNavigation() { {t("navigation.docs")} + {t("navigation.blog")}