실측(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
43 lines
2.0 KiB
TypeScript
43 lines
2.0 KiB
TypeScript
import {index, layout, route, type RouteConfig} from '@react-router/dev/routes';
|
|
|
|
/**
|
|
* 라우트 표. 예전 `app/router.tsx` 의 배열이 여기로 왔다.
|
|
*
|
|
* ★ 순서·경로는 그대로다. 바뀐 건 선언 방식뿐이고, 페이지 컴포넌트는 손대지 않았다.
|
|
* ★ 프리렌더 대상은 여기가 아니라 `react-router.config.ts` 가 정한다.
|
|
* 목록에 없는 경로는 지금까지처럼 SPA 폴백으로 나간다.
|
|
*/
|
|
export default [
|
|
// 비로그인 = 랜딩. ★ 예전엔 로그인 여부로 갈랐는데 지금은 둘 다 랜딩이다
|
|
// (2026-09-04, 사장님 지적: 로그인하면 랜딩·요금에 갈 길이 없었다).
|
|
index('pages/LandingPage.tsx'),
|
|
|
|
route('login', 'pages/LoginPage.tsx'),
|
|
// 로그인 화면의 [회원가입] 이 여기로 온다. 이 줄이 없으면 링크는 있고 목적지만 404 다.
|
|
route('signup', 'pages/SignupPage.tsx'),
|
|
|
|
// 로그인 전 화면. ★ 랜딩과 같은 껍데기(MarketingShell)를 쓴다 — 사이드바 없는 문서형이다.
|
|
route('pricing', 'pages/PricingPage.tsx'),
|
|
route('showcase', 'pages/ShowcasePage.tsx'),
|
|
|
|
// 로그인한 사장님의 홈. 예전엔 페이지마다 <RequireAuth> 로 감쌌는데,
|
|
// 가드는 한 자리에 있어야 빠뜨리지 않아서 레이아웃 라우트로 모았다.
|
|
layout('components/layout/RequireAuthLayout.tsx', [
|
|
route('sites', 'pages/SitesPage.tsx'),
|
|
route('account', 'pages/AccountPage.tsx'),
|
|
]),
|
|
|
|
/**
|
|
* 빌더는 로그인 화면을 앞에 세우지 않는다 — 위저드를 열자마자 로그인부터 만나면
|
|
* 만들어 보기도 전에 막힌다. 세션은 useAutoLogin() 이 조용히 확보한다.
|
|
*/
|
|
route('builder', 'pages/BuilderPage.tsx'),
|
|
|
|
// 개발 빌드에서만 열린다. 운영 번들에는 라우트 자체가 없다.
|
|
...(process.env.NODE_ENV !== 'production'
|
|
? [route('dev/showcase', 'pages/DevShowcasePage.tsx')]
|
|
: []),
|
|
|
|
route('*', 'pages/NotFoundPage.tsx'),
|
|
] satisfies RouteConfig;
|