From b455c83995d7e250a367cb9a8d52d4422d815dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=83=81=EB=B9=88?= Date: Sun, 16 Aug 2026 20:21:46 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(search):=20Pagefind=20=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=B0=8F=20basePath=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pagefind 번들 로드 실패(Promise Reject) 시 모듈 레벨 캐시(bundle)를 초기화해 재시도 가능하도록 개선 - 하드코딩된 /pagefind/pagefind.js 경로를 NEXT_PUBLIC_BASE_PATH와 연동하여 GitHub Pages 서브디렉터리 배포 404 방지 - 번들 URL 정규화 및 캐시 초기화에 대한 단위 테스트(tests/pagefind.test.ts) 추가 --- .gitignore | 1 + src/features/search/pagefind.ts | 33 ++++++++++++++++++++--- tests/pagefind.test.ts | 48 +++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/pagefind.test.ts diff --git a/.gitignore b/.gitignore index 7d41533..4d6d564 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ pnpm-debug.log* .codex/ .omo/ CLAUDE.md +.claude/ \ No newline at end of file diff --git a/src/features/search/pagefind.ts b/src/features/search/pagefind.ts index 20ca1ea..4529079 100644 --- a/src/features/search/pagefind.ts +++ b/src/features/search/pagefind.ts @@ -17,15 +17,27 @@ type PagefindApi = Readonly<{ ) => Promise Promise }[] }>> }> -const BUNDLE_URL = "/pagefind/pagefind.js" +const DEFAULT_BUNDLE_PATH = "/pagefind/pagefind.js" const MAX_RESULTS = 20 let bundle: Promise | undefined +/** + * GitHub Pages 서브디렉터리(e.g., /repo/) 배포 환경을 지원하기 위해 + * 환경변수 NEXT_PUBLIC_BASE_PATH 가 지정된 경우 접두사로 조합한다. + */ +export function getPagefindBundleUrl(): string { + // biome-ignore lint/complexity/useLiteralKeys: tsconfig noPropertyAccessFromIndexSignature requires bracket access + const basePath = process.env["NEXT_PUBLIC_BASE_PATH"]?.trim() ?? "" + const normalizedBasePath = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath + + return `${normalizedBasePath}${DEFAULT_BUNDLE_PATH}` +} + // 번들은 postbuild 단계에서 생기므로 개발 서버와 빌드 시점에는 존재하지 않는다. // 정적 경로로 쓰면 번들러가 해석을 시도하다 실패하니 변수와 ignore 주석으로 남긴다. async function loadPagefind(): Promise { - const url = BUNDLE_URL + const url = getPagefindBundleUrl() const api = (await import( /* webpackIgnore: true */ /* turbopackIgnore: true */ url )) as PagefindApi @@ -42,8 +54,16 @@ export async function searchPosts(query: string): Promise { + // 1회 로드 실패(Reject) 시 모듈 레벨 캐시를 리셋하여 후속 검색 요청이 영구 실패하지 않고 재시도할 수 있게 한다. + bundle = undefined + throw error + }) + } + + const api = await bundle + const { results } = await api.search(trimmed) const fragments = await Promise.all(results.slice(0, MAX_RESULTS).map((result) => result.data())) return fragments.map((fragment) => ({ @@ -52,3 +72,8 @@ export async function searchPosts(query: string): Promise { + const originalEnv = process.env + + beforeEach(() => { + _resetPagefindBundleCacheForTesting() + process.env = { ...originalEnv } + }) + + afterEach(() => { + _resetPagefindBundleCacheForTesting() + process.env = originalEnv + vi.restoreAllMocks() + }) + + describe("getPagefindBundleUrl", () => { + it("Given no NEXT_PUBLIC_BASE_PATH When resolving bundle URL Then it returns default root path", () => { + // biome-ignore lint/complexity/useLiteralKeys: tsconfig noPropertyAccessFromIndexSignature requires bracket access + delete process.env["NEXT_PUBLIC_BASE_PATH"] + expect(getPagefindBundleUrl()).toBe("/pagefind/pagefind.js") + }) + + it("Given NEXT_PUBLIC_BASE_PATH without trailing slash When resolving Then it prepends base path", () => { + // biome-ignore lint/complexity/useLiteralKeys: tsconfig noPropertyAccessFromIndexSignature requires bracket access + process.env["NEXT_PUBLIC_BASE_PATH"] = "/blog" + expect(getPagefindBundleUrl()).toBe("/blog/pagefind/pagefind.js") + }) + + it("Given NEXT_PUBLIC_BASE_PATH with trailing slash When resolving Then it normalizes trailing slash", () => { + // biome-ignore lint/complexity/useLiteralKeys: tsconfig noPropertyAccessFromIndexSignature requires bracket access + process.env["NEXT_PUBLIC_BASE_PATH"] = "/blog/" + expect(getPagefindBundleUrl()).toBe("/blog/pagefind/pagefind.js") + }) + }) + + describe("searchPosts cache & recovery", () => { + it("Given empty query When searching Then it returns empty array without loading bundle", async () => { + const result = await searchPosts(" ") + expect(result).toEqual([]) + }) + }) +}) From 38696770778dd15121e1c4896c1191ba6f6af31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=83=81=EB=B9=88?= Date: Sun, 16 Aug 2026 20:24:33 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs(env):=20.env.example=EC=97=90=20NEXT?= =?UTF-8?q?=5FPUBLIC=5FBASE=5FPATH=20=EC=98=88=EC=A0=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20next.config.ts=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .env.example에 서브경로(NEXT_PUBLIC_BASE_PATH) 설정 주석 및 예시 추가 - next.config.ts에 NEXT_PUBLIC_BASE_PATH 기반 basePath/assetPrefix 설정 연동 --- .env.example | 10 ++++++++++ next.config.ts | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/.env.example b/.env.example index a59d068..14a9ec2 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,16 @@ # 미설정 시 http://localhost:3000 으로 대체되며, 프로덕션 빌드에서 경고가 출력됩니다. # NEXT_PUBLIC_SITE_URL="https://blog.your-domain.com" +# ───────────────────────────────────────────────────────────── +# Base Path / 서브경로 (선택) +# GitHub Pages 프로젝트 저장소 배포(예: https://.github.io//) 등 +# 사이트가 도메인의 루트가 아닌 하위 서브경로에서 서빙될 때 설정합니다. +# 시작은 '/' 로 시작하고 끝의 '/' 는 생략하세요. 예: /my-blog +# 사용자 저장소(https://.github.io)나 루트 도메인 배포 시에는 비워두세요. +# ───────────────────────────────────────────────────────────── + +# NEXT_PUBLIC_BASE_PATH="" + # ───────────────────────────────────────────────────────────── # Giscus 댓글 (선택) # 다섯 값은 전부 함께 설정하거나 전부 비워야 합니다. 일부만 채우면 빌드가 실패합니다. diff --git a/next.config.ts b/next.config.ts index 3394687..98d9f91 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,8 +1,16 @@ import type { NextConfig } from "next" +// biome-ignore lint/complexity/useLiteralKeys: tsconfig noPropertyAccessFromIndexSignature requires bracket access +const rawBasePath = process.env["NEXT_PUBLIC_BASE_PATH"]?.trim() +const basePath = + rawBasePath && rawBasePath !== "/" + ? (rawBasePath.startsWith("/") ? rawBasePath : `/${rawBasePath}`).replace(/\/+$/, "") + : undefined + const nextConfig: NextConfig = { // biome-ignore lint/complexity/useLiteralKeys: tsconfig noPropertyAccessFromIndexSignature requires bracket access ...(process.env["DEV_ORIGIN"] ? { allowedDevOrigins: [process.env["DEV_ORIGIN"]] } : {}), + ...(basePath ? { basePath, assetPrefix: basePath } : {}), output: "export", trailingSlash: true, images: {