From 78f7156988c4cbcce53df661a132c266b3cf7d20 Mon Sep 17 00:00:00 2001 From: hjclover <107539614+hyeonjinan096@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:19:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20dynamic=20meta=20=EB=B0=8F=20og=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/picky/thumbnail.png | Bin 0 -> 7593 bytes src/apis/vote.ts | 2 +- .../_components/CommentInput/index.tsx | 2 + src/app/(post)/result/[postId]/layout.tsx | 7 ++- src/app/layout.tsx | 14 ++--- src/constants/meta.ts | 9 +++ src/utils/getGenerateMetadata.ts | 42 ++++++++++++++ src/utils/getMetadata.ts | 54 ++++++++++++++++++ 8 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 public/picky/thumbnail.png create mode 100644 src/constants/meta.ts create mode 100644 src/utils/getGenerateMetadata.ts create mode 100644 src/utils/getMetadata.ts diff --git a/public/picky/thumbnail.png b/public/picky/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..b1d1b50e8ad971bdadd39b4e5dbbe6051f1a94c6 GIT binary patch literal 7593 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 { +export const getVoteDetail = (postId: string) => { return api.get({ url: `/votes/${postId}`, }); diff --git a/src/app/(post)/result/[postId]/_components/CommentInput/index.tsx b/src/app/(post)/result/[postId]/_components/CommentInput/index.tsx index 370d95ae..46e62e37 100644 --- a/src/app/(post)/result/[postId]/_components/CommentInput/index.tsx +++ b/src/app/(post)/result/[postId]/_components/CommentInput/index.tsx @@ -1,3 +1,5 @@ +'use client'; + import { ChangeEvent, useState } from 'react'; import { useParams } from 'next/navigation'; diff --git a/src/app/(post)/result/[postId]/layout.tsx b/src/app/(post)/result/[postId]/layout.tsx index b21777c6..1eb1f938 100644 --- a/src/app/(post)/result/[postId]/layout.tsx +++ b/src/app/(post)/result/[postId]/layout.tsx @@ -1,12 +1,13 @@ -'use client'; - import { PropsWithChildren, Suspense } from 'react'; import Spinner from '@/src/components/Spinner'; +import TopBar from '@/src/components/Topbar'; +import { getGenerateMetadata } from '@/src/utils/getGenerateMetadata'; -import TopBar from '../../../../components/Topbar'; import CommentInput from './_components/CommentInput'; +export const generateMetadata = getGenerateMetadata(); + const PostDetailLayout = ({ children }: PropsWithChildren) => { return ( <> diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9063240a..631e058d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata, Viewport } from 'next'; +import type { Viewport } from 'next'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; @@ -6,9 +6,12 @@ import GoogleAnalytics from '@/src/components/GoogleAnalystics'; import Introduction from '../components/Introduction'; import { MSWComponent } from '../mocks/MSWComponent'; +import { getMetadata } from '../utils/getMetadata'; import Providers from './Providers'; import './globals.css'; +export const metadata = getMetadata(); + export const viewport: Viewport = { themeColor: '#ffffff', initialScale: 1, @@ -16,15 +19,6 @@ export const viewport: Viewport = { viewportFit: 'cover', }; -export const metadata: Metadata = { - title: '픽키', - description: '투표 플랫폼 픽키 - COPYRIGHT ©picky', - manifest: '/manifest.json', - icons: { - icon: '/app-Icon/icon-512x512.png', - }, -}; - const RootLayout = ({ children, }: Readonly<{ diff --git a/src/constants/meta.ts b/src/constants/meta.ts new file mode 100644 index 00000000..c3b2bf4b --- /dev/null +++ b/src/constants/meta.ts @@ -0,0 +1,9 @@ +export const META = { + title: 'picky', + siteName: 'picky', + description: '까다로운 고민에 대한 해답을 pick!', + url: 'https://picky-fe.vercel.app/', + keyword: ['고민', 'picky', 'pick', '투표', '추천', '결정'], + image: '/picky/thumbnail.png', + icon: '/app-Icon/icon-512x512.png', +}; diff --git a/src/utils/getGenerateMetadata.ts b/src/utils/getGenerateMetadata.ts new file mode 100644 index 00000000..e9df0d6b --- /dev/null +++ b/src/utils/getGenerateMetadata.ts @@ -0,0 +1,42 @@ +import { getVoteDetail } from '@/src/apis/vote'; +import { getMetadata } from '@/src/utils/getMetadata'; + +const RESULT_MESSAGE = '투표 결과를 확인하세요.'; +const VOTE_MESSAGE = 'picky에서 투표해보세요!'; + +export const getGenerateMetadata = + () => + async ({ params }: { params: { postId: string } }) => { + try { + const postId = params.postId; + const response = await getVoteDetail(postId); + if (!response || !response.body) { + return; + } + const { + voteTitle, + hasVoted, + voteOptions, + terminated: isTerminated, + } = response.body; + + const thumbnailImageUrl = + voteOptions && + voteOptions + .map(({ voteOptionImageUrl }) => voteOptionImageUrl) + .filter(url => url)[0]; + + const title = `${voteTitle}`; + const description = + hasVoted || isTerminated ? RESULT_MESSAGE : VOTE_MESSAGE; + + return getMetadata({ + title, + description, + asPath: `/Result/${postId}`, + image: thumbnailImageUrl, + }); + } catch (error) { + return; + } + }; diff --git a/src/utils/getMetadata.ts b/src/utils/getMetadata.ts new file mode 100644 index 00000000..fe09e0cf --- /dev/null +++ b/src/utils/getMetadata.ts @@ -0,0 +1,54 @@ +import type { Metadata } from 'next'; + +import { META } from '@/src/constants/meta'; + +interface GetMetadataProps { + title?: string; + description?: string; + asPath?: string; + image?: string; + icon?: string; +} + +export const getMetadata = (metadataProps?: GetMetadataProps) => { + const { title, description, asPath, image, icon } = metadataProps || {}; + + const TITLE = title ? `${title} | picky` : META.title; + const DESCRIPTION = description || META.description; + const PAGE_URL = asPath ? META.url + asPath : META.url; + const IMAGE = image || META.image; + const ICON = icon || META.icon; + const metadata: Metadata = { + metadataBase: new URL(META.url), + alternates: { + canonical: PAGE_URL, + }, + manifest: '/manifest.json', + title: TITLE, + description: DESCRIPTION, + keywords: [...META.keyword], + icons: { + icon: ICON, + }, + openGraph: { + title: TITLE, + description: DESCRIPTION, + siteName: TITLE, + locale: 'ko_KR', + type: 'website', + url: PAGE_URL, + images: { + url: IMAGE, + }, + }, + twitter: { + title: TITLE, + description: DESCRIPTION, + images: { + url: IMAGE, + }, + }, + }; + + return metadata; +};