[FEAT] SEO 최적화 — 메타 태그 + OG 이미지 + sitemap - #83
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 53 minutes and 33 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthrough애플리케이션에 SEO 메타데이터 관리를 위한 react-helmet-async 라이브러리를 통합했습니다. HelmetProvider를 추가하고 새로운 SeoHead 컴포넌트를 생성하여 여러 페이지에 적용했으며, robots.txt 및 sitemap.xml 파일을 추가했습니다. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
index.html (1)
5-14:⚠️ Potential issue | 🟠 Major정적 메타와 Helmet 메타의 중복 노출 가능성.
루트 코멘트(
package.json)에서 설명한 대로, React 19 +react-helmet-async@3는 기존 v2처럼 동일 속성의 meta 태그를 dedupe하지 않고 React의 네이티브 호이스팅에 맡깁니다. 이 때문에 여기 선언된description/og:*/twitter:card는 모든 라우트에서SeoHead가 추가하는 동일 속성 태그와<head>에 공존하게 됩니다.크롤러는 보통 먼저 만나는 태그를 채택하므로, 정적 태그가 페이지별 동적 태그를 가리는 시나리오가 발생할 수 있습니다(예:
/news/:id에서 OG가 사이트 기본값으로 노출). 최소 fallback만 남기고 페이지별 값은SeoHead단독으로 관리하는 것을 권장합니다.또한 nit:
<title>WEFIN</title>(L41)이og:*아래에 위치해 있어 읽기 흐름상<meta charset>과<meta viewport>근처로 옮겨두는 편이 일반적입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@index.html` around lines 5 - 14, Remove the full set of static social/meta tags from index.html so they don't clash with per-page tags created by the SeoHead component: keep only a minimal fallback (e.g., site-wide canonical or very generic description) and delete the duplicate description, og:* and twitter:card meta entries that SeoHead will supply dynamically; also move the <title> element closer to the top near the <meta charset> and <meta viewport> for proper head ordering. Ensure SeoHead is the single source of truth for page-specific meta (description, og:title, og:description, og:image, og:url, twitter:card) and index.html only provides a minimal fallback value.
🧹 Nitpick comments (2)
public/sitemap.xml (1)
1-33:<lastmod>추가 권장, 공개 뉴스 상세 경로는 의도적 제외 확인.
- 각
<url>에<lastmod>(ISO 8601)를 포함하면 크롤러가 변경 감지에 활용합니다. 현 항목들은 대부분 정적이므로 배포 시점 기준의 고정 날짜라도 넣어두는 것이 좋습니다.robots.txt에서/news는 차단되지 않았는데 sitemap에는 뉴스 상세(/news/:clusterId)가 없습니다. 동적 목록을 빌드 타임에 생성하지 않을 거라면 의도한 설계인지만 확인 부탁드립니다(색인을 원하면 동적 sitemap 생성이 필요).<priority>는 구글이 공식적으로 무시한다고 밝힌 지 오래된 힌트라, 유지/제거 모두 상관없습니다(참고용).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@public/sitemap.xml` around lines 1 - 33, Add an ISO 8601 <lastmod> element to each <url> entry in public/sitemap.xml (e.g., immediately after <loc>) — use a build/deploy date constant or the current release date for static pages — and ensure the format is YYYY-MM-DD or full timestamp. Also confirm whether news detail pages (/news/:clusterId) should be indexed; if yes, implement a dynamic sitemap generator to emit those <url> entries at build/runtime and include them in the sitemap; if not, add a comment in sitemap.xml noting that news details are intentionally omitted while robots.txt allows /news.src/shared/ui/seo-head.tsx (1)
17-17:path결합 시 슬래시 처리에 주의하세요.
path가/stocks처럼/로 시작하는 경우는 문제없지만, 호출부에서 실수로stocks혹은/stocks/처럼 전달되면 canonical/og:url이https://www.wefin.ai.krstocks또는 trailing slash 불일치로 정규화 문제가 생길 수 있습니다. 방어적으로 정규화하거나 JSDoc으로 규약을 명시해 두는 것을 권장합니다.♻️ 제안 수정
- const pageUrl = path ? `${SITE_URL}${path}` : SITE_URL + const normalizedPath = path ? (path.startsWith('/') ? path : `/${path}`) : '' + const pageUrl = `${SITE_URL}${normalizedPath}`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/ui/seo-head.tsx` at line 17, The pageUrl construction using pageUrl = path ? `${SITE_URL}${path}` : SITE_URL is fragile when callers pass "stocks", "/stocks/", or other variants; update seo-head.tsx to normalize path before concatenation (or use URL resolution): ensure path is either null/empty or begins with a single leading slash and has no trailing slash (except "/"), then build pageUrl from SITE_URL + normalized path (or use new URL(normalizedPath, SITE_URL).toString()) so canonical and og:url are always well-formed; update JSDoc for the path prop on the component to document the accepted format.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Line 38: react-helmet-async v3.0.0 changes cause duplicate meta tags in React
19 because HelmetProvider becomes transparent and <Helmet> meta tags are hoisted
to native JSX, bypassing the library's duplicate-filtering; fix by choosing one
approach: either strip index.html down to only essential static meta tags (e.g.,
basic og:image and site name) and let your SeoHead component own per-page
description/og/twitter tags, or replace react-helmet-async with an alternative
that guarantees duplicate suppression (e.g., `@dr.pogodin/react-helmet`) and
update usages of HelmetProvider and <Helmet>/SeoHead accordingly so only one
source injects per-page OG/meta tags.
In `@public/robots.txt`:
- Around line 3-7: Update the Disallow rules so they block only directory
prefixes by appending a trailing slash: replace entries like "Disallow:
/account", "Disallow: /settings", "Disallow: /admin", "Disallow: /chat", and
"Disallow: /payment" with "Disallow: /account/", "Disallow: /settings/",
"Disallow: /admin/", "Disallow: /chat/", and "Disallow: /payment/" respectively
to avoid unintentionally blocking similarly prefixed routes (e.g., /accounts or
/account-overview); verify no public detail pages rely on the broader prefix
blocking.
In `@src/features/news-feed/ui/cluster-detail-content.tsx`:
- Line 60: Normalize and hard-truncate the description before passing to
SeoHead: take cluster.summary, collapse consecutive whitespace/newlines into
single spaces, strip problematic characters like unescaped quotes, then truncate
to ~155 characters and pass that as description to SeoHead (use cluster.title
unchanged); also validate clusterId by converting to a number (e.g.,
numericClusterId = Number(clusterId)) and only render a numeric canonical path
when Number.isFinite(numericClusterId) (e.g., `/news/${numericClusterId}`),
otherwise omit or fallback to a safe non-numeric path to avoid exposing
`/news/abc` as canonical.
In `@src/shared/ui/seo-head.tsx`:
- Around line 10-12: DEFAULT_DESCRIPTION currently differs from the static
og:description in index.html causing inconsistent crawler previews; pick the
canonical description (either the shorter "실시간 모의투자 트레이딩 시스템" or the longer
sentence) and update the other source to match—e.g., if you choose the longer
sentence, replace the static og:description in index.html with the value used by
DEFAULT_DESCRIPTION, or if you prefer the short phrase, set DEFAULT_DESCRIPTION
to that string; ensure both the DEFAULT_DESCRIPTION constant and the static meta
tag use the identical text so crawlers and client-side React render the same
description.
- Around line 14-35: SeoHead currently hardcodes og:type as "website" and omits
twitter:image; update the SeoHead component to accept an optional prop (e.g.,
type) defaulting to "website" and render <meta property="og:type" content={type}
/>, and when rendering image meta add <meta property="og:image"
content={`${SITE_URL}/og-image.png`} /> (already present) and also output <meta
name="twitter:image" content={`${SITE_URL}/og-image.png`} /> so Twitter has an
explicit image; ensure callers like cluster-detail-content can pass
type="article" to enable article semantics (and allow adding article-specific
metas such as article:published_time when type === "article").
---
Duplicate comments:
In `@index.html`:
- Around line 5-14: Remove the full set of static social/meta tags from
index.html so they don't clash with per-page tags created by the SeoHead
component: keep only a minimal fallback (e.g., site-wide canonical or very
generic description) and delete the duplicate description, og:* and twitter:card
meta entries that SeoHead will supply dynamically; also move the <title> element
closer to the top near the <meta charset> and <meta viewport> for proper head
ordering. Ensure SeoHead is the single source of truth for page-specific meta
(description, og:title, og:description, og:image, og:url, twitter:card) and
index.html only provides a minimal fallback value.
---
Nitpick comments:
In `@public/sitemap.xml`:
- Around line 1-33: Add an ISO 8601 <lastmod> element to each <url> entry in
public/sitemap.xml (e.g., immediately after <loc>) — use a build/deploy date
constant or the current release date for static pages — and ensure the format is
YYYY-MM-DD or full timestamp. Also confirm whether news detail pages
(/news/:clusterId) should be indexed; if yes, implement a dynamic sitemap
generator to emit those <url> entries at build/runtime and include them in the
sitemap; if not, add a comment in sitemap.xml noting that news details are
intentionally omitted while robots.txt allows /news.
In `@src/shared/ui/seo-head.tsx`:
- Line 17: The pageUrl construction using pageUrl = path ? `${SITE_URL}${path}`
: SITE_URL is fragile when callers pass "stocks", "/stocks/", or other variants;
update seo-head.tsx to normalize path before concatenation (or use URL
resolution): ensure path is either null/empty or begins with a single leading
slash and has no trailing slash (except "/"), then build pageUrl from SITE_URL +
normalized path (or use new URL(normalizedPath, SITE_URL).toString()) so
canonical and og:url are always well-formed; update JSDoc for the path prop on
the component to document the accepted format.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bc7561f5-9caf-4551-878a-6c8acb7fa32d
⛔ Files ignored due to path filters (3)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/og-image.pngis excluded by!**/*.pngpublic/og-image.svgis excluded by!**/*.svg
📒 Files selected for processing (9)
index.htmlpackage.jsonpublic/robots.txtpublic/sitemap.xmlsrc/app/providers/app-providers.tsxsrc/features/news-feed/ui/cluster-detail-content.tsxsrc/pages/home/ui/home-page.tsxsrc/pages/stocks/ui/stocks-page.tsxsrc/shared/ui/seo-head.tsx
| "lucide-react": "^1.6.0", | ||
| "react": "^19.2.4", | ||
| "react-dom": "^19.2.4", | ||
| "react-helmet-async": "^3.0.0", |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
react-helmet-async 3.0.0 React 19 meta tag deduplication behavior
💡 Result:
In react-helmet-async version 3.0.0 (released March 3, 2026), when used with React 19, there is no meta tag deduplication. The library detects React version at runtime and switches behavior: for React 19+, Helmet renders native JSX elements (title, meta, link, etc.) that React automatically hoists to , acting as a transparent passthrough without the library's prior deduplication logic. For React 16-18, the original deduplication behavior (collecting instances across the component tree, removing duplicates by key attributes like name/property, and applying via manual DOM manipulation) is preserved. This change aligns with React 19's native metadata hoisting, which does not deduplicate tags—multiple renders result in duplicates in , with later ones potentially overriding earlier based on browser behavior (e.g., last title wins, but multiple metas may persist). Features like prioritizeSeoTags and SSR context serialization have no effect on React 19 path. If deduplication is needed with React 19, alternatives like @dr.pogodin/react-helmet maintain it, or manual logic is required.
Citations:
- 1: v3.0.0 — React 19 support, TypeScript cleanup, switch to pnpm staylor/react-helmet-async#260
- 2: Support for react 19 staylor/react-helmet-async#239
- 3: https://registry.npmjs.org/react-helmet-async
- 4: https://github.com/staylor/react-helmet-async
- 5: https://github.com/staylor/react-helmet-async/blob/master/README.md
- 6: https://github.com/staylor/react-helmet-async/blob/main/README.md
react-helmet-async v3.0.0 + React 19에서 메타 태그 중복 발생 위험
v3.0.0은 React 19에서 HelmetProvider를 투명한 통과층으로 만들고, <Helmet>의 메타 태그들을 네이티브 JSX로 렌더링하여 React 19의 헤드 호이스팅에 위임합니다. 이 과정에서 라이브러리의 메타 태그 중복 제거 로직이 작동하지 않아, index.html의 정적 description, og:*, twitter:card 태그와 SeoHead가 주입하는 동일 속성 태그가 모두 <head>에 렌더링됩니다.
SNS 크롤러가 어떤 태그를 우선으로 사용할지는 브라우저 동작에 따라 달라지므로, 페이지별 OG 메타데이터가 의도와 다르게 인식될 수 있습니다.
다음 중 하나를 선택하세요:
index.html의 메타 태그를 최소 필수(예: 기본og:image, 사이트명)만 유지하고 나머지는SeoHead에서 관리- 또는 React 19 네이티브 메타 호이스팅이나
@dr.pogodin/react-helmet같은 중복 제거를 보장하는 대안 검토
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` at line 38, react-helmet-async v3.0.0 changes cause duplicate
meta tags in React 19 because HelmetProvider becomes transparent and <Helmet>
meta tags are hoisted to native JSX, bypassing the library's
duplicate-filtering; fix by choosing one approach: either strip index.html down
to only essential static meta tags (e.g., basic og:image and site name) and let
your SeoHead component own per-page description/og/twitter tags, or replace
react-helmet-async with an alternative that guarantees duplicate suppression
(e.g., `@dr.pogodin/react-helmet`) and update usages of HelmetProvider and
<Helmet>/SeoHead accordingly so only one source injects per-page OG/meta tags.
|
|
||
| return ( | ||
| <div className="overflow-hidden rounded-2xl bg-wefin-surface p-4 sm:rounded-3xl sm:p-8"> | ||
| <SeoHead title={cluster.title} description={cluster.summary} path={`/news/${clusterId}`} /> |
There was a problem hiding this comment.
description으로 전달되는 cluster.summary 길이 고려 필요.
검색/SNS 크롤러는 meta description을 대략 150~160자, og:description도 비슷한 범위로만 표시합니다. 기사 요약은 이를 초과할 가능성이 높아 잘린 채 노출될 수 있습니다. 또한 요약에 개행/따옴표가 포함되면 렌더된 메타 속성 값이 지저분해질 수 있으니 정규화(공백 압축) + 하드 트렁케이트를 SeoHead 또는 여기서 적용하는 것을 권장합니다.
💡 예시 구현
- <SeoHead title={cluster.title} description={cluster.summary} path={`/news/${clusterId}`} />
+ <SeoHead
+ title={cluster.title}
+ description={truncate(cluster.summary.replace(/\s+/g, ' ').trim(), 155)}
+ path={`/news/${clusterId}`}
+ />추가로 clusterId는 URL에서 온 문자열이며 numericClusterId가 NaN일 수도 있습니다. path에는 원문 문자열을 넣고 있어 /news/abc 같은 경로가 canonical로 노출될 수 있으니, 유효성 체크(Number.isFinite(numericClusterId)) 후 렌더하는 것이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/news-feed/ui/cluster-detail-content.tsx` at line 60, Normalize
and hard-truncate the description before passing to SeoHead: take
cluster.summary, collapse consecutive whitespace/newlines into single spaces,
strip problematic characters like unescaped quotes, then truncate to ~155
characters and pass that as description to SeoHead (use cluster.title
unchanged); also validate clusterId by converting to a number (e.g.,
numericClusterId = Number(clusterId)) and only render a numeric canonical path
when Number.isFinite(numericClusterId) (e.g., `/news/${numericClusterId}`),
otherwise omit or fallback to a safe non-numeric path to avoid exposing
`/news/abc` as canonical.
… robots.txt 슬래시 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📌 PR 설명
SEO 최적화 + OG 태그 + sitemap/robots.txt 추가로 검색 엔진 노출 및 카카오톡/슬랙 링크 미리보기를 지원합니다.
✅ 완료한 기능 명세
📸 스크린샷
OG 이미지 미리보기:
💭 고민과 해결과정
<head>메타 태그를 동적 주입하여 해결.Googlebot은 JS 렌더링을 지원하므로 인덱싱 가능
Summary by CodeRabbit
릴리스 노트
New Features
Chores