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
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# 이 파일을 .env.local 로 복사한 뒤 필요한 값만 채우세요.
# 정적 export 사이트이므로 모든 값은 빌드 시점에 번들에 박힙니다. 값을 바꾸면 다시 빌드해야 합니다.
# 정적 export 사이트이므로 설정값은 빌드 시점에 번들에 박힙니다. 값을 바꾸면 다시 빌드해야 합니다.
# NEXT_PUBLIC_BUILD_VERSION은 pnpm build가 내부적으로 생성하는 배포 식별자입니다.
# 사용자 설정값이 아니므로 .env.local에 직접 넣지 마세요.

# ─────────────────────────────────────────────────────────────
# 사이트
Expand Down
6 changes: 4 additions & 2 deletions DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ docker run -d -p 8080:8080 true-log

secret 마운트는 빌드 중에만 존재하고 이미지 레이어에도 빌드 캐시에도 남지 않습니다. 프로덕션 빌드에는 `NEXT_PUBLIC_SITE_URL`이 필수이며 미설정 시 실패합니다. 개발 서버에서만 localhost 기본값을 사용합니다.

`NEXT_PUBLIC_BUILD_VERSION`은 사용자 설정값이 아닙니다. `pnpm build`가 실행될 때 `scripts/build.mjs`가 새 배포 식별자를 생성해 클라이언트 번들에 주입하고, 같은 값을 `out/build-version.json`과 `out/sw.js`에 반영합니다. 세 산출물이 같은 배포 버전을 공유해야 열린 탭의 업데이트 감지와 서비스 워커 캐시 교체가 안전하게 동작합니다.

## 캐시 정책

파일명이 만들어지는 방식에 따라 갈립니다.
Expand Down Expand Up @@ -181,6 +183,7 @@ Content-Type html·txt·xml·png·svg·js 정상, webmanifest → application/
오프라인일 때 해당 앱의 캐시로 복구합니다. 캐시는 최대 128개 항목이며,
오프라인 안내 페이지를 보존합니다. 방문한 모든 페이지의 영구 보관을 보장하지 않습니다.
새 워커는 자신의 scope에 속하는 이전 버전 캐시만 정리합니다.
`build-version.json`과 `sw.js`는 서비스 워커 캐시 대상에서 제외해 항상 현재 배포를 확인합니다.

열린 탭은 자신의 빌드 버전과 `/build-version.json`을 비교합니다. load 이후,
온라인 복귀, 탭 재활성화 및 5분 주기로 확인하며 숨겨진 탭에서는 건너뜁니다.
Expand All @@ -191,7 +194,6 @@ Content-Type html·txt·xml·png·svg·js 정상, webmanifest → application/
이전 버전의 페이지를 이미 열어 둔 방문자는 새 알림 코드가 없으므로 최초 전환 때는
한 번 페이지를 다시 열어야 합니다. 이후 배포부터 열린 탭의 버전 비교가 동작합니다.


## 배포 검증과 복구

PR CI는 production export를 빌드한 후 Chromium에서 실제 Pagefind 검색과 글 이동,
Expand Down Expand Up @@ -226,4 +228,4 @@ pnpm test:e2e

예약 배포가 며칠 동안 실행되지 않거나 실패한 경우 Actions의 예약 실행 상태와 로그를
확인합니다. 예약 글 공개는 성공한 재빌드 시점에 이루어지므로 정확한 분 단위 발행은
보장하지 않습니다.
보장하지 않습니다.
33 changes: 21 additions & 12 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ Node 22 이상, pnpm 은 `package.json` 의 `packageManager` 필드에서 버전
| 명령 | 설명 |
| --- | --- |
| `pnpm dev` | 개발 서버 |
| `pnpm build` | 정적 export → `out/`. 이어서 `postbuild` 가 검색 색인을 만듭니다 |
| `pnpm build` | 배포 버전 생성 → 정적 export → PWA 산출물 버전 주입 → `postbuild` 에서 Pagefind 색인 생성·검증 |
| `pnpm test` | Vitest |
| `pnpm test:e2e` | production 산출물을 대상으로 하는 Playwright 브라우저 스모크 테스트 |
| `pnpm typecheck` | `tsc --noEmit` |
| `pnpm lint` | Biome 검사 |
| `pnpm format` | Biome 자동 수정 |

