실측(2026-09-07): `curl /` 가 3,021바이트에 본문 0자·`<a>` 0개였다. 같은 호스트의
발행본은 48,072바이트다. 구글은 JS 를 실행하지만 **렌더링 큐가 따로** 돌고 신규
도메인은 뒤로 밀린다 — 그동안 색인에는 "제목만 있고 내용 없는 페이지"로 들어가 있다.
서치콘솔이 "URL이 Google에 등록되어 있음"이라고 답하면서도 브랜드명 검색에조차 안
걸리던 이유다.
스크립트를 새로 짜지 않았다. react-router 7.17 에 프리렌더가 내장돼 있고
`ssr: false` 와 함께 쓰면 런타임 Node 서버 없이 지정한 경로만 HTML 로 굽는다 —
나머지는 지금까지처럼 SPA 폴백이다. 배포 구조가 그대로다.
- react-router.config.ts: `ssr:false` + `prerender: ['/', '/pricing', '/showcase']`.
로그인 뒤에만 의미가 있는 화면은 굽지 않는다(구울 내용이 사용자별이다)
- src/root.tsx · src/routes.ts: 예전 index.html + app/router.tsx 가 하던 일.
가드는 페이지마다 감싸지 않고 RequireAuthLayout 레이아웃 라우트 하나로 모았다
- 랜딩·요금·사례에 meta export: 제목을 브랜드가 아니라 **검색어**로 시작하게 바꿨다.
예전 제목("Web4Ai · AI 웹 빌더")에는 사람이 치는 말이 한 단어도 없었다.
랜딩에 Organization JSON-LD 추가 — 발행본에는 있는데 정작 랜딩엔 없었다
- src/lib/site.ts: 발행 호스트의 단일 출처. 모듈 최상위의 `window.location` 폴백을
전부 걷었다 — 서버 번들은 라우트를 한 파일로 묶어서 프리렌더 대상이 아닌 화면의
최상위 코드도 빌드 때 실행된다(실측: BuilderPage 에서 빌드가 죽었다)
- LoginPage: homePath 기본값 `/` → `/sites`. 예전엔 router.tsx 가 넘기던 값이라
라우트 모듈로 옮기면서 그대로 두면 로그인 후 랜딩으로 갔다
- nginx: SPA 폴백을 `/index.html` → `/__spa-fallback.html`. 프리렌더 뒤로
`/index.html` 은 **랜딩이 구워진 파일**이라, 그리로 넘기면 `/builder` 에 랜딩
HTML 이 내려가고 클라이언트가 다른 주소로 하이드레이트한다
- nginx/Dockerfile: 산출물이 `dist` → `build/client`. 경로가 어긋나면 COPY 가
조용히 빈 디렉토리를 만들고 컨테이너는 정상으로 뜬다
- site/seo/robots.ts: `/builder` `/login` `/signup` `/sites` `/account` Disallow.
이 경로들은 빈 SPA 폴백을 받는다 — 긁히면 호스트 전체에 저품질 신호가 쌓인다
검증: tsc·eslint·react-router build 통과.
랜딩 3,021B → 21,799B, 본문 1,278자, 링크 6개.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fteiJNvAEbTnUKq8fSqoj
127 lines
5.6 KiB
TypeScript
127 lines
5.6 KiB
TypeScript
import {Link} from 'react-router';
|
|
import {Check} from 'lucide-react';
|
|
import {MarketingShell, Section} from '@/components/layout/MarketingShell';
|
|
import {Button} from '@/components/ui/button';
|
|
import type {MetaFunction} from 'react-router';
|
|
import {ORIGIN} from '@/lib/site';
|
|
export const meta: MetaFunction = () => [
|
|
{title: '요금 · 가게 홈페이지 제작 비용 | Web4Ai'},
|
|
{
|
|
name: 'description',
|
|
content: 'Web4Ai 로 가게 홈페이지를 만들고 발행하는 데 드는 비용입니다. 발행 후에도 AI 인용 상태를 계속 확인합니다.',
|
|
},
|
|
{name: 'robots', content: 'index, follow'},
|
|
{tagName: 'link', rel: 'canonical', href: `${ORIGIN}/pricing`},
|
|
{property: 'og:title', content: '요금 · 가게 홈페이지 제작 비용'},
|
|
{property: 'og:url', content: `${ORIGIN}/pricing`},
|
|
];
|
|
|
|
|
|
/**
|
|
* 요금 — 플랜 하나다.
|
|
*
|
|
* ★ 플랜을 여럿 늘어놓지 않는다. 이건 셀프서비스 구독이 아니라 **월 단위로 사람이 붙는
|
|
* 서비스**라 고를 것이 가격대가 아니라 "할 것이냐"뿐이다. 비교표를 만들면 없는 선택지를
|
|
* 지어내게 된다.
|
|
*/
|
|
const INCLUDED = ['AI 노출 진단 리포트', '최적화 가이드', '영상 콘텐츠 2종', '페이지 제작 1회'];
|
|
|
|
const TERMS: {label: string; value: string}[] = [
|
|
{label: '무엇을 넣나', value: '운영 중인 홈페이지'},
|
|
{label: '무엇이 나오나', value: '진단 리포트 + 최적화 가이드 + 페이지'},
|
|
{label: '월 산출물', value: '리포트 1회 · 영상 2종 · 페이지 1회'},
|
|
{label: '확인 절차', value: '리포트 리뷰 미팅'},
|
|
];
|
|
|
|
export function PricingPage() {
|
|
return (
|
|
<MarketingShell>
|
|
<Section>
|
|
<header className="mb-12 text-center">
|
|
<h1 className="text-3xl font-bold tracking-tight text-balance sm:text-4xl">요금</h1>
|
|
<p className="mx-auto mt-3 max-w-lg text-sm text-muted-foreground">
|
|
매달 진단하고, 고치고, 결과를 같이 봅니다.
|
|
</p>
|
|
</header>
|
|
|
|
<div className="mx-auto max-w-2xl rounded-2xl border border-border bg-card p-7 sm:p-9">
|
|
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
|
<h2 className="text-2xl font-bold tracking-tight">Web4AI</h2>
|
|
<p className="text-2xl font-bold tracking-tight">
|
|
70만원
|
|
<span className="ml-1 text-sm font-medium text-muted-foreground">/ 월</span>
|
|
</p>
|
|
</div>
|
|
|
|
<p className="mt-7 text-base font-semibold tracking-tight">AI가 우리를 찾아내게 만듭니다</p>
|
|
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
|
ChatGPT·제미나이·퍼플렉시티 답변에 우리 정보가 인용되도록 홈페이지를 진단하고 고칩니다.
|
|
</p>
|
|
|
|
<ul className="mt-6 flex flex-wrap gap-2">
|
|
{INCLUDED.map((item) => (
|
|
<li
|
|
key={item}
|
|
className="inline-flex items-center gap-1.5 rounded-full border border-border px-3.5 py-1.5 text-[13px]"
|
|
>
|
|
<Check className="size-3.5 text-success" aria-hidden />
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<dl className="mt-8 grid gap-3 border-t border-border pt-7 text-sm sm:grid-cols-[7rem_1fr]">
|
|
{TERMS.map(({label, value}) => (
|
|
<div key={label} className="grid gap-1 sm:col-span-2 sm:grid-cols-subgrid">
|
|
<dt className="text-muted-foreground">{label}</dt>
|
|
<dd className="m-0">{value}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
|
|
<Link to="/builder?new=1" className="mt-8 block">
|
|
<Button variant="primary" size="lg" className="w-full">
|
|
먼저 만들어 보기
|
|
</Button>
|
|
</Link>
|
|
<p className="mt-3 text-center text-xs text-muted-foreground">
|
|
가입하지 않아도 끝까지 만들어 볼 수 있습니다.
|
|
</p>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section muted>
|
|
<h2 className="mb-8 text-xl font-bold tracking-tight">자주 묻는 질문</h2>
|
|
<dl className="grid gap-6 sm:grid-cols-2">
|
|
{[
|
|
{
|
|
q: '네이버 플레이스랑 뭐가 다른가요?',
|
|
a: '네이버는 AI 크롤러를 막습니다. 그래서 네이버에 올린 정보는 ChatGPT 같은 AI가 읽지 못합니다. 이 홈페이지는 AI가 읽을 수 있게 만듭니다.',
|
|
},
|
|
{
|
|
q: '홈페이지가 이미 있는데도 필요한가요?',
|
|
a: '있는 홈페이지가 AI에게 읽히는지가 관건입니다. 화면에는 보이는데 크롤러에게는 빈 페이지인 경우가 많습니다. 진단이 그걸 먼저 봅니다.',
|
|
},
|
|
{
|
|
q: '무엇을 준비해야 하나요?',
|
|
a: '지금 운영 중인 홈페이지 주소 하나면 시작합니다. 없으면 여기서 만드는 것부터 합니다.',
|
|
},
|
|
{
|
|
q: '결과는 어떻게 확인하나요?',
|
|
a: '매달 진단 리포트를 드리고, 리뷰 미팅에서 무엇이 달라졌는지 같이 봅니다.',
|
|
},
|
|
].map(({q, a}) => (
|
|
<div key={q}>
|
|
<dt className="text-sm font-semibold">{q}</dt>
|
|
<dd className="mt-1.5 text-sm leading-relaxed text-muted-foreground">{a}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</Section>
|
|
</MarketingShell>
|
|
);
|
|
}
|
|
|
|
// 라우트 모듈은 default export 를 요구한다(routes.ts 가 이 파일을 가리킨다).
|
|
export default PricingPage;
|