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 && }