네 가지(lint·typecheck·test·build)는 [CI](.github/workflows/ci.yml) 에서 모든 PR 에 대해 돌아갑니다.
CI에서는 모든 PR에 대해 lint, typecheck, 단위 테스트, production build, Playwright 브라우저 스모크 테스트를 실행합니다. 브라우저 테스트 실패 시 trace를 workflow artifact로 보관합니다.

## 구조

Expand All @@ -35,10 +36,10 @@ content/posts/ 글 (Markdown)
src/
app/ 라우트. (site) 그룹이 공통 레이아웃을 감쌉니다
lib/markdown/ Markdown 파이프라인 — 호스트 비의존
lib/ 파일 IO·캐시·RSS·SEO 등 앱 계층
features/ post-toc · post-diagram · search · theme
lib/ 글 IO·컬렉션·배포 경로·RSS·SEO 등 애플리케이션 로직
features/ post-toc · post-diagram · pwa · search · theme
components/ UI. ui/ 는 shadcn, typography.tsx 는 타이포 프리미티브
config/ site · navigation · integrations
config/ site · navigation · integrations · deployment 검증
deploy/nginx.conf 배포용 nginx 설정
```

Expand All @@ -48,30 +49,38 @@ deploy/nginx.conf 배포용 nginx 설정

MDX 를 쓰지 않는 이유도 같습니다. 글이 JSX 를 품는 순간 React 없이는 렌더할 수 없어집니다.

**전체 정적 export 입니다.** `output: "export"` 라 서버가 없습니다. 이 제약이 몇 가지를 결정합니다 — CSP nonce 를 쓸 수 없고([DEPLOY.md](DEPLOY.md) 참고), RSS 는 `force-static` 라우트로 만들며, 검색 색인은 빌드된 HTML 을 읽어야 해서 `postbuild` 단계에 있습니다.
**전체 정적 export 입니다.** `output: "export"` 라 애플리케이션 서버가 없습니다. 이 제약이 몇 가지를 결정합니다 — CSP nonce 를 쓸 수 없고([DEPLOY.md](DEPLOY.md) 참고), RSS 는 `force-static` 라우트로 만들며, 검색 색인은 빌드된 HTML 을 읽어야 해서 export 이후에 생성합니다.

**빌드는 페이지 번들·업데이트 엔드포인트·서비스 워커가 공유하는 하나의 배포 버전을 만듭니다.** `scripts/build.mjs` 가 버전을 생성해 클라이언트 번들에 `NEXT_PUBLIC_BUILD_VERSION` 으로 주입하고, `out/build-version.json` 을 만든 뒤 서비스 워커의 버전 마커를 치환합니다. 이 값은 사용자 설정이 아니라 내부 빌드 메타데이터입니다.

**PWA 는 production 사이트의 일부입니다.** 웹 앱 매니페스트를 제공하고 production 에서만 서비스 워커를 등록합니다. 서비스 워커는 내비게이션, immutable Next 에셋, 폰트, KaTeX, Pagefind, RSC 페이로드를 대상으로 오프라인 복구와 제한된 캐시를 제공합니다. 열린 탭은 자기 번들에 박힌 배포 버전과 `build-version.json` 을 주기적으로 비교하고, 새 배포를 감지하면 오래된 자산과 새 자산을 섞어 쓰는 대신 새로고침 안내를 표시합니다.

**다이어그램은 클라이언트에서, 필요할 때만 그립니다.** Mermaid 를 서버에서 렌더하려면 헤드리스 브라우저가 필요합니다. 대신 다이어그램이 있는 글에서만, 그것도 화면에 들어올 때 `IntersectionObserver` 로 모듈을 불러옵니다.

**본문 HTML 은 `rehype-sanitize` 를 거칩니다.** `dangerouslySetInnerHTML` 에 넘길 수 있는 값은 `SanitizedHtml` 브랜드 타입으로 좁혀, 정제를 건너뛴 문자열이 들어가면 타입 검사에서 막힙니다.

## 설정

모든 값은 선택이며 빌드 시점에 번들에 박힙니다. `.env.example` 을 `.env.local` 로 복사해 쓰세요.
설정값은 빌드 시점에 번들에 박힙니다. `.env.example` 을 `.env.local` 로 복사해 쓰세요.

| 변수 | 용도 |
| --- | --- |
| `NEXT_PUBLIC_SITE_URL` | canonical·Open Graph·sitemap·RSS 의 절대 URL 기준 |
| `NEXT_PUBLIC_GISCUS_*` | 댓글. 다섯 값을 전부 채우거나 전부 비워야 합니다 |
| `NEXT_PUBLIC_GA_MEASUREMENT_ID` | Google Analytics 4 |
| `NEXT_PUBLIC_KAKAO_JS_KEY` | Kakao 공유 버튼(카카오톡). Kakao Developers 앱의 JavaScript 키 |
| `NEXT_PUBLIC_SITE_URL` | production 에서 필수. canonical·Open Graph·sitemap·RSS 의 절대 URL 기준 |
| `NEXT_PUBLIC_BASE_PATH` | 루트 배포 계약. 비우거나 `/`만 허용하며 하위 경로 배포는 거부 |
| `NEXT_PUBLIC_GISCUS_*` | 선택 댓글. 다섯 값을 전부 채우거나 전부 비워야 함 |
| `NEXT_PUBLIC_GA_MEASUREMENT_ID` | 선택 Google Analytics 4 |
| `NEXT_PUBLIC_KAKAO_JS_KEY` | 선택 Kakao 공유 버튼(카카오톡). Kakao Developers 앱의 JavaScript 키 |

`NEXT_PUBLIC_BUILD_VERSION` 은 설정값으로 취급하지 않습니다. `pnpm build` 가 PWA 업데이트 조정을 위해 내부적으로 생성합니다.

## 문서

| 문서 | 내용 |
| --- | --- |
| [DESIGN.md](DESIGN.md) | 색상·타이포·간격·컴포넌트 규칙 |
| [CONTENT.md](CONTENT.md) | 프런트매터 스키마와 글 작성 규칙 |
| [DEPLOY.md](DEPLOY.md) | 컨테이너 배포, 캐시 정책, 보안 헤더 |
| [DEPLOY.md](DEPLOY.md) | GitHub Pages·컨테이너 배포, PWA/캐시 정책, 보안 헤더, 복구 절차 |
| [SECURITY.md](SECURITY.md) | 의존성 검토 상태와 배포 구조 기준 보안 평가 |

## 서드파티 자산

Expand Down
33 changes: 21 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ Posts live in `content/posts/<slug>.md`, where the filename is the URL. The dev
| Command | Description |
| --- | --- |
| `pnpm dev` | Development server |
| `pnpm build` | Static export to `out/`, then `postbuild` builds the search index |
| `pnpm build` | Generate a deployment version, run the static export to `out/`, write versioned PWA artifacts, then build and verify the Pagefind index in `postbuild` |
| `pnpm test` | Vitest |
| `pnpm test:e2e` | Playwright browser smoke tests against the production export |
| `pnpm typecheck` | `tsc --noEmit` |
| `pnpm lint` | Biome check |
| `pnpm format` | Biome autofix |

All four gates — lint, typecheck, test, build — run on every pull request in [CI](.github/workflows/ci.yml).
CI runs lint, typecheck, unit tests, the production build, and Playwright browser smoke tests on every pull request. Failure traces from the browser tests are preserved as workflow artifacts.

## Layout

Expand All @@ -35,10 +36,10 @@ content/posts/ Posts (Markdown)
src/
app/ Routes. The (site) group wraps the shared layout
lib/markdown/ Markdown pipeline — host independent
lib/ App layer: file IO, caching, RSS, SEO
features/ post-toc · post-diagram · search · theme
lib/ Application logic: post IO, collections, deployment paths, RSS, SEO
features/ post-toc · post-diagram · pwa · search · theme
components/ UI. ui/ is shadcn, typography.tsx holds the type primitives
config/ site · navigation · integrations
config/ site · navigation · integrations · deployment validation
deploy/nginx.conf nginx configuration for deployment
```

