Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
<!doctype html>
<html lang="en">
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta
name="description"
content="실시간 모의투자 트레이딩 시스템. 실시간 거래, 타임머신 투자, AI 시장 브리핑을 제공합니다."
/>
<meta property="og:type" content="website" />
<meta property="og:title" content="WeFin — 과거에서 배우고, 현재에 투자하는 모의투자" />
<meta
property="og:description"
content="실시간 모의투자 트레이딩 시스템. 실시간 거래, 타임머신 투자, AI 시장 브리핑을 제공합니다."
/>
<meta property="og:url" content="https://www.wefin.ai.kr" />
<meta property="og:image" content="https://www.wefin.ai.kr/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<script>
window.global = window
;(function () {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"lucide-react": "^1.6.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-helmet-async": "^3.0.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


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.

"react-router-dom": "^7.13.2",
"recharts": "^3.8.1",
"sockjs-client": "^1.6.1",
Expand Down
40 changes: 40 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added public/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
121 changes: 121 additions & 0 deletions public/og-image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
User-agent: *
Allow: /
Disallow: /account/
Disallow: /settings/
Disallow: /admin/
Disallow: /chat/
Disallow: /payment/

Sitemap: https://www.wefin.ai.kr/sitemap.xml
33 changes: 33 additions & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.wefin.ai.kr/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://www.wefin.ai.kr/stocks</loc>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://www.wefin.ai.kr/history</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://www.wefin.ai.kr/about</loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://www.wefin.ai.kr/terms</loc>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://www.wefin.ai.kr/privacy</loc>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
</urlset>
53 changes: 28 additions & 25 deletions src/app/providers/app-providers.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import type { PropsWithChildren } from 'react'
import { HelmetProvider } from 'react-helmet-async'
import { Toaster } from 'sonner'

import ErrorBoundary from './error-boundary'
Expand All @@ -17,31 +18,33 @@ const queryClient = new QueryClient({
function AppProviders({ children }: PropsWithChildren) {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
{children}
<Toaster
position="top-center"
style={{ zIndex: 9999 }}
toastOptions={{
style: {
borderRadius: '16px',
padding: '12px 20px',
fontSize: '14px',
fontWeight: 600,
background: 'var(--surface)',
color: 'var(--text)',
boxShadow: '0 8px 32px -8px rgba(0,0,0,0.2)',
border: '1px solid var(--line)'
},
classNames: {
success: '!bg-wefin-mint !text-white !border-wefin-mint-deep/30',
error: '!bg-wefin-red !text-white !border-wefin-red/30',
warning: '!bg-wefin-amber !text-white !border-wefin-amber/30'
}
}}
offset={20}
/>
</QueryClientProvider>
<HelmetProvider>
<QueryClientProvider client={queryClient}>
{children}
<Toaster
position="top-center"
style={{ zIndex: 9999 }}
toastOptions={{
style: {
borderRadius: '16px',
padding: '12px 20px',
fontSize: '14px',
fontWeight: 600,
background: 'var(--surface)',
color: 'var(--text)',
boxShadow: '0 8px 32px -8px rgba(0,0,0,0.2)',
border: '1px solid var(--line)'
},
classNames: {
success: '!bg-wefin-mint !text-white !border-wefin-mint-deep/30',
error: '!bg-wefin-red !text-white !border-wefin-red/30',
warning: '!bg-wefin-amber !text-white !border-wefin-amber/30'
}
}}
offset={20}
/>
</QueryClientProvider>
</HelmetProvider>
</ErrorBoundary>
)
}
Expand Down
8 changes: 8 additions & 0 deletions src/features/news-feed/ui/cluster-detail-content.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useParams } from 'react-router-dom'

import SeoHead from '@/shared/ui/seo-head'

import { useClusterDetailQuery } from '../model/use-cluster-detail-query'
import { useMarkClusterRead } from '../model/use-mark-cluster-read'
import ClusterDetailFooter from './cluster-detail-footer'
Expand Down Expand Up @@ -55,6 +57,12 @@ export default function ClusterDetailContent() {

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}`}
type="article"
/>
<ClusterDetailHeader cluster={cluster} />
<ClusterDetailSections sections={cluster.sections} articleContent={cluster.articleContent} />
<ClusterDetailFooter cluster={cluster} />
Expand Down
Loading
Loading