.
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 1m49s

This commit is contained in:
ParkWonYeop
2025-12-29 00:46:47 +09:00
parent b3ba5c2374
commit 2847cf52f8
2 changed files with 38 additions and 46 deletions

View File

@@ -1,18 +1,20 @@
import { Metadata } from 'next';
import PostDetailClient from '@/components/post/PostDetailClient';
import { notFound } from 'next/navigation';
import { Post } from '@/types';
// 서버 사이드 메타데이터 생성을 위한 타입
type Props = {
params: Promise<{ slug: string }>;
};
// 메타데이터용 데이터 패칭 (src/api/http.ts를 거치지 않고 직접 호출)
async function getPostForMetadata(slug: string) {
// 🛠️ 서버 사이드 데이터 패칭 함수 (Metadata와 Page 양쪽에서 재사용)
async function getPostFromServer(slug: string): Promise<Post | null> {
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
try {
const res = await fetch(`${BASE_URL}/api/posts/${slug}`, {
// 메타데이터는 캐시해도 되지만, 글 수정 시 반영을 위해 적절한 시간 설정
// 캐시 설정: 60초마다 갱신 (블로그 특성상 적절)
// 즉시 반영이 필요하다면 'no-store'로 설정하거나 revalidate: 0 사용
next: { revalidate: 60 },
});
@@ -21,15 +23,15 @@ async function getPostForMetadata(slug: string) {
const json = await res.json();
return json.data;
} catch (error) {
console.error('Metadata fetch error:', error);
console.error('Server fetch error:', error);
return null;
}
}
// 🌟 핵심: 동적 메타데이터 생성 (SEO & 카톡 공유 미리보기)
// 🌟 메타데이터 생성 (SEO)
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPostForMetadata(slug);
const post = await getPostFromServer(slug);
if (!post) {
return {
@@ -37,15 +39,14 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}
// 본문 내용을 150자로 잘라서 설명으로 사용 (마크다운 제거)
// 본문 요약 및 이미지 추출 로직
const description = post.content
?.replace(/[#*`_~]/g, '')
.replace(/\n/g, ' ')
.substring(0, 150) + '...';
// 썸네일 이미지 추출 (본문에 이미지가 있다면 첫 번째 이미지 사용)
const imageMatch = post.content?.match(/!\[.*?\]\((.*?)\)/);
const imageUrl = imageMatch ? imageMatch[1] : '/og-image.png'; // 기본 이미지
const imageUrl = imageMatch ? imageMatch[1] : '/og-image.png';
return {
title: post.title,
@@ -55,15 +56,9 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
description: description,
url: `https://blog.wypark.me/posts/${slug}`,
siteName: 'WYPark Blog',
images: [
{
url: imageUrl,
width: 1200,
height: 630,
},
],
images: [{ url: imageUrl, width: 1200, height: 630 }],
type: 'article',
publishedTime: post.updatedAt,
publishedTime: post.createdAt, // created_at 대신 createdAt 사용 주의 (타입 정의 따름)
authors: ['WYPark'],
},
twitter: {
@@ -75,8 +70,18 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}
// 실제 페이지 렌더링
// 🌟 실제 페이지 렌더링 (SSR 적용)
export default async function PostDetailPage({ params }: Props) {
const { slug } = await params;
return <PostDetailClient slug={slug} />;
// 1. 서버에서 데이터를 미리 가져옵니다.
const post = await getPostFromServer(slug);
// 2. 데이터가 없으면 404 페이지로 보냅니다. (봇에게도 404 신호를 줌)
if (!post) {
notFound();
}
// 3. 가져온 데이터를 클라이언트 컴포넌트에 'initialPost'로 넘겨줍니다.
return <PostDetailClient slug={slug} initialPost={post} />;
}