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"; type SourceType = "twitter" | "pixiv"; type PostDetailResponse = { _id: string; type: SourceType; url?: string; author?: string; tags?: string[]; tweet?: { text?: string; title?: string; }; mediaUrl?: string; mediaIndex?: number; }; 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}`; } function createDetailTitle(post: PostDetailResponse) { const author = post.author?.trim() || "unknown"; const source = getSourceLabel(post.type); const rawText = post.tweet?.text || post.tweet?.title; const contentText = rawText?.trim() || post._id; return `${author} - ${contentText} | ${source}`; } function toAbsoluteUrl(value: string, baseUrl: string) { try { return new URL(value, baseUrl).toString(); } catch { return value; } } async function getApiBaseUrl() { const configuredBaseUrl = process.env.API_BASE_URL; if (configuredBaseUrl) { return configuredBaseUrl; } const requestHeaders = await headers(); const protocol = requestHeaders.get("x-forwarded-proto") ?? "http"; const host = requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host"); if (!host) { throw new Error("Unable to determine request host for server-side fetches."); } return `${protocol}://${host}`; } async function fetchPostDetail(apiBaseUrl: string, id: string) { const response = await fetch(`${apiBaseUrl}/api/post/detail/${id}`, { cache: "no-store", }); if (response.status === 404 || response.status >= 500) { return null; } if (!response.ok) { throw new Error(`상세 정보를 불러오지 못했습니다: ${response.status}`); } return response.json() as Promise; } async function fetchViewerRole(apiBaseUrl: string) { const cookieHeader = (await cookies()).toString(); if (!cookieHeader) { return "guest" as const; } try { const response = await fetch(`${apiBaseUrl}/api/auth/me`, { cache: "no-store", headers: { cookie: cookieHeader, }, }); if (!response.ok) { return "guest" as const; } const me = (await response.json()) as Me; return me.role; } catch { return "guest" as const; } } 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 = createDetailTitle(post); const description = createDetailDescription(post); const ogImageUrl = post.mediaUrl ? toAbsoluteUrl(post.mediaUrl, apiBaseUrl) : undefined; const ogImages = ogImageUrl ? [ { url: ogImageUrl, 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: ogImageUrl ? [ogImageUrl] : 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(); const postPromise = fetchPostDetail(apiBaseUrl, id); const viewerRolePromise = fetchViewerRole(apiBaseUrl); let post: PostDetailResponse | null; try { post = await postPromise; } catch (loadError) { return (

Detail

ID: {id}

{loadError instanceof Error ? loadError.message : "상세 정보를 불러오지 못했습니다."}

돌아가기
); } if (!post) { notFound(); } const viewerRole = await viewerRolePromise; const canManagePost = viewerRole === "admin" || viewerRole === "writer"; const sourceLabel = post.type === "twitter" ? "𝕏" : post.type === "pixiv" ? "Pixiv" : "-"; const tags = post.tags ?? []; return (

Detail

Source: {sourceLabel}

ID: {id}

{post.mediaUrl ? ( {post.author ) : (
)}
); }