Compare commits

...

1 Commits

Author SHA1 Message Date
Haewon Kam
13f8c362c2 feat: 랜딩 AI 크롤러 가독성 — SSR 프리렌더 + 메타/JSON-LD + robots/sitemap/llms.txt
- src/entry-server.tsx + scripts/prerender.ts: 빌드 시 / 를 renderToString으로 정적 HTML 주입 (크롤러 본문 4단어 → 800+단어)
- main.tsx: 프리렌더된 랜딩은 hydrateRoot, 나머지 라우트는 createRoot
- index.html: lang=ko, description/OG/canonical, Organization·SoftwareApplication·WebSite JSON-LD
- public/: robots.txt(AI 크롤러 명시 허용), sitemap.xml, llms.txt (SPA 리라이트로 index.html 반환되던 문제 해소)
- Badge.tsx: 메인 체크아웃에만 있고 미커밋이던 컴포넌트 추가 (빌드 실패 원인)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 15:43:47 +09:00
9 changed files with 275 additions and 6 deletions

View File

@ -1,9 +1,64 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="ko">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>INFINITH - Infinite Marketing</title> <title>INFINITH - 성형외과 AI 마케팅 전략 플랫폼</title>
<meta name="description" content="INFINITH는 성형외과·피부과의 유튜브, 인스타그램, 페이스북, 네이버 블로그, 네이버 플레이스, 강남언니 채널을 AI로 분석해 마케팅 진단 리포트, 실행 로드맵, KPI를 제안하는 전략 기획 엔진입니다." />
<meta name="keywords" content="성형외과 마케팅, 피부과 마케팅, 병원 마케팅 분석, AI 마케팅 리포트, 의료 마케팅 전략, INFINITH" />
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large" />
<link rel="canonical" href="https://infinith-demo.vercel.app/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="INFINITH" />
<meta property="og:locale" content="ko_KR" />
<meta property="og:title" content="INFINITH - 성형외과 AI 마케팅 전략 플랫폼" />
<meta property="og:description" content="병원 채널 URL만 입력하면 유튜브·인스타그램·네이버·강남언니를 AI가 분석해 마케팅 진단 리포트와 실행 로드맵을 만듭니다." />
<meta property="og:url" content="https://infinith-demo.vercel.app/" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="INFINITH - 성형외과 AI 마케팅 전략 플랫폼" />
<meta name="twitter:description" content="병원 채널 URL만 입력하면 유튜브·인스타그램·네이버·강남언니를 AI가 분석해 마케팅 진단 리포트와 실행 로드맵을 만듭니다." />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://infinith-demo.vercel.app/#organization",
"name": "INFINITH",
"url": "https://infinith-demo.vercel.app/",
"description": "성형외과·피부과 대상 AI 마케팅 분석 및 전략 기획 플랫폼",
"areaServed": "KR",
"knowsLanguage": ["ko", "en"]
},
{
"@type": "SoftwareApplication",
"@id": "https://infinith-demo.vercel.app/#software",
"name": "INFINITH",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"url": "https://infinith-demo.vercel.app/",
"publisher": { "@id": "https://infinith-demo.vercel.app/#organization" },
"description": "성형외과·피부과의 유튜브, 인스타그램, 페이스북, 네이버 블로그, 네이버 플레이스, 강남언니 채널을 AI로 분석해 마케팅 진단 리포트, 실행 로드맵, KPI를 제안합니다.",
"featureList": [
"다채널 마케팅 데이터 자동 수집 (YouTube, Instagram, Facebook, 네이버 블로그, 네이버 플레이스, 강남언니)",
"AI 마케팅 진단 리포트 생성",
"채널별 변환 전략 및 실행 로드맵 제안",
"KPI 설계 및 성과 분석",
"AI 콘텐츠 스튜디오 (이미지 생성)"
],
"offers": { "@type": "Offer", "url": "https://infinith-demo.vercel.app/pricing", "priceCurrency": "KRW" }
},
{
"@type": "WebSite",
"@id": "https://infinith-demo.vercel.app/#website",
"url": "https://infinith-demo.vercel.app/",
"name": "INFINITH",
"inLanguage": "ko",
"publisher": { "@id": "https://infinith-demo.vercel.app/#organization" }
}
]
}
</script>
<!-- Self-hosted Pretendard Variable preload (CDN 차단 망 대비) --> <!-- Self-hosted Pretendard Variable preload (CDN 차단 망 대비) -->
<link rel="preload" href="/fonts/PretendardVariable.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/PretendardVariable.woff2" as="font" type="font/woff2" crossorigin />
<!-- Google Fonts: Inter + Playfair Display (영문 — fallback 안전) --> <!-- Google Fonts: Inter + Playfair Display (영문 — fallback 안전) -->

