Skip to content

Commit f3777d8

Browse files
luciscursoragent
andcommitted
Redirect English browsers from / to /en
Negotiate language at the edge via Accept-Language, and persist PT/EN choice in a cookie so the switcher sticks. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0724b0f commit f3777d8

7 files changed

Lines changed: 114 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Repositório: [github.com/lucis/blog](https://github.com/lucis/blog)
1212

1313
Cada post MDX declara `lang` (`pt` | `en`) e `translationKey` para ligar as versões. A UI e as datas seguem o idioma da rota. O poema da sidebar permanece em português.
1414

15+
A home `/` redireciona para `/en` quando o `Accept-Language` do browser prioriza inglês (e não há cookie `locale=pt`). O seletor PT/EN grava essa preferência.
16+
1517
## Desenvolvimento
1618

1719
```sh

src/components/LanguageSwitcher.astro

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ const enLink = enHref ?? localePath("en", "/");
1717
<div class="flex items-center justify-center gap-3 text-sm font-serif tracking-wide">
1818
<a
1919
href={ptLink}
20+
data-set-locale="pt"
2021
class:list={[
21-
"transition-opacity hover:opacity-100",
22+
"locale-link transition-opacity hover:opacity-100",
2223
locale === "pt" ? "opacity-100 underline underline-offset-4" : "opacity-40",
2324
]}
2425
aria-current={locale === "pt" ? "page" : undefined}
@@ -28,8 +29,9 @@ const enLink = enHref ?? localePath("en", "/");
2829
<span class="opacity-30" aria-hidden="true">/</span>
2930
<a
3031
href={enLink}
32+
data-set-locale="en"
3133
class:list={[
32-
"transition-opacity hover:opacity-100",
34+
"locale-link transition-opacity hover:opacity-100",
3335
locale === "en" ? "opacity-100 underline underline-offset-4" : "opacity-40",
3436
]}
3537
aria-current={locale === "en" ? "page" : undefined}

src/layouts/BaseLayout.astro

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,17 @@ const lang = htmlLang(locale);
8686
<body class="bg-white min-h-screen">
8787
<slot />
8888
<Footer />
89+
<script>
90+
const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
91+
92+
document.querySelectorAll<HTMLAnchorElement>("[data-set-locale]").forEach((link) => {
93+
link.addEventListener("click", () => {
94+
const chosen = link.dataset.setLocale;
95+
if (chosen === "pt" || chosen === "en") {
96+
document.cookie = `locale=${encodeURIComponent(chosen)}; Path=/; Max-Age=${ONE_YEAR_SECONDS}; SameSite=Lax`;
97+
}
98+
});
99+
});
100+
</script>
89101
</body>
90102
</html>

src/pages/blog/[slug].astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const jsonLd = {
103103
enHref && (
104104
<a
105105
href={enHref}
106+
data-set-locale="en"
106107
class="text-sm text-gray-600 hover:text-black transition-colors font-serif tracking-wide"
107108
>
108109
EN

src/pages/en/blog/[slug].astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const jsonLd = {
106106
ptHref && (
107107
<a
108108
href={ptHref}
109+
data-set-locale="pt"
109110
class="text-sm text-gray-600 hover:text-black transition-colors font-serif tracking-wide"
110111
>
111112
PT

worker/index.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
interface Env {
2+
ASSETS: Fetcher;
3+
}
4+
5+
const LOCALE_COOKIE = "locale";
6+
7+
function getCookie(request: Request, name: string): string | null {
8+
const cookieHeader = request.headers.get("Cookie");
9+
if (!cookieHeader) {
10+
return null;
11+
}
12+
13+
for (const part of cookieHeader.split(";")) {
14+
const [key, ...rest] = part.trim().split("=");
15+
if (key === name) {
16+
return decodeURIComponent(rest.join("="));
17+
}
18+
}
19+
20+
return null;
21+
}
22+
23+
/**
24+
* Prefer English only when the highest-priority language tag is English.
25+
* A Brazilian abroad with pt-BR first stays on Portuguese.
26+
*/
27+
function prefersEnglish(acceptLanguage: string | null): boolean {
28+
if (!acceptLanguage) {
29+
return false;
30+
}
31+
32+
const tags = acceptLanguage
33+
.split(",")
34+
.map((part) => {
35+
const [rawTag, ...params] = part.trim().split(";");
36+
const qParam = params.find((param) => param.trim().startsWith("q="));
37+
const quality = qParam ? Number.parseFloat(qParam.split("=")[1] ?? "1") : 1;
38+
39+
return {
40+
lang: rawTag.trim().toLowerCase(),
41+
quality: Number.isFinite(quality) ? quality : 0,
42+
};
43+
})
44+
.filter((tag) => tag.lang.length > 0)
45+
.sort((a, b) => b.quality - a.quality);
46+
47+
for (const { lang } of tags) {
48+
if (lang.startsWith("pt")) {
49+
return false;
50+
}
51+
52+
if (lang.startsWith("en")) {
53+
return true;
54+
}
55+
}
56+
57+
return false;
58+
}
59+
60+
function isPortugueseHome(pathname: string): boolean {
61+
return pathname === "/" || pathname === "/index.html";
62+
}
63+
64+
export default {
65+
async fetch(request: Request, env: Env): Promise<Response> {
66+
const url = new URL(request.url);
67+
const isGetOrHead = request.method === "GET" || request.method === "HEAD";
68+
const savedLocale = getCookie(request, LOCALE_COOKIE);
69+
70+
const shouldOfferEnglishHome =
71+
isGetOrHead &&
72+
isPortugueseHome(url.pathname) &&
73+
savedLocale !== "pt" &&
74+
(savedLocale === "en" || prefersEnglish(request.headers.get("Accept-Language")));
75+
76+
if (shouldOfferEnglishHome) {
77+
const target = new URL("/en", url);
78+
return new Response(null, {
79+
status: 302,
80+
headers: {
81+
Location: target.toString(),
82+
Vary: "Accept-Language, Cookie",
83+
"Cache-Control": "private, no-store",
84+
},
85+
});
86+
}
87+
88+
return env.ASSETS.fetch(request);
89+
},
90+
};

wrangler.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ name = "lucispersonalblog"
22
account_id = "7658c5018fe528307fc4f4b5bf6a1127"
33
compatibility_date = "2026-08-02"
44
workers_dev = true
5+
main = "worker/index.ts"
56

67
[assets]
78
directory = "./dist"
9+
binding = "ASSETS"
810
not_found_handling = "404-page"
911
html_handling = "auto-trailing-slash"
12+
# Só a home PT precisa negociar idioma antes dos assets estáticos.
13+
run_worker_first = ["/", "/index.html"]
1014

1115
[[routes]]
1216
pattern = "blog.lucis.dev"

0 commit comments

Comments
 (0)