Expand All @@ -48,30 +49,38 @@ deploy/nginx.conf nginx configuration for deployment

MDX is absent for the same reason. The moment a post contains JSX, it cannot be rendered without React.

**Everything is a static export.** `output: "export"` means there is no server, and that constraint decides several things: CSP nonces are impossible (see [DEPLOY.md](DEPLOY.md)), the RSS feed is a `force-static` route, and the search index runs in `postbuild` because it has to read the built HTML.
**Everything is a static export.** `output: "export"` means there is no application server. That constraint decides several things: CSP nonces are impossible (see [DEPLOY.md](DEPLOY.md)), the RSS feed is a `force-static` route, and the search index runs after the HTML export because Pagefind has to read the built pages.

**The build creates one deployment version for the page bundle, update endpoint and service worker.** `scripts/build.mjs` generates the version, exposes it to the client bundle as `NEXT_PUBLIC_BUILD_VERSION`, writes `out/build-version.json`, and replaces the service worker version marker before `postbuild` creates the search index. The variable is internal build metadata and is not a user configuration value.

**PWA support is part of the production site.** The app ships a web manifest and registers a service worker only in production. The worker provides offline fallback and controlled caching for navigation, immutable Next assets, fonts, KaTeX, Pagefind and RSC payloads. Open tabs periodically compare their embedded deployment version with `build-version.json`; when a new deployment is detected, the UI offers a refresh instead of silently serving a mixed old/new asset set.

