diff --git a/package.json b/package.json index 5240acc6..c239f30b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "react-datepicker": "^8.4.0", "react-dom": "^19.1.0", "react-ga4": "^2.1.0", + "react-helmet-async": "^3.0.0", "react-router-dom": "^7.6.0", "zustand": "^5.0.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d47d142..324f7d90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: react-ga4: specifier: ^2.1.0 version: 2.1.0 + react-helmet-async: + specifier: ^3.0.0 + version: 3.0.0(react@19.1.0) react-router-dom: specifier: ^7.6.0 version: 7.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1151,6 +1154,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1226,6 +1232,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1418,9 +1428,17 @@ packages: peerDependencies: react: ^19.1.0 + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + react-ga4@2.1.0: resolution: {integrity: sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==} + react-helmet-async@3.0.0: + resolution: {integrity: sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -1506,6 +1524,9 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2832,6 +2853,10 @@ snapshots: inherits@2.0.4: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ipaddr.js@1.9.1: {} is-arrayish@0.2.1: {} @@ -2887,6 +2912,10 @@ snapshots: lodash.merge@4.6.2: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -3050,8 +3079,17 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-fast-compare@3.2.2: {} + react-ga4@2.1.0: {} + react-helmet-async@3.0.0(react@19.1.0): + dependencies: + invariant: 2.2.4 + react: 19.1.0 + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + react-is@16.13.1: {} react-refresh@0.17.0: {} @@ -3161,6 +3199,8 @@ snapshots: setprototypeof@1.2.0: {} + shallowequal@1.1.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..50c0a6a6 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,8 @@ +User-agent: * +Allow: /search/book/ +Allow: /group/detail/ +Allow: /feed/ +Disallow: /mypage +Disallow: /signup +Disallow: /memory/ +Sitemap: https://thip.co.kr/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 00000000..9b9e3925 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,15 @@ + + + + https://thip.co.kr/ + 1.0 + + + https://thip.co.kr/feed + 0.8 + + + https://thip.co.kr/group + 0.8 + + diff --git a/src/components/common/SEOHead.tsx b/src/components/common/SEOHead.tsx new file mode 100644 index 00000000..76e7d083 --- /dev/null +++ b/src/components/common/SEOHead.tsx @@ -0,0 +1,22 @@ +import { Helmet } from 'react-helmet-async'; + +interface SEOHeadProps { + title?: string; + description?: string; +} + +const DEFAULT_TITLE = 'THIP, 독서를 기록하는 가장 힙한 방법'; +const DEFAULT_DESC = '커뮤니티형 독서 기록 플랫폼 THIP'; + +const SEOHead = ({ title, description }: SEOHeadProps) => { + const fullTitle = title ? `${title} - THIP` : DEFAULT_TITLE; + + return ( + + {fullTitle} + + + ); +}; + +export default SEOHead; diff --git a/src/main.tsx b/src/main.tsx index 8cda91d4..62e7edb5 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,8 +1,13 @@ import { createRoot } from 'react-dom/client'; +import { HelmetProvider } from 'react-helmet-async'; import './main.css'; import App from './App.tsx'; import { initGA } from './shared/lib/analytics/ga'; initGA(); -createRoot(document.getElementById('root')!).render(); \ No newline at end of file +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/pages/groupDetail/GroupDetail.tsx b/src/pages/groupDetail/GroupDetail.tsx index 9bcf859a..24082283 100644 --- a/src/pages/groupDetail/GroupDetail.tsx +++ b/src/pages/groupDetail/GroupDetail.tsx @@ -47,6 +47,7 @@ import bookCoverLargeImg from '../../assets/books/bookCoverLarge.svg'; import PasswordModal from '@/components/group/PasswordModal'; import { usePopupStore } from '@/stores/popupStore'; import { BannerSkeleton, BookSkeleton } from '@/shared/ui/Skeleton'; +import SEOHead from '@/components/common/SEOHead'; const GroupDetail = () => { const { roomId } = useParams<{ roomId: string }>(); @@ -91,9 +92,7 @@ const GroupDetail = () => { setError(null); const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500)); - const [response] = await Promise.all([ - getRoomDetail(Number(roomId)), - ]); + const [response] = await Promise.all([getRoomDetail(Number(roomId))]); await minLoadingTime; if (response.isSuccess) { @@ -293,6 +292,10 @@ const GroupDetail = () => { return ( +
diff --git a/src/pages/searchBook/SearchBook.tsx b/src/pages/searchBook/SearchBook.tsx index 5a4eb4cb..43189417 100644 --- a/src/pages/searchBook/SearchBook.tsx +++ b/src/pages/searchBook/SearchBook.tsx @@ -42,6 +42,7 @@ import { usePopupStore } from '@/stores/popupStore'; import { FeedPostSkeleton, BookDetailSkeleton } from '@/shared/ui/Skeleton'; import { usePreventDoubleClick } from '@/hooks/usePreventDoubleClick'; import { useInifinieScroll } from '@/hooks/useInifinieScroll'; +import SEOHead from '@/components/common/SEOHead'; const FILTER = ['최신순', '인기순'] as const; const toFeedSort = (f: (typeof FILTER)[number]): FeedSort => (f === '최신순' ? 'latest' : 'like'); @@ -212,6 +213,12 @@ const SearchBook = () => { return ( + {bookDetail && ( + + )} {bookDetail && }