Skip to content

feat(website): unify i18n on a single [lang] route + Accept-Language redirect#27

Merged
minjun0219 merged 5 commits into
mainfrom
claude/fix-language-redirect-UTZNx
May 6, 2026
Merged

feat(website): unify i18n on a single [lang] route + Accept-Language redirect#27
minjun0219 merged 5 commits into
mainfrom
claude/fix-language-redirect-UTZNx

Conversation

@minjun0219

@minjun0219 minjun0219 commented Apr 30, 2026

Copy link
Copy Markdown
Owner

Summary

Replace the per-language (en)/ and (ko)/ route-group duplication with a single app/[lang]/ tree, and adopt the Next.js i18n-routing reference pattern for locale handling.

  • Middleware redirects unprefixed paths to /<picked-lang><path> based on Accept-Language. Lang-prefixed paths (/en/*, /ko/*) pass through unchanged — the EN/KO toggle is therefore safe even under a mismatched Accept-Language (no redirect loop).
  • One app/[lang]/ tree. New routes (e.g. inspect) become one file (app/[lang]/inspect/page.tsx) instead of duplicating across route groups.
  • Pair Inter (Latin) with Noto Sans KR (Hangul) via next/font + Tailwind fontFamily chaining; browsers select per-glyph through unicode-range so a single font-sans works for mixed-language UI.
  • Add vitest to the website package (matches packages/ogpeek and packages/ogpeek-react) with unit tests for pickLangFromAcceptLanguage, stripLangPrefix, and the middleware (passthrough + redirect cases, query-string preservation, no-loop guarantee).

Behavior matrix

URL Result
/ (Korean AL) 307 → /ko
/ (English AL / none) 307 → /en
/inspect (Korean AL) 307 → /ko/inspect
/en / /ko passthrough
/en/inspect / /ko/inspect passthrough
/en (Korean AL) passthrough — no loop

Why the reference style

An earlier revision rewrote / internally to /en so the canonical English URL stayed ogpeek.dev rather than ogpeek.dev/en. That required a server-set x-public-pathname header to let generateMetadata reconstruct 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 typecheck
  • pnpm check (biome) — 0 errors
  • Browser smoke: / redirects per Accept-Language; KO toggle on /en, EN toggle on /ko; query preservation on submit; Hangul renders Noto Sans KR, Latin renders Inter
Korean

페이지별 (en)/, (ko)/ 라우트 그룹 중복을 단일 app/[lang]/ 트리로 통합하고, Next.js i18n-routing 공식 예제 패턴을 채택했습니다.

  • 미들웨어는 lang prefix가 없는 경로를 Accept-Language 기반으로 /<lang><path>로 리다이렉트만 합니다. /en/*, /ko/*는 패스스루 — 토글이 항상 안전합니다.
  • 새 라우트는 app/[lang]/inspect/page.tsx 한 파일이면 충분합니다.
  • 폰트는 Inter(라틴) + Noto Sans KR(한글) 페어링.
  • vitest 도입 + i18n/middleware 단위 테스트 23건.

이전 리비전은 /를 내부 rewrite로 /en에 매핑해 영어 canonical을 도메인 루트로 유지했습니다. 이를 위해 서버가 설정하는 x-public-pathname 헤더 + spoofing 가드가 필요했는데, 엔진 소개 데모 사이트 성격상 도메인 루트 canonical의 가치는 추가 메커니즘 비용보다 작아서 레퍼런스 패턴으로 정렬했습니다.

🤖 Generated with Claude Code

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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Apr 30, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@minjun0219 minjun0219 marked this pull request as ready for review May 6, 2026 10:41
Copilot AI review requested due to automatic review settings May 6, 2026 10:41
…edirect-UTZNx

# Conflicts:
#	website/components/LangToggle.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread website/app/[lang]/layout.tsx Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Korean Accept-Language, otherwise rewrite to /en... while keeping the public URL unprefixed.
  • Add stripLangPrefix and update LangToggle + 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.

Comment thread website/middleware.ts
Comment thread website/app/[lang]/layout.tsx Outdated
claude added 2 commits May 6, 2026 10:48
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.
@minjun0219 minjun0219 changed the title feat(website): serve English at root + drop page-level lang duplication feat(website): unify i18n on a single [lang] route + Accept-Language redirect May 6, 2026
Mirror the engine card: version, monthly downloads, and minzipped
size badges, all linking to the @ogpeek/react npm page.
@minjun0219 minjun0219 merged commit bc727b4 into main May 6, 2026
4 checks passed
@minjun0219 minjun0219 deleted the claude/fix-language-redirect-UTZNx branch May 6, 2026 11:24
@github-actions github-actions Bot mentioned this pull request May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants