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