From be855563bb033ee57217b5dd3203fafc2341b9ea Mon Sep 17 00:00:00 2001 From: imnyang Date: Sun, 19 Apr 2026 20:27:00 +0900 Subject: [PATCH] feat: add metadata generation for post detail page and improve description formatting --- apps/frontend/src/app/detail/[id]/page.tsx | 80 ++++++++++++++++++++++ apps/frontend/src/app/robots.txt | 2 + 2 files changed, 82 insertions(+) create mode 100644 apps/frontend/src/app/robots.txt diff --git a/apps/frontend/src/app/detail/[id]/page.tsx b/apps/frontend/src/app/detail/[id]/page.tsx index 86b16e0..85fcf68 100644 --- a/apps/frontend/src/app/detail/[id]/page.tsx +++ b/apps/frontend/src/app/detail/[id]/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { cookies, headers } from "next/headers"; import { notFound } from "next/navigation"; +import type { Metadata } from "next"; import Header from "@/components/header"; import DetailRawPanel from "@/components/detail-raw-panel"; @@ -20,6 +21,20 @@ type Me = { role: "admin" | "writer" | "reader"; }; +function getSourceLabel(type: SourceType) { + return type === "twitter" ? "X" : type === "pixiv" ? "Pixiv" : "-"; +} + +function createDetailDescription(post: PostDetailResponse) { + const author = post.author?.trim() || "unknown"; + const source = getSourceLabel(post.type); + const tags = (post.tags ?? []).filter((tag) => tag.trim().length > 0).slice(0, 5); + const tagText = tags.length > 0 ? ` | Tags: ${tags.join(", ")}` : ""; + return `${author} | ${source} post${tagText}`; +} + + + async function getApiBaseUrl() { const configuredBaseUrl = process.env.API_BASE_URL; if (configuredBaseUrl) { @@ -78,6 +93,71 @@ async function fetchViewerRole(apiBaseUrl: string) { } } +export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { + const { id } = await params; + const fallbackTitle = `Detail ${id} | Akiyama Mizuki`; + + try { + const apiBaseUrl = await getApiBaseUrl(); + const post = await fetchPostDetail(apiBaseUrl, id); + const detailUrl = `${apiBaseUrl}/detail/${id}`; + + if (!post) { + return { + title: fallbackTitle, + description: "Post detail", + alternates: { + canonical: detailUrl, + }, + robots: { + index: false, + follow: false, + }, + }; + } + + const source = getSourceLabel(post.type); + const author = post.author?.trim() || "unknown"; + const title = `${author} | ${source}`; + const description = createDetailDescription(post); + const ogImages = post.mediaUrl + ? [ + { + url: post.mediaUrl, + alt: `${author} ${source}`, + }, + ] + : undefined; + + return { + title, + description, + alternates: { + canonical: detailUrl, + }, + openGraph: { + type: "article", + title, + description, + url: detailUrl, + siteName: "Akiyama Mizuki", + images: ogImages, + }, + twitter: { + card: post.mediaUrl ? "summary_large_image" : "summary", + title, + description, + images: post.mediaUrl ? [post.mediaUrl] : undefined, + }, + }; + } catch { + return { + title: fallbackTitle, + description: "Post detail", + }; + } +} + export default async function DetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const apiBaseUrl = await getApiBaseUrl(); diff --git a/apps/frontend/src/app/robots.txt b/apps/frontend/src/app/robots.txt new file mode 100644 index 0000000..4e0bfc5 --- /dev/null +++ b/apps/frontend/src/app/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: / \ No newline at end of file