공개 명함 문서 배포
공개 URL은 /card/{slug}?source=share&v={revision}이다. 브라우저와 크롤러 모두 같은 URL을 사용한다.
라우팅
현재 동적 OG HTML은 백엔드 PublicCardDocument 모듈이 만든다. reverse proxy는 다음 순서로
라우팅해야 한다.
/card/* -> taple-be
/v1/* -> taple-be
/assets/* -> 이 저장소의 dist/assets
그 외 -> 이 저장소의 정적 SPA위 순서는 배포 전 필수 조건이다. 특히 /card/*와 /assets/*를 SPA fallback보다 먼저 처리해야
한다. 존재하지 않는 /assets/*는 HTML로 fallback하지 않고 404를 반환한다. 고정 엔트리
app.js·app.css는 no-cache 또는 재검증 정책을 사용하고, hash가 붙은 chunk와 font만
immutable로 캐시한다. 이 정책을 처음 적용할 때는 Cloudflare에 남아 있는 기존 고정 자산
캐시도 함께 제거한다.
운영 진입점은 홈서버 Caddy다. 저장소 변수 PUBLIC_CARD_SMOKE_SLUG에 항상 공개 상태인 QA 명함
slug를 등록하면 배포 직후 /card/{slug}가 백엔드 HTML·OG·고정 앱 자산을 함께 반환하는지
확인한다. 변수가 없으면 앱과 고정 자산만 검사하고 /card 검사는 경고와 함께 건너뛴다.
pnpm build는 문서 서버가 참조하는 /assets/app.js, /assets/app.css를 만든다. 이 두 엔트리는
배포마다 교체하고 CDN에서 재검증한다. 해시가 붙은 route chunk와 font는 장기 캐시해도 된다.
빌드 마지막에 verify-public-card-assets.mjs가 다음 계약을 검증한다. 하나라도 어기면
빌드를 실패 처리한다.
dist/assets/app.js,dist/assets/app.css가 빈 파일이 아닌지dist/spa.html이 두 고정 자산을 참조하는지#app[data-prerendered="false"],#modal-root가 있는지
브라우저 계약
- 공개 전체 API의
card.shareUrl을 사이트 origin과 결합해 공유한다. /card/$slug는source=nfc|share와 양의 정수v만 받아들인다.source=share방문은 analyticssourceType=SHARE, URL의v는shareRevision으로 전송한다.v는 과거 스냅샷 조회 키가 아니다. 백엔드는 항상 현재 공개 상태를 다시 확인한다.- NFC resolve의 네트워크/5xx는 사용 불가로 이동하지 않고 재시도 화면을 보여준다.
/card/*에서 React가 실행되어도 공통 SEO effect는 백엔드가 생성한 title, Open Graph,
Twitter, canonical을 덮어쓰지 않는다. 해당 문서에서 다른 SPA 경로로 이동할 때는 명함별
og:image, twitter:image를 제거하고 공통 메타로 복귀한다.
CSP 호환성 확인
현재 React 앱은 Emotion이 런타임에 생성하는 <style>을 사용한다. 백엔드 /card/* 응답이
style-src 'self'만 허용하면 해당 스타일은 차단되고 명함 레이아웃이 깨진다. 따라서 운영 배포
전에 백엔드 문서의 nonce를 Emotion cache에 전달하거나, 합의한 style-src 정책으로 두 계층을
반드시 맞춰야 한다. 정적 빌드만으로 확인할 수 없으므로 실제 /card/{slug}에서도 다음을 검사한다.
- 브라우저 콘솔에 CSP style 차단 오류가 없는지
- 명함의 색상·간격·레이아웃이 정상 적용되는지
프론트 빌드에서 CSP를 임의로 완화하지 않는다.
로컬 개발에서는 Vite가 SPA를 제공하므로 동적 OG HTML을 직접 확인하려면 백엔드의
PUBLIC_ASSET_ORIGIN=http://localhost:3000, PUBLIC_SPA_SCRIPT_PATH=/src/app/main.tsx 설정과 함께
http://localhost:8080/card/{slug}를 연다.