**Diagrams render on the client, and only when needed.** Rendering Mermaid on the server would require a headless browser. Instead the module loads only on posts that contain a diagram, and only once that diagram scrolls into view via `IntersectionObserver`.

**Post HTML passes through `rehype-sanitize`.** The value accepted by `dangerouslySetInnerHTML` is narrowed to a `SanitizedHtml` branded type, so a string that skipped sanitization fails type checking.

## Configuration

Every value is optional and is baked into the bundle at build time. Copy `.env.example` to `.env.local`.
Configuration values are baked into the bundle at build time. Copy `.env.example` to `.env.local`.

| Variable | Purpose |
| --- | --- |
| `NEXT_PUBLIC_SITE_URL` | Base for absolute URLs in canonical tags, Open Graph, sitemap and RSS |
| `NEXT_PUBLIC_GISCUS_*` | Comments. Set all five values or leave all five empty |
| `NEXT_PUBLIC_GA_MEASUREMENT_ID` | Google Analytics 4 |
| `NEXT_PUBLIC_KAKAO_JS_KEY` | KakaoTalk share button. JavaScript key from a Kakao Developers app |
| `NEXT_PUBLIC_SITE_URL` | Required for production. Base for absolute URLs in canonical tags, Open Graph, sitemap and RSS |
| `NEXT_PUBLIC_BASE_PATH` | Root deployment contract. Leave empty (or `/`); subpath deployments are rejected |
| `NEXT_PUBLIC_GISCUS_*` | Optional comments. Set all five values or leave all five empty |
| `NEXT_PUBLIC_GA_MEASUREMENT_ID` | Optional Google Analytics 4 |
| `NEXT_PUBLIC_KAKAO_JS_KEY` | Optional KakaoTalk share button. JavaScript key from a Kakao Developers app |

`NEXT_PUBLIC_BUILD_VERSION` is intentionally not listed as configuration: `pnpm build` generates it internally for PWA update coordination.

## Documentation

| Document | Contents |
| --- | --- |
| [DESIGN.md](DESIGN.md) | Color, typography, spacing and component rules |
| [CONTENT.md](CONTENT.md) | Frontmatter schema and authoring rules |
| [DEPLOY.md](DEPLOY.md) | Container deployment, cache policy, security headers |
| [DEPLOY.md](DEPLOY.md) | GitHub Pages and container deployment, PWA/cache policy, security headers and recovery |
| [SECURITY.md](SECURITY.md) | Dependency review status and deployment-specific security assessment |

## Third-party assets

Expand Down