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 ( + + {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}`; +};