View File

@ -5,7 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port=3000 --host=0.0.0.0", "dev": "vite --port=3000 --host=0.0.0.0",
"build": "vite build", "build": "vite build && vite build --ssr src/entry-server.tsx --outDir dist/server && tsx scripts/prerender.ts",
"preview": "vite preview", "preview": "vite preview",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"lint": "tsc --noEmit", "lint": "tsc --noEmit",

25
public/llms.txt Normal file
View File

@ -0,0 +1,25 @@
# INFINITH
> 성형외과·피부과를 위한 AI 마케팅 분석 및 전략 기획 플랫폼. 병원의 유튜브, 인스타그램, 페이스북, 네이버 블로그, 네이버 플레이스, 강남언니 채널을 자동 수집·분석해 마케팅 진단 리포트, 채널별 변환 전략, 실행 로드맵, KPI를 제안합니다.
## 핵심 기능
- 다채널 자동 수집: 병원 홈페이지 URL 하나로 SNS 채널을 발견하고 YouTube Data API, Instagram/Facebook, Naver API, 강남언니 데이터를 실측 수집
- AI 진단 리포트: 채널별 감사(Audit), 강점·약점, 경쟁 포지셔닝을 정량 수치와 함께 제시
- 전략 기획: 채널별 변환 전략, 90일 실행 로드맵, KPI 설계
- 콘텐츠 스튜디오: AI 이미지 생성으로 콘텐츠 제작 지원
- 성과 분석: 배포 이후 채널 성과 추적
## 대상
- 서울 강남권을 포함한 국내 성형외과·피부과
- 프리미엄 의료 마케팅을 내부 역량으로 운영하려는 병원 마케팅 담당자와 원장
## 페이지
- [홈](https://infinith-demo.vercel.app/): 서비스 소개, 대상, 문제 정의, 해결 방식, 핵심 모듈, 활용 사례
- [요금제](https://infinith-demo.vercel.app/pricing): 월간·연간 요금
## 연락
- 이메일: team.backstageai@gmail.com

28
public/robots.txt Normal file
View File

@ -0,0 +1,28 @@
# INFINITH — AI 크롤러 및 검색엔진 전면 허용
User-agent: *
Allow: /
Disallow: /report/loading
Disallow: /api-dashboard
Disallow: /data-validation
# AI 답변엔진 크롤러 (명시 허용)
User-agent: GPTBot
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: ChatGPT-User
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: anthropic-ai
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Yeti
Allow: /
Sitemap: https://infinith-demo.vercel.app/sitemap.xml

15
public/sitemap.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://infinith-demo.vercel.app/</loc>
<lastmod>2026-08-28</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://infinith-demo.vercel.app/pricing</loc>
<lastmod>2026-08-28</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>

19
scripts/prerender.ts Normal file
View File

@ -0,0 +1,19 @@
// vite build(클라이언트) + vite build --ssr(서버) 이후 실행.
// dist/server/entry-server.js 의 render()로 랜딩 HTML을 만들어 dist/index.html 의 #root 에 주입한다.
import { readFileSync, writeFileSync, rmSync } from 'node:fs';
import { resolve } from 'node:path';
const dist = resolve(process.cwd(), 'dist');
const template = readFileSync(resolve(dist, 'index.html'), 'utf-8');
const { render } = await import(resolve(dist, 'server/entry-server.js'));
const routes = ['/'];
for (const url of routes) {
const appHtml = render(url);
const html = template.replace('<div id="root"></div>', `<div id="root">${appHtml}</div>`);
const out = url === '/' ? 'index.html' : `${url.replace(/^\//, '')}/index.html`;
writeFileSync(resolve(dist, out), html);
const words = appHtml.replace(/<[^>]*>/g, ' ').split(/\s+/).filter(Boolean).length;
console.log(`[prerender] ${url} -> dist/${out} (${words} words)`);
}
rmSync(resolve(dist, 'server'), { recursive: true, force: true });

96
src/components/Badge.tsx Normal file
View File

@ -0,0 +1,96 @@
/**
* Badge / .
*
* 3 variant:
* - 'available' Product 1.0 (Modules·Pricing )
* - 'roadmap' Q4 2026 (Mock/)
* - 'popular' Pricing
*
* :
* - src/components/Modules.tsx
* - src/pages/PricingPage.tsx INTELLIGENCE "가장 인기있는 플랜"
*/
import { CheckCircle2, Clock, Sparkles } from 'lucide-react';
export type BadgeVariant = 'available' | 'roadmap' | 'popular';
interface BadgeProps {
variant: BadgeVariant;
/** 직접 라벨을 주면 variant 기본값을 오버라이드 */
label?: string;
size?: 'sm' | 'md';
className?: string;
}
/**
* variant별 & .
*
* "랜딩 전체의 시각적 위계" .
* - available: 편안한 ( , '확인됨' )
* - roadmap: 낮은 ( )
* - popular: accent ( , 1 )
*
* starting point입니다. .
*/
const variantStyles: Record<
BadgeVariant,
{ bg: string; text: string; border: string; Icon: typeof CheckCircle2; defaultLabel: string }
> = {
available: {
// TODO(user): 초록 계열 vs accent 파스텔 톤 중 선택. 현재는 청록 파스텔.
bg: 'bg-emerald-50/80',
text: 'text-emerald-700',
border: 'border-emerald-200/60',
Icon: CheckCircle2,
defaultLabel: 'Available Now',
},
roadmap: {
// TODO(user): 회색 톤 강도. 너무 흐리면 안 보이고, 너무 진하면 주목받음.
bg: 'bg-slate-100/80',
text: 'text-slate-500',
border: 'border-slate-200/60',
Icon: Clock,
defaultLabel: 'Q4 2026 Roadmap',
},
popular: {
// TODO(user): 단색 vs 그라디언트. Modules와 겹치지 않게 Pricing 전용 포인트 컬러 권장.
bg: 'bg-gradient-to-r from-accent/10 to-purple-200/40',
text: 'text-accent',
border: 'border-accent/30',
Icon: Sparkles,
defaultLabel: '가장 인기있는 플랜',
},
};
const sizeStyles = {
sm: 'text-xs px-2 py-0.5 gap-1',
md: 'text-sm px-3 py-1 gap-1.5',
} as const;
const iconSize = { sm: 12, md: 14 } as const;
export default function Badge({
variant,
label,
size = 'sm',
className = '',
}: BadgeProps) {
const { bg, text, border, Icon, defaultLabel } = variantStyles[variant];
return (
<span
className={[
'inline-flex items-center rounded-full border backdrop-blur-sm font-medium',
bg,
text,
border,
sizeStyles[size],
className,
].join(' ')}
>
<Icon size={iconSize[size]} strokeWidth={2.5} />
<span>{label ?? defaultLabel}</span>
</span>
);
}

23
src/entry-server.tsx Normal file
View File

@ -0,0 +1,23 @@
// 빌드 시 랜딩(/)만 정적 HTML로 프리렌더하는 서버 엔트리.
// AI 크롤러(GPTBot/ClaudeBot/PerplexityBot)는 JS를 실행하지 않으므로
// 본문이 HTML에 직접 들어 있어야 인용 가능하다. scripts/prerender.ts에서 호출.
import { StrictMode } from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router';
import { Routes, Route } from 'react-router';
import App from './App.tsx';
import LandingPage from './pages/LandingPage.tsx';
export function render(url: string): string {
return renderToString(
<StrictMode>
<StaticRouter location={url}>
<Routes>
<Route element={<App />}>
<Route index element={<LandingPage />} />
</Route>
</Routes>
</StaticRouter>
</StrictMode>,
);
}

View File

@ -1,5 +1,5 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot, hydrateRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router'; import { BrowserRouter, Routes, Route } from 'react-router';
import App from './App.tsx'; import App from './App.tsx';
import LandingPage from './pages/LandingPage.tsx'; import LandingPage from './pages/LandingPage.tsx';
@ -17,7 +17,8 @@ import ApiDashboardPage from './pages/ApiDashboardPage.tsx';
import LoginPage from './pages/LoginPage.tsx'; import LoginPage from './pages/LoginPage.tsx';
import './index.css'; import './index.css';
createRoot(document.getElementById('root')!).render( const container = document.getElementById('root')!;
const app = (
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
@ -39,5 +40,12 @@ createRoot(document.getElementById('root')!).render(
</Route> </Route>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</StrictMode>, </StrictMode>
); );
// 랜딩(/)은 빌드 시 프리렌더된 HTML이 들어 있으므로 hydrate, 나머지는 클라이언트 렌더
if (container.hasChildNodes()) {
hydrateRoot(container, app);
} else {
createRoot(container).render(app);
}