feat(website): unify i18n on a single [lang] route + Accept-Language redirect#27
Conversation
Switch to a single [lang]/ tree where the middleware decides per request: Korean Accept-Language redirects to /ko<path>; everything else rewrites internally to /en<path> while the browser URL stays unprefixed. /en and /ko are passthrough so the EN toggle works even for visitors with a Korean Accept-Language. Adds Inter for Latin glyphs alongside Noto Sans KR (unicode-range fallback), surfaces the public path via x-public-pathname for canonical URLs, and introduces vitest in the website package with unit tests for pickLangFromAcceptLanguage, stripLangPrefix, and the middleware branching.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
ogpeek | 0e82442 | Commit Preview URL Branch Preview URL |
May 06 2026, 11:08 AM |
…edirect-UTZNx # Conflicts: # website/components/LangToggle.tsx
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f283704c04
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR updates the website’s i18n routing so English content can be served at the root (/) via internal rewrites while Korean users are redirected to /ko..., and it consolidates routing under a single app/[lang]/ tree. It also adds Vitest-based unit tests and adjusts typography to support mixed Latin/Hangul rendering via chained fonts.
Changes:
- Implement per-request middleware branching:
/ko...redirect for KoreanAccept-Language, otherwise rewrite to/en...while keeping the public URL unprefixed. - Add
stripLangPrefixand updateLangToggle+ metadata generation to build canonical/alternate URLs based on the public path. - Add
vitest+ unit tests for i18n helpers and middleware behavior; update Tailwind font family to chain Inter + Noto Sans KR.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| website/middleware.ts | Adds Accept-Language based redirect/rewrite logic and forwards x-public-pathname on rewrites. |
| website/app/[lang]/layout.tsx | Updates fonts, reads x-public-pathname, and computes canonical/alternate language URLs. |
| website/lib/i18n.ts | Adds stripLangPrefix and provides pickLangFromAcceptLanguage used by middleware/tests. |
| website/components/LangToggle.tsx | Builds stable /en... and /ko... toggle targets based on the language-agnostic path. |
| website/tailwind.config.ts | Chains font families to support mixed-language UI with one font-sans. |
| website/vitest.config.ts | Introduces Vitest config with @ alias and test include globs. |
| website/test/i18n.test.ts | Adds unit tests for Accept-Language parsing and lang-prefix stripping. |
| website/test/middleware.test.ts | Adds unit tests for middleware passthrough/redirect/rewrite branches and header behavior. |
| website/package.json | Adds Vitest scripts and dependency. |
| pnpm-lock.yaml | Locks Vitest dependency tree changes. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Previously the passthrough branch for /en/* and /ko/* returned
NextResponse.next() without overwriting x-public-pathname, with two
consequences flagged in review:
1. On nested lang-prefixed routes (e.g. /en/inspect), generateMetadata
fell back to "/${lang}" and emitted truncated canonical/alternate
URLs that dropped the sub-path.
2. Clients could spoof x-public-pathname on direct /en or /ko visits
and influence the canonical/alternates emitted by the server.
Always rebuild the request headers from the incoming request and set
x-public-pathname to the actual pathname before returning — both for
the rewrite branch and the passthrough branch. Korean redirects don't
need it (the client makes a fresh request to /ko<path>).
Add a test asserting the passthrough branch forwards the lang-prefixed
pathname, plus a regression test that a client-supplied
x-public-pathname is overwritten.
…iddleware) Drop the rewrite-for-default + redirect-for-others scheme in favour of the official Next.js i18n-routing pattern: every page lives under /<lang>/ and the middleware just redirects unprefixed paths to /<picked-lang><path> based on Accept-Language. Lang-prefixed paths pass through unchanged, so /en stays reachable for the toggle even under a Korean Accept-Language. This deletes the x-public-pathname custom header and the getPublicPathname helper in [lang]/layout.tsx — both existed only to let server components reconstruct the public URL after the rewrite. With every URL already lang-prefixed, the canonical URL question becomes trivial and the spoofing/sub-path concerns flagged in review disappear with the mechanism. Trade-off: the canonical English URL is now /en rather than the bare domain root. For the engine's introduction site this is fine — the simpler middleware (10 lines, matching the reference example) is the better fit.
Mirror the engine card: version, monthly downloads, and minzipped size badges, all linking to the @ogpeek/react npm page.
Summary
Replace the per-language
(en)/and(ko)/route-group duplication with a singleapp/[lang]/tree, and adopt the Next.js i18n-routing reference pattern for locale handling./<picked-lang><path>based onAccept-Language. Lang-prefixed paths (/en/*,/ko/*) pass through unchanged — the EN/KO toggle is therefore safe even under a mismatchedAccept-Language(no redirect loop).app/[lang]/tree. New routes (e.g.inspect) become one file (app/[lang]/inspect/page.tsx) instead of duplicating across route groups.next/font+ TailwindfontFamilychaining; browsers select per-glyph throughunicode-rangeso a singlefont-sansworks for mixed-language UI.vitestto thewebsitepackage (matchespackages/ogpeekandpackages/ogpeek-react) with unit tests forpickLangFromAcceptLanguage,stripLangPrefix, and the middleware (passthrough + redirect cases, query-string preservation, no-loop guarantee).Behavior matrix
/(Korean AL)/ko/(English AL / none)/en/inspect(Korean AL)/ko/inspect/en//ko/en/inspect//ko/inspect/en(Korean AL)Why the reference style
An earlier revision rewrote
/internally to/enso the canonical English URL stayedogpeek.devrather thanogpeek.dev/en. That required a server-setx-public-pathnameheader to letgenerateMetadatareconstruct the public URL, plus a spoof-guard since clients could supply that header on direct lang-prefixed visits. For an engine introduction site the bare-domain canonical isn't worth the extra mechanism — matching the reference example trades two SEO points for ~10 lines of middleware and zero custom request-header plumbing.Test plan
pnpm -F website test— 23 passing (i18n + middleware)pnpm -F website typecheckpnpm check(biome) — 0 errors/redirects perAccept-Language; KO toggle on/en, EN toggle on/ko; query preservation on submit; Hangul renders Noto Sans KR, Latin renders InterKorean
페이지별
(en)/,(ko)/라우트 그룹 중복을 단일app/[lang]/트리로 통합하고, Next.js i18n-routing 공식 예제 패턴을 채택했습니다.Accept-Language기반으로/<lang><path>로 리다이렉트만 합니다./en/*,/ko/*는 패스스루 — 토글이 항상 안전합니다.app/[lang]/inspect/page.tsx한 파일이면 충분합니다.vitest도입 + i18n/middleware 단위 테스트 23건.이전 리비전은
/를 내부 rewrite로/en에 매핑해 영어 canonical을 도메인 루트로 유지했습니다. 이를 위해 서버가 설정하는x-public-pathname헤더 + spoofing 가드가 필요했는데, 엔진 소개 데모 사이트 성격상 도메인 루트 canonical의 가치는 추가 메커니즘 비용보다 작아서 레퍼런스 패턴으로 정렬했습니다.🤖 Generated with Claude Code