From eefb4006a9baa5e4ffbe9a7d35b936166cc28894 Mon Sep 17 00:00:00 2001 From: dooohun Date: Mon, 11 May 2026 15:38:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20SEO=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=9D=B8=ED=94=84=EB=9D=BC=20=EC=B6=94=EA=B0=80=20(refs=20#?= =?UTF-8?q?1256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/static/seo.ts: SITE_NAME, DEFAULT_TITLE, DEFAULT_DESCRIPTION, DEFAULT_OG_IMAGE, TWITTER_CARD_TYPE 상수와 buildAbsoluteUrl 헬퍼 추가 - src/components/seo/Seo/index.tsx: title, description, canonical, Open Graph, Twitter Card, noindex 옵션을 처리하는 공통 컴포넌트 추가. 메타 태그에 key 부여로 페이지 단위 override 동작 보장 - src/pages/_document.tsx: 페이지별로 변동되는 메타(description, og:title, og:description, og:image, og:image:width) 제거. Seo 컴포넌트가 담당 - src/pages/_app.tsx: 을 <Seo title={pageTitle} />로 교체. Component.title fallback을 'KOIN'에서 undefined로 변경하여 Seo의 DEFAULT_TITLE이 적용되도록 조정 --- src/components/seo/Seo/index.tsx | 56 ++++++++++++++++++++++++++++++++ src/pages/_app.tsx | 8 ++--- src/pages/_document.tsx | 12 +------ src/static/seo.ts | 15 +++++++++ 4 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 src/components/seo/Seo/index.tsx create mode 100644 src/static/seo.ts diff --git a/src/components/seo/Seo/index.tsx b/src/components/seo/Seo/index.tsx new file mode 100644 index 000000000..ac582f257 --- /dev/null +++ b/src/components/seo/Seo/index.tsx @@ -0,0 +1,56 @@ +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { + DEFAULT_DESCRIPTION, + DEFAULT_OG_IMAGE, + DEFAULT_TITLE, + SITE_NAME, + TWITTER_CARD_TYPE, + buildAbsoluteUrl, +} from 'static/seo'; + +interface SeoProps { + title?: string; + description?: string; + url?: string; + image?: string; + type?: 'website' | 'article'; + noindex?: boolean; + imageAlt?: string; +} + +export default function Seo({ + title, + description = DEFAULT_DESCRIPTION, + url, + image = DEFAULT_OG_IMAGE, + type = 'website', + noindex = false, + imageAlt, +}: SeoProps) { + const router = useRouter(); + const resolvedTitle = title || DEFAULT_TITLE; + const canonicalUrl = buildAbsoluteUrl(url ?? router.asPath?.split('?')[0]); + + return ( + <Head> + <title>{resolvedTitle} + + + {noindex && } + + + + + + + + {imageAlt && } + + + + + + + ); +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 043fc6202..db7b2a536 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from 'react'; import type { NextPage } from 'next'; import type { AppProps } from 'next/app'; -import Head from 'next/head'; import { useRouter } from 'next/router'; import './index.scss'; import { GoogleAnalytics, GoogleTagManager } from '@next/third-parties/google'; @@ -11,6 +10,7 @@ import Toast from 'components/feedback/Toast'; import Layout from 'components/layout'; import MaintenancePage from 'components/Maintenance'; import PortalProvider from 'components/modal/Modal/PortalProvider'; +import Seo from 'components/seo/Seo'; import ROUTES from 'static/routes'; import { COOKIE_KEY } from 'static/url'; import useAutoLogin from 'utils/hooks/auth/useAutoLogin'; @@ -79,7 +79,7 @@ export default function App({ Component, pageProps }: AppPropsWithAuth) { const getLayout = Component.getLayout || ((page) => {page}); const pageTitle = React.useMemo(() => { - if (!Component.title) return 'KOIN'; + if (!Component.title) return undefined; return typeof Component.title === 'function' ? Component.title(router.asPath) : Component.title; }, [Component, router.asPath]); @@ -141,9 +141,7 @@ export default function App({ Component, pageProps }: AppPropsWithAuth) { {GA_ID && } - - {pageTitle} - + {getLayout()} diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index bf9868d8c..f719fd6f3 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -8,20 +8,10 @@ export default function Document() { - {/* 기본 메타 */} + {/* 기본 메타 (페이지 단위 메타는 components/seo/Seo 가 처리) */} - - - {/* Open Graph */} - - - - {/* 국제화/번역 제어 */} diff --git a/src/static/seo.ts b/src/static/seo.ts new file mode 100644 index 000000000..8192d4466 --- /dev/null +++ b/src/static/seo.ts @@ -0,0 +1,15 @@ +import { KOIN_BASE_URL } from 'static/url'; + +export const SITE_NAME = '코인'; +export const DEFAULT_TITLE = '코인 - 한기대 커뮤니티'; +export const DEFAULT_DESCRIPTION = + '보다 편하게, 한국기술교육대학교 생활에 필요한 서비스를 만날 수 있습니다.'; +export const DEFAULT_OG_IMAGE = 'https://static.koreatech.in/assets/img/facebook_showcase_image.png'; +export const TWITTER_CARD_TYPE = 'summary_large_image'; + +export const buildAbsoluteUrl = (path?: string) => { + if (!path) return KOIN_BASE_URL; + if (/^https?:\/\//.test(path)) return path; + const normalized = path.startsWith('/') ? path : `/${path}`; + return `${KOIN_BASE_URL}${normalized}`; +};