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
2 changes: 1 addition & 1 deletion .astro/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"_variables": {
"lastUpdateCheck": 1772339173160
"lastUpdateCheck": 1775922065555
}
}
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
# .env
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX
PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
24 changes: 20 additions & 4 deletions src/components/BaseHead.astro
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,23 @@ const esURL = lang === 'es' ? canonicalURL.href : (alternateUrl ?? new URL('/es/
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"
/>

{
import.meta.env.PUBLIC_GA_MEASUREMENT_ID && (
<>
<script
is:inline
async
src={`https://www.googletagmanager.com/gtag/js?id=${import.meta.env.PUBLIC_GA_MEASUREMENT_ID}`}
/>
<script is:inline define:vars={{ id: import.meta.env.PUBLIC_GA_MEASUREMENT_ID }}>
window.dataLayer = window.dataLayer || [];
window.gtag = function () {
window.dataLayer.push(arguments);
};
gtag('js', new Date());
gtag('config', id);
</script>
</>
)
}
8 changes: 8 additions & 0 deletions src/components/HeaderInteractive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ const HeaderInteractive = ({ navItems, logoSrc, lang, altLangHref }: Props) => {

const altLang = lang === 'es' ? 'EN' : 'ES';

const handleLangSwitch = () => {
const targetLang = lang === 'es' ? 'en' : 'es';
localStorage.setItem('preferred_lang', targetLang);
sessionStorage.removeItem('lang_redirected');
};

return (
<header
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
Expand Down Expand Up @@ -95,6 +101,7 @@ const HeaderInteractive = ({ navItems, logoSrc, lang, altLangHref }: Props) => {
{/* Language switcher */}
<a
href={altLangHref}
onClick={handleLangSwitch}
className={`text-sm px-3 py-1 rounded-full border transition-all duration-300 font-bold ${
isScrolled || isMenuOpen
? 'text-foreground border-foreground/20 hover:border-primary hover:text-primary'
Expand Down Expand Up @@ -146,6 +153,7 @@ const HeaderInteractive = ({ navItems, logoSrc, lang, altLangHref }: Props) => {
<div className="pt-6 border-t border-foreground/10">
<a
href={altLangHref}
onClick={handleLangSwitch}
className="inline-flex items-center text-lg font-bold text-foreground hover:text-primary transition-colors"
aria-label={`Switch to ${altLang}`}
>
Expand Down
55 changes: 55 additions & 0 deletions src/components/LanguageDetector.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
// src/components/LanguageDetector.astro
// Client-side browser language detection with SEO-safe redirection.
// Only performs automatic detection on the home page to avoid breaking
// deep-linked / indexed pages. Manual preference (via header toggle)
// is respected on ALL pages.
import type { Lang } from '@/lib/i18n';

interface Props {
lang: Lang;
alternateUrl?: string;
}

const { lang, alternateUrl } = Astro.props;
---

<script
define:vars={{ pageLang: lang, alternateUrl: alternateUrl ?? null }}
>
(function detectLanguage() {
const PREFERRED_LANG_KEY = 'preferred_lang';
const REDIRECTED_KEY = 'lang_redirected';

// Never redirect if we already redirected this session (prevents loops)
if (sessionStorage.getItem(REDIRECTED_KEY)) return;

const storedPref = localStorage.getItem(PREFERRED_LANG_KEY);
const isHomePage =
window.location.pathname === '/' ||
window.location.pathname === '/es/' ||
window.location.pathname === '/es';

/** @type {'en' | 'es' | null} */
let targetLang = null;

if (storedPref === 'en' || storedPref === 'es') {
// User has explicitly chosen a language before — respect it everywhere
targetLang = storedPref;
} else if (isHomePage) {
// First visit, no preference saved — detect from browser, but ONLY on
// the home page to preserve SEO for deep-linked pages.
const browserLang = navigator.language || navigator.userLanguage || 'en';
targetLang = browserLang.startsWith('es') ? 'es' : 'en';

// Persist the detected preference so subsequent pages are consistent
localStorage.setItem(PREFERRED_LANG_KEY, targetLang);
}

// If we have a target and it differs from the current page, redirect
if (targetLang && targetLang !== pageLang && alternateUrl) {
sessionStorage.setItem(REDIRECTED_KEY, '1');
window.location.replace(alternateUrl);
}
})();
</script>
2 changes: 2 additions & 0 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import BaseHead from '@/components/BaseHead.astro';
import Header from '@/components/Header.astro';
import Footer from '@/components/Footer.astro';
import LanguageDetector from '@/components/LanguageDetector.astro';
import type { Lang } from '@/lib/i18n';
import '@/index.css';

Expand All @@ -20,6 +21,7 @@ const { title, description, lang = 'en', alternateUrl } = Astro.props;
<html lang={lang}>
<head>
<BaseHead title={title} description={description} lang={lang} alternateUrl={alternateUrl} />
<LanguageDetector lang={lang} alternateUrl={alternateUrl} />
</head>
<body class="min-h-screen bg-background flex flex-col">
<Header lang={lang} />
Expand Down
4 changes: 3 additions & 1 deletion src/lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// src/lib/analytics.ts
// Utility for Google Analytics event tracking

const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID;
const GA_MEASUREMENT_ID = import.meta.env.PUBLIC_GA_MEASUREMENT_ID;

export function trackEvent({
action,
Expand All @@ -20,6 +20,8 @@ export function trackEvent({
event_label: label,
value: value,
});
} else if (import.meta.env.DEV) {
console.log('[Analytics]', { action, category, label, value });
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const translations = { en, es } as const;

export type Lang = 'en' | 'es';

/** localStorage key for the user's explicitly chosen language */
export const PREFERRED_LANG_KEY = 'preferred_lang' as const;

export function getLang(url: URL): Lang {
const [, lang] = url.pathname.split('/');
if (lang === 'es') return 'es';
Expand Down
Loading