From 0f4a379716a05070acf3d028d2afea17b6858e8d Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 8 Apr 2026 13:14:49 +0200 Subject: [PATCH] Add German and English localization for landing and technical sections - Created German localization files for landing sections: about me, experience, hero, processes, reasons, references, winnings. - Created English localization files for landing sections: about me, experience, hero, processes, reasons, references, winnings. - Added German localization for technical sections: about, certifications, hero, services, skills. - Added English localization for technical sections: about, certifications, hero, services, skills. - Introduced SCSS file for landing hero section styling. --- index.html | 179 +++++++++++++++--- prerender.js | 28 ++- public/og-image.png | Bin 0 -> 22167 bytes public/sitemap.xml | 38 +++- src/app/AppRouter.tsx | 105 ++++++---- src/app/PortfolioApp.tsx | 6 +- src/assets/og-image.png | Bin 0 -> 22167 bytes src/components/LanguageToggle.tsx | 15 +- src/components/layout/Navigation.tsx | 7 +- src/components/sections/AboutSection.tsx | 4 +- src/components/sections/HeroSection.tsx | 73 +++---- src/config/certifications-config.ts | 2 +- src/config/locales/de/index.ts | 24 +-- src/config/locales/de/landing-aboutme.ts | 10 - .../locales/de/landing/landing-aboutme.ts | 17 ++ .../de/{ => landing}/landing-experience.ts | 0 src/config/locales/de/landing/landing-hero.ts | 5 + .../de/{ => landing}/landing-processes.ts | 0 .../de/{ => landing}/landing-reasons.ts | 6 +- .../de/{ => landing}/landing-references.ts | 0 .../de/{ => landing}/landing-winnings.ts | 0 .../locales/de/{ => technical}/about.ts | 2 +- .../de/{ => technical}/certifications.ts | 2 +- src/config/locales/de/{ => technical}/hero.ts | 2 +- .../locales/de/{ => technical}/services.ts | 0 .../locales/de/{ => technical}/skills.ts | 0 src/config/locales/en/TextConfig.ts | 7 + src/config/locales/en/index.ts | 24 +-- .../en/{ => landing}/landing-aboutme.ts | 0 .../en/{ => landing}/landing-experience.ts | 0 src/config/locales/en/landing/landing-hero.ts | 5 + .../en/{ => landing}/landing-processes.ts | 0 .../en/{ => landing}/landing-reasons.ts | 0 .../en/{ => landing}/landing-references.ts | 0 .../en/{ => landing}/landing-winnings.ts | 0 .../locales/en/{ => technical}/about.ts | 0 .../en/{ => technical}/certifications.ts | 2 +- src/config/locales/en/{ => technical}/hero.ts | 0 .../locales/en/{ => technical}/services.ts | 0 .../locales/en/{ => technical}/skills.ts | 0 src/contexts/LanguageContext.tsx | 35 +--- src/hooks/usePageSeo.ts | 9 +- src/pages/HomePage.tsx | 12 +- src/pages/LandingPage.tsx | 22 ++- src/scss/sections/hero-section.scss | 2 +- src/scss/sections/landing-hero-section.scss | 0 src/utils/scrollUtils.ts | 27 ++- vercel.json | 16 +- 48 files changed, 474 insertions(+), 212 deletions(-) create mode 100644 public/og-image.png create mode 100644 src/assets/og-image.png delete mode 100644 src/config/locales/de/landing-aboutme.ts create mode 100644 src/config/locales/de/landing/landing-aboutme.ts rename src/config/locales/de/{ => landing}/landing-experience.ts (100%) create mode 100644 src/config/locales/de/landing/landing-hero.ts rename src/config/locales/de/{ => landing}/landing-processes.ts (100%) rename src/config/locales/de/{ => landing}/landing-reasons.ts (85%) rename src/config/locales/de/{ => landing}/landing-references.ts (100%) rename src/config/locales/de/{ => landing}/landing-winnings.ts (100%) rename src/config/locales/de/{ => technical}/about.ts (97%) rename src/config/locales/de/{ => technical}/certifications.ts (72%) rename src/config/locales/de/{ => technical}/hero.ts (89%) rename src/config/locales/de/{ => technical}/services.ts (100%) rename src/config/locales/de/{ => technical}/skills.ts (100%) rename src/config/locales/en/{ => landing}/landing-aboutme.ts (100%) rename src/config/locales/en/{ => landing}/landing-experience.ts (100%) create mode 100644 src/config/locales/en/landing/landing-hero.ts rename src/config/locales/en/{ => landing}/landing-processes.ts (100%) rename src/config/locales/en/{ => landing}/landing-reasons.ts (100%) rename src/config/locales/en/{ => landing}/landing-references.ts (100%) rename src/config/locales/en/{ => landing}/landing-winnings.ts (100%) rename src/config/locales/en/{ => technical}/about.ts (100%) rename src/config/locales/en/{ => technical}/certifications.ts (71%) rename src/config/locales/en/{ => technical}/hero.ts (100%) rename src/config/locales/en/{ => technical}/services.ts (100%) rename src/config/locales/en/{ => technical}/skills.ts (100%) create mode 100644 src/scss/sections/landing-hero-section.scss diff --git a/index.html b/index.html index ce83c1e..05605ae 100644 --- a/index.html +++ b/index.html @@ -17,16 +17,20 @@ name="keywords" content="freelance software developer, accessible web development, barrierefreie Webentwicklung, React developer, TypeScript, web accessibility, BFSG, Germany" /> - + - - - + + + - + - - - + + + + + + + - + + + - + + + + + + + + + +
diff --git a/prerender.js b/prerender.js index 201a062..fdf2240 100644 --- a/prerender.js +++ b/prerender.js @@ -6,17 +6,24 @@ * Usage: node prerender.js (run after `vite build`) */ -import { createServer } from 'http' -import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs' -import { join, dirname } from 'path' -import { fileURLToPath } from 'url' +import { createServer } from 'node:http' +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' import puppeteer from 'puppeteer' const __dirname = dirname(fileURLToPath(import.meta.url)) const DIST = join(__dirname, 'dist') const PORT = 4173 -const ROUTES = ['/', '/technical', '/imprint', '/privacy-policy'] +const ROUTES = [ + '/de/', + '/de/technical', + '/en/', + '/en/technical', + '/imprint', + '/privacy-policy', +] /** Minimal static file server that falls back to index.html (SPA). */ function startServer() { @@ -81,7 +88,10 @@ async function prerender() { await page.close() // Write to dist//index.html - const outDir = route === '/' ? DIST : join(DIST, route) + // Normalise the route: strip trailing slash for directory creation (except root, which is now redirected) + const routeDir = + route.endsWith('/') && route !== '/' ? route.slice(0, -1) : route + const outDir = routeDir === '/' ? DIST : join(DIST, routeDir) if (!existsSync(outDir)) { mkdirSync(outDir, { recursive: true }) } @@ -95,7 +105,9 @@ async function prerender() { console.log('\n✅ Prerendering complete!\n') } -prerender().catch((err) => { +try { + await prerender() +} catch (err) { console.error('Prerendering failed:', err) process.exit(1) -}) +} diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..9c74e21df395b63bc787c9d3ccafc08b860e9e85 GIT binary patch literal 22167 zcmeIaXIPZowk2AMih_UvK|w%2P$Wy1Y(|2hM9CsKN6EoY1Oy36l&l2FIp-ufl;l_h ziA5+fMG=agD&FIfArJ$oO`#=dI(bCt@W-o=a^%Rx%^(pNs^J!kRT8UGO1^e z6%dG1c?iTwhqEW)f9n2PAw?imw51+DP;!c0MLE9KSkPTN@@lI7S&cn&E+o4D#;x|T zAjvA8mHv@ci_zN_R%_KJTN6K?SM&<3K93Q2E=iLb6n8s+=As7urCSPKVUI7KI%{pE zFiwd@a8F32wHEk1`F!u*jfAxYu5-VgyD2U_nfBd^U3j^Q+hmZ2D?lJN(!$CA{P5z8 z_XWZar=HwAMfm<8J%I7@#?1%PE!N1^!@r`c-*rwrFkHJ)==ga=q;-W(f0vxcl{l(e zkIitN97_wxOo+y{(o$rrzi^}5U&e0Ifq)q z(SPUrO}C|^ao%iOTYX!ks`J!Tk*ekn2N|duQ^%~)Rk=u+%$H{Urh)2718GNZEAD&N z-tauWPZ+g%-@bkPJoWK2eEqyW^=)xIFD>tGwhxl*1Y)=w@?G97t@v0|47zZed^;e3 zJWzggWq0R#F}$ucxs_$NTM1( zU6!@OgU*A!rWj0IbWG%dJ!hoZ8$*XTh9z@e!IIwIKbo8Lf3_Gf{Y+_Txh*Mq(BS=3 z*laAJdR;L^@mUB{`T%P-T)ih?_Q za$lT=uO8|>$6WCmnl8IsHLLa&`*^z}z$8tF`@M z`P%#;n!VJ(@t&}+kBG0&WR}%uCYF1363jwU3GoVH@zF6MKj(5q)Y*o~R<+D($+od< zvN9~)&$zl}JEWvvoO_YGtUfv}rthd-qLTE5@jT+A&vELBI%=V~oXcd*rQL(_NUl3^ zO=6@SY|Y)4dPb#8Qo`A{nOSV)J-S1~iNm^!4cN1XX+|pt_ost?efHXaPx46jfoZz~ zhYwq3W?&o&Ltk8bS3c-2;>Ha?IeXXb$3-@wocZ0*L4y1W#Nrp z3?t>@juwW3%E!li$Ud)-w@B1Ftcc`RO^+5#iqV$+ofk{Erk8|=_U4mDdhB(dOHq7H z>WWR=fJfzDVbg8-wf?g+t5Op1MqMFnx^Guu+H0#kWYLtvn-gZn`xDDnEO8AMZ&gaU zD1Njw-F}Su4EO%*Cs$O;Z0I{r7FCwN`yt` zBz`1QW6rR_4x>XK5O@|T8xToYaRKni2?4R%bp|UsRpHYm--GHqiVZ*YvJ83?J}4u6 zfGG>Ep?Hl~+A8qpREBETUaXaDT)d7k-BnvG0X?_=4X>fiUs<*{h&uz5F-8+rpo^X=s3W~P?MPkeo!czgStIei9qK)ioo(od89bk$l| zRb{ngKsg2_URuV-FDSt;D9A4&A=l@3^$}toezWFX?O>H5EFOz5W(N zY^?b;cMz=~2%o=ictor)@5VAqNMaH>)s>YlWSgAyuQUmBnvW2mO!?|h;6s?2RzH^E zNN*c0>`S(c5p$K2pD!Uh-^D>vA@%fudV6WjUaW?1nPv`dZq7Sp4wo`l)3dQ#|2+a< zA3yyv>e7V01G=buuC0-{%1+o(%Y03i- znhkxa``tG7cUZbwknYe`o<0ecx2>O}FzF|f8Xm&vu+6Et$PoRtV0Tw%X_3car;{uH zPPgyNbfxcq{|zIA$$8V3Tq|;~SKVjOtHv62X z=sF54b~2B555M&OZam}u_}ThA@u_zf?;f7LZ=dPVf*ag5O)CFhZD-;9)G@O16?1Bz zvXw=o-Yg`TNw@8KqJ1}^Gqtx_gn3wmH{($$a;cemi<>DyId+2rxmr7;XLv5p)E@3N z2Gg-0FDF!u3oh-(uUML(Y4b#4)dP{)l&>A{(Tu`o�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 OOFfZ$ob&MY`~L=E=%ru) literal 0 HcmV?d00001 diff --git a/public/sitemap.xml b/public/sitemap.xml index 53c760d..8bf6994 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,18 +1,44 @@ + - https://sascha-bach.de/ - - + https://sascha-bach.de/de/ + + + + 2025-06-28 monthly 1.0 + - https://sascha-bach.de/technical - - + https://sascha-bach.de/en/ + + + + 2025-06-28 + monthly + 0.9 + + + + https://sascha-bach.de/de/technical + + + + 2025-06-28 monthly 0.8 + + + https://sascha-bach.de/en/technical + + + + 2025-06-28 + monthly + 0.7 + diff --git a/src/app/AppRouter.tsx b/src/app/AppRouter.tsx index 956139e..193cec8 100644 --- a/src/app/AppRouter.tsx +++ b/src/app/AppRouter.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import '../scss/App.scss'; import Navbar from '../components/layout/Navigation'; import Footer from '../components/layout/Footer'; @@ -8,13 +8,73 @@ import LandingPage from '../pages/LandingPage'; import ImprintPage from '../pages/ImprintPage'; import PrivacyPolicy from '../pages/PrivacyPolicy'; import BackToTopButton from '../components/BackToTopButton'; +import { getLanguageFromPath, useLanguage } from '../contexts/LanguageContext'; import type { Theme } from '../data/types'; +/** Syncs the LanguageContext whenever the URL prefix (/de/ or /en/) changes. */ +function LanguageSyncer() { + const { setLanguage, language } = useLanguage(); + const location = useLocation(); + + useEffect(() => { + const urlLang = getLanguageFromPath(location.pathname); + if (urlLang && urlLang !== language) { + setLanguage(urlLang); + } + }, [location.pathname, language, setLanguage]); + + return null; +} + +function AppShell({ theme, setTheme }: Readonly<{ theme: Theme; setTheme: React.Dispatch>; }>) { + const location = useLocation(); + + useEffect(() => { + const hash = location.hash.substring(1); + if (hash) { + setTimeout(() => { + const element = document.getElementById(hash); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } + }, 100); + } + }, [location.hash]); + + return ( +
+ Skip to main content + + +
+ + {/* Root redirects → German (primary market) */} + } /> + } /> + + {/* German routes */} + } /> + } /> + + {/* English routes */} + } /> + } /> + + {/* Shared pages (noindex, language-independent) */} + } /> + } /> + +
+
+ +
+ ); +} + function AppRouter() { const [theme, setTheme] = useState('light'); useEffect(() => { - // Check for saved theme preference or default to light mode const savedTheme = localStorage.getItem('theme') as Theme | null; if (savedTheme) { setTheme(savedTheme); @@ -22,52 +82,13 @@ function AppRouter() { }, []); useEffect(() => { - // Apply theme to document and save preference document.documentElement.dataset.theme = theme; localStorage.setItem('theme', theme); }, [theme]); - useEffect(() => { - // Handle hash-based navigation for sections - const handleHashChange = () => { - const hash = globalThis.location.hash.substring(1); - if (hash) { - setTimeout(() => { - const element = document.getElementById(hash); - if (element) { - element.scrollIntoView({ behavior: 'smooth' }); - } - }, 100); - } - }; - - // Check hash on initial load - handleHashChange(); - - // Listen for hash changes - globalThis.addEventListener('hashchange', handleHashChange); - - return () => { - globalThis.removeEventListener('hashchange', handleHashChange); - }; - }, []); - return ( -
- Skip to main content - -
- - } /> - } /> - } /> - } /> - -
-
- -
+
); } diff --git a/src/app/PortfolioApp.tsx b/src/app/PortfolioApp.tsx index 667908b..9d18304 100644 --- a/src/app/PortfolioApp.tsx +++ b/src/app/PortfolioApp.tsx @@ -1,9 +1,11 @@ -import { LanguageProvider } from '../contexts/LanguageContext'; +import { LanguageProvider, getLanguageFromPath } from '../contexts/LanguageContext'; import AppRouter from './AppRouter'; function PortfolioApp() { + const initialLanguage = getLanguageFromPath(globalThis.location.pathname) ?? 'de'; + return ( - + ); diff --git a/src/assets/og-image.png b/src/assets/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..9c74e21df395b63bc787c9d3ccafc08b860e9e85 GIT binary patch literal 22167 zcmeIaXIPZowk2AMih_UvK|w%2P$Wy1Y(|2hM9CsKN6EoY1Oy36l&l2FIp-ufl;l_h ziA5+fMG=agD&FIfArJ$oO`#=dI(bCt@W-o=a^%Rx%^(pNs^J!kRT8UGO1^e z6%dG1c?iTwhqEW)f9n2PAw?imw51+DP;!c0MLE9KSkPTN@@lI7S&cn&E+o4D#;x|T zAjvA8mHv@ci_zN_R%_KJTN6K?SM&<3K93Q2E=iLb6n8s+=As7urCSPKVUI7KI%{pE zFiwd@a8F32wHEk1`F!u*jfAxYu5-VgyD2U_nfBd^U3j^Q+hmZ2D?lJN(!$CA{P5z8 z_XWZar=HwAMfm<8J%I7@#?1%PE!N1^!@r`c-*rwrFkHJ)==ga=q;-W(f0vxcl{l(e zkIitN97_wxOo+y{(o$rrzi^}5U&e0Ifq)q z(SPUrO}C|^ao%iOTYX!ks`J!Tk*ekn2N|duQ^%~)Rk=u+%$H{Urh)2718GNZEAD&N z-tauWPZ+g%-@bkPJoWK2eEqyW^=)xIFD>tGwhxl*1Y)=w@?G97t@v0|47zZed^;e3 zJWzggWq0R#F}$ucxs_$NTM1( zU6!@OgU*A!rWj0IbWG%dJ!hoZ8$*XTh9z@e!IIwIKbo8Lf3_Gf{Y+_Txh*Mq(BS=3 z*laAJdR;L^@mUB{`T%P-T)ih?_Q za$lT=uO8|>$6WCmnl8IsHLLa&`*^z}z$8tF`@M z`P%#;n!VJ(@t&}+kBG0&WR}%uCYF1363jwU3GoVH@zF6MKj(5q)Y*o~R<+D($+od< zvN9~)&$zl}JEWvvoO_YGtUfv}rthd-qLTE5@jT+A&vELBI%=V~oXcd*rQL(_NUl3^ zO=6@SY|Y)4dPb#8Qo`A{nOSV)J-S1~iNm^!4cN1XX+|pt_ost?efHXaPx46jfoZz~ zhYwq3W?&o&Ltk8bS3c-2;>Ha?IeXXb$3-@wocZ0*L4y1W#Nrp z3?t>@juwW3%E!li$Ud)-w@B1Ftcc`RO^+5#iqV$+ofk{Erk8|=_U4mDdhB(dOHq7H z>WWR=fJfzDVbg8-wf?g+t5Op1MqMFnx^Guu+H0#kWYLtvn-gZn`xDDnEO8AMZ&gaU zD1Njw-F}Su4EO%*Cs$O;Z0I{r7FCwN`yt` zBz`1QW6rR_4x>XK5O@|T8xToYaRKni2?4R%bp|UsRpHYm--GHqiVZ*YvJ83?J}4u6 zfGG>Ep?Hl~+A8qpREBETUaXaDT)d7k-BnvG0X?_=4X>fiUs<*{h&uz5F-8+rpo^X=s3W~P?MPkeo!czgStIei9qK)ioo(od89bk$l| zRb{ngKsg2_URuV-FDSt;D9A4&A=l@3^$}toezWFX?O>H5EFOz5W(N zY^?b;cMz=~2%o=ictor)@5VAqNMaH>)s>YlWSgAyuQUmBnvW2mO!?|h;6s?2RzH^E zNN*c0>`S(c5p$K2pD!Uh-^D>vA@%fudV6WjUaW?1nPv`dZq7Sp4wo`l)3dQ#|2+a< zA3yyv>e7V01G=buuC0-{%1+o(%Y03i- znhkxa``tG7cUZbwknYe`o<0ecx2>O}FzF|f8Xm&vu+6Et$PoRtV0Tw%X_3car;{uH zPPgyNbfxcq{|zIA$$8V3Tq|;~SKVjOtHv62X z=sF54b~2B555M&OZam}u_}ThA@u_zf?;f7LZ=dPVf*ag5O)CFhZD-;9)G@O16?1Bz zvXw=o-Yg`TNw@8KqJ1}^Gqtx_gn3wmH{($$a;cemi<>DyId+2rxmr7;XLv5p)E@3N z2Gg-0FDF!u3oh-(uUML(Y4b#4)dP{)l&>A{(Tu`o�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 OOFfZ$ob&MY`~L=E=%ru) literal 0 HcmV?d00001 diff --git a/src/components/LanguageToggle.tsx b/src/components/LanguageToggle.tsx index d365b09..a664ca5 100644 --- a/src/components/LanguageToggle.tsx +++ b/src/components/LanguageToggle.tsx @@ -1,15 +1,26 @@ +import { useNavigate, useLocation } from 'react-router-dom'; import { useLanguage } from '../contexts/LanguageContext'; import type { Language } from '../data/types'; import '../scss/language-toggle.scss'; import { useScreenReaderAnnouncements } from '../hooks/useScreenReaderAnnouncements'; +/** Swap /de/ ↔ /en/ prefix (or /de/technical ↔ /en/technical) in the pathname. */ +function getEquivalentPath(pathname: string, targetLang: Language): string { + if (pathname.startsWith('/de')) return pathname.replace(/^\/de/, `/${targetLang}`); + if (pathname.startsWith('/en')) return pathname.replace(/^\/en/, `/${targetLang}`); + return `/${targetLang}/`; +} + export default function LanguageToggle() { - const { language, setLanguage } = useLanguage(); + const { language } = useLanguage(); const { announce } = useScreenReaderAnnouncements(); + const navigate = useNavigate(); + const location = useLocation(); const handleLanguageChange = (newLang: Language) => { if (newLang !== language) { - setLanguage(newLang); + const targetPath = getEquivalentPath(location.pathname, newLang); + navigate(targetPath); announce(`Language changed to ${newLang === 'en' ? 'English' : 'Deutsch'}`); } }; diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index 8b2b48b..e7e6b45 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -23,14 +23,15 @@ export default function Navbar({ const navigate = useNavigate(); const { texts } = useLanguage(); - const isLandingPage = location.pathname === '/'; + const isLandingPage = location.pathname === '/de/' || location.pathname === '/en/' || location.pathname === '/'; + const technicalPath = location.pathname.startsWith('/en') ? '/en/technical' : '/de/technical'; const menuItems = isLandingPage ? texts.navigation.landingMenuItems : texts.navigation.menuItems; // Track active section based on scroll position useEffect(() => { - if (location.pathname !== '/technical' && location.pathname !== '/') { + if (location.pathname !== technicalPath && !isLandingPage) { setActiveSection(''); return; } @@ -66,7 +67,7 @@ export default function Navbar({ return (
{ e.preventDefault(); diff --git a/src/components/sections/AboutSection.tsx b/src/components/sections/AboutSection.tsx index d111e84..16f2bf8 100644 --- a/src/components/sections/AboutSection.tsx +++ b/src/components/sections/AboutSection.tsx @@ -79,8 +79,8 @@ export default function AboutSection(props: AboutSectionProps = {}) { {greeting ?? `Hi, I'm ${name}!`}
- {bio.map((paragraph, index) => ( -

{paragraph}

+ {bio.map((paragraph) => ( +

{paragraph}

))}
diff --git a/src/components/sections/HeroSection.tsx b/src/components/sections/HeroSection.tsx index 94c2b1a..aa18523 100644 --- a/src/components/sections/HeroSection.tsx +++ b/src/components/sections/HeroSection.tsx @@ -14,6 +14,9 @@ interface HeroSectionProps { tertiaryButtonText?: string; statItems?: StatItem[]; hideStats?: boolean; + showPrimaryButton?: boolean; + showSecondaryButton?: boolean; + showTertiaryButton?: boolean; } export default function HeroSection(props: HeroSectionProps = {}) { @@ -30,6 +33,9 @@ export default function HeroSection(props: HeroSectionProps = {}) { tertiaryButtonText = texts.tertiaryButtonText, statItems = texts.statItems, hideStats = false, + showPrimaryButton = true, + showSecondaryButton = true, + showTertiaryButton = true, } = props; const location = useLocation(); const navigate = useNavigate(); @@ -54,14 +60,9 @@ export default function HeroSection(props: HeroSectionProps = {}) { link.remove(); announce('Resume download started successfully'); - } catch (error) { + } catch { announce('Resume download failed, opening in new tab'); - - try { - window.open(resumePDF, '_blank'); - } catch { - announce('Unable to access resume file'); - } + window.open(resumePDF, '_blank'); } }; @@ -88,32 +89,38 @@ export default function HeroSection(props: HeroSectionProps = {}) { {/* Hidden descriptions for screen readers */} diff --git a/src/config/certifications-config.ts b/src/config/certifications-config.ts index a375dae..4878f2c 100644 --- a/src/config/certifications-config.ts +++ b/src/config/certifications-config.ts @@ -1,4 +1,4 @@ -import { certifications } from './locales/en/certifications'; +import { certifications } from './locales/en/technical/certifications'; import type { Certification } from '../data/Certification'; export const certificationsData: Certification[] = certifications.certificationItems diff --git a/src/config/locales/de/index.ts b/src/config/locales/de/index.ts index f4d8981..e13fd64 100644 --- a/src/config/locales/de/index.ts +++ b/src/config/locales/de/index.ts @@ -1,18 +1,19 @@ import { navigation } from './navigation'; import { themeToggle } from '../../theme-toggle'; import { footer } from './footer'; -import { hero } from './hero'; -import { about } from './about'; -import { landingAboutMe } from './landing-aboutme'; -import { landingReasons } from './landing-reasons'; -import { landingExperience } from './landing-experience'; -import { landingWinnings } from './landing-winnings'; -import { landingProcesses } from './landing-processes'; -import { landingReferences } from './landing-references'; -import { services } from './services'; -import { skills } from './skills'; +import { hero } from './technical/hero'; +import { about } from './technical/about'; +import { landingAboutMe } from './landing/landing-aboutme'; +import { landingHero } from './landing/landing-hero'; +import { landingReasons } from './landing/landing-reasons'; +import { landingExperience } from './landing/landing-experience'; +import { landingWinnings } from './landing/landing-winnings'; +import { landingProcesses } from './landing/landing-processes'; +import { landingReferences } from './landing/landing-references'; +import { services } from './technical/services'; +import { skills } from './technical/skills'; import { projects } from './projects'; -import { certifications } from './certifications'; +import { certifications } from './technical/certifications'; import { contact } from './contact'; import { imprint } from './imprint'; import { privacyPolicy } from './privacy-policy'; @@ -25,6 +26,7 @@ export const de = { hero, about, landingAboutMe, + landingHero, landingReasons, landingExperience, landingWinnings, diff --git a/src/config/locales/de/landing-aboutme.ts b/src/config/locales/de/landing-aboutme.ts deleted file mode 100644 index c131e19..0000000 --- a/src/config/locales/de/landing-aboutme.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const landingAboutMe = { - title: 'Mehr Kunden durch Barrierefreiheit', - subtitle: '', - greeting: 'Hi, ich bin Sascha.', - name: 'Sascha', - bio: [ - 'Ich verhelfe Unternehmen zu Webseiten, die von allen genutzt werden können. Eine Webseite, die für alle funktioniert, erreicht alle.', - ], - featureCards: [] as Array<{ title: string; description: string; }>, -}; diff --git a/src/config/locales/de/landing/landing-aboutme.ts b/src/config/locales/de/landing/landing-aboutme.ts new file mode 100644 index 0000000..ec8e426 --- /dev/null +++ b/src/config/locales/de/landing/landing-aboutme.ts @@ -0,0 +1,17 @@ +export const landingAboutMe = { + title: 'Über mich', + subtitle: '', + greeting: 'Hi, ich bin Sascha.', + name: 'Sascha', + bio: [ + `Seit ich zum ersten Mal mit dem Thema Barrierefreiheit in Berührung gekommen bin, habe ich + für mich einen Mehrwert in der Webentwicklung entdeckt, der mich seitdem nicht mehr loslässt. + Barrierefreiheit ist für mich nicht nur ein technisches Thema, sondern eine Angelegenheit, die mit meinen persönlichen Werten + von Inklusion und Gleichberechtigung übereinstimmt. + Ich helfe Webagenturen und Unternehmen bei der Entwicklung und Umsetzung von Apps und Webseiten mit und ohne Barrierefreiheit.`, + `In meiner Freizeit tanze ich Brasilianischen Zouk und West Coast Swing. Ich beschäftige mich mit alternativen + Beziehungsformen und bin ein großer Fan von Science-Fiction-Filmen. Außerdem interessiere ich mich für Persönlichkeitsentwicklung und + Ethik in der Technologie. Ich bin immer offen für neue Kontakte und spannende Projekte, also zögert nicht, mich zu kontaktieren!`, + ], + featureCards: [] as Array<{ title: string; description: string; }>, +}; diff --git a/src/config/locales/de/landing-experience.ts b/src/config/locales/de/landing/landing-experience.ts similarity index 100% rename from src/config/locales/de/landing-experience.ts rename to src/config/locales/de/landing/landing-experience.ts diff --git a/src/config/locales/de/landing/landing-hero.ts b/src/config/locales/de/landing/landing-hero.ts new file mode 100644 index 0000000..93d234c --- /dev/null +++ b/src/config/locales/de/landing/landing-hero.ts @@ -0,0 +1,5 @@ +export const landingHero = { + title: 'Barrierefreiheit: 20\u00A0% mehr Reichweite, 100\u00A0% mehr Zugang.', + description: 'Ich helfe Ihnen, Ihre Website für jeden zugänglich zu machen – im Einklang mit dem BFSG.', + tertiaryButtonText: 'Kontakt aufnehmen', +}; diff --git a/src/config/locales/de/landing-processes.ts b/src/config/locales/de/landing/landing-processes.ts similarity index 100% rename from src/config/locales/de/landing-processes.ts rename to src/config/locales/de/landing/landing-processes.ts diff --git a/src/config/locales/de/landing-reasons.ts b/src/config/locales/de/landing/landing-reasons.ts similarity index 85% rename from src/config/locales/de/landing-reasons.ts rename to src/config/locales/de/landing/landing-reasons.ts index ccf6735..75cb3c6 100644 --- a/src/config/locales/de/landing-reasons.ts +++ b/src/config/locales/de/landing/landing-reasons.ts @@ -3,11 +3,11 @@ export const landingReasons = { reasons: [ { - stat: '15 %', - text: 'aller Menschen in Deutschland haben eine Beeinträchtigung. Das sind 12 Millionen potenzielle Kunden.', + stat: '20\u00A0%', + text: 'aller Menschen in Deutschland haben eine Beeinträchtigung. Das sind 16 Millionen potenzielle Kunden.', }, { - stat: '80 %', + stat: '80\u00A0%', text: 'dieser Menschen verlassen Webseiten, die nicht für sie funktionieren.', }, { diff --git a/src/config/locales/de/landing-references.ts b/src/config/locales/de/landing/landing-references.ts similarity index 100% rename from src/config/locales/de/landing-references.ts rename to src/config/locales/de/landing/landing-references.ts diff --git a/src/config/locales/de/landing-winnings.ts b/src/config/locales/de/landing/landing-winnings.ts similarity index 100% rename from src/config/locales/de/landing-winnings.ts rename to src/config/locales/de/landing/landing-winnings.ts diff --git a/src/config/locales/de/about.ts b/src/config/locales/de/technical/about.ts similarity index 97% rename from src/config/locales/de/about.ts rename to src/config/locales/de/technical/about.ts index 855d031..bd8f023 100644 --- a/src/config/locales/de/about.ts +++ b/src/config/locales/de/technical/about.ts @@ -6,7 +6,7 @@ export const about = { bio: [ 'Ich habe Informatik an der TU Darmstadt studiert und 2016 meinen Bachelor mit einer Arbeit über Motion Sickness in Virtual Reality abgeschlossen. Seitdem arbeite ich als Software- und Webentwickler mit Schwerpunkt auf Angular und React.', 'In meiner Arbeit habe ich gesehen, dass jedes Unternehmen anders ist, aber viele stehen vor der gleichen Herausforderung: ihre digitale Präsenz zugänglich, modern und zukunftssicher zu halten. Vielen Unternehmen ist bereits bewusst, dass Barrierefreiheit gesetzlich vorgeschrieben ist, aber in der Praxis kommt es vor allem darauf an, dass eine Website für alle Nutzer einfach und angenehm funktioniert.', - 'Meine Spezialisierung liegt in Web Accessibility (WCAG 2.1, BFSG, European Accessibility Act). Ich helfe Unternehmen dabei, ihre Websites und Anwendungen zu aktualisieren, sodass sie konform und gleichzeitig für jeden einfach zu bedienen sind. Was meine Kunden schätzen, ist, dass ich technisches Fachwissen in Angular, React und TypeScript mit einem menschenzentrierten Ansatz verbinde. Denn Barrierefreiheit geht nicht nur um Code, sondern um Menschen.', + 'Meine Spezialisierung liegt in Web Accessibility (WCAG 2.2, BFSG, European Accessibility Act). Ich helfe Unternehmen dabei, ihre Websites und Anwendungen zu aktualisieren, sodass sie konform und gleichzeitig für jeden einfach zu bedienen sind. Was meine Kunden schätzen, ist, dass ich technisches Fachwissen in Angular, React und TypeScript mit einem menschenzentrierten Ansatz verbinde. Denn Barrierefreiheit geht nicht nur um Code, sondern um Menschen.', 'Wenn Sie sich fragen, ob Ihre Website bereits den Standards für Barrierefreiheit entspricht, oder wenn Sie das Gefühl haben, dass es Zeit ist, Ihre Online-Präsenz inklusiver und rechtssicher zu gestalten, würde ich Sie gerne unterstützen. Oft können bereits wenige klare Schritte einen großen Unterschied machen und Ihrer Website helfen, mehr Menschen zu erreichen und sich von der Konkurrenz abzuheben.', 'Lassen Sie uns verbinden und schauen, wie wir Ihre Website barrierefrei, modern und zukunftssicher machen können.', ], diff --git a/src/config/locales/de/certifications.ts b/src/config/locales/de/technical/certifications.ts similarity index 72% rename from src/config/locales/de/certifications.ts rename to src/config/locales/de/technical/certifications.ts index ef95eb3..1e35ce8 100644 --- a/src/config/locales/de/certifications.ts +++ b/src/config/locales/de/technical/certifications.ts @@ -1,4 +1,4 @@ -import { certificationItems } from '../../certifications-data'; +import { certificationItems } from '../../../certifications-data'; export const certifications = { title: 'Zertifizierungen & Erfolge', diff --git a/src/config/locales/de/hero.ts b/src/config/locales/de/technical/hero.ts similarity index 89% rename from src/config/locales/de/hero.ts rename to src/config/locales/de/technical/hero.ts index 1968d7b..4db7850 100644 --- a/src/config/locales/de/hero.ts +++ b/src/config/locales/de/technical/hero.ts @@ -1,5 +1,5 @@ export const hero = { - title: 'Jeder verdient ein reibungsloses Online-Erlebnis', + title: 'Barrierefreie Webentwicklung für alle', description: 'Ich helfe Ihnen, barrierefreie Websites zu erstellen, die jeden Besucher einbeziehen, im Einklang mit dem Barrierefreiheitsstärkungsgesetz.', primaryButtonText: 'Projekte ansehen', diff --git a/src/config/locales/de/services.ts b/src/config/locales/de/technical/services.ts similarity index 100% rename from src/config/locales/de/services.ts rename to src/config/locales/de/technical/services.ts diff --git a/src/config/locales/de/skills.ts b/src/config/locales/de/technical/skills.ts similarity index 100% rename from src/config/locales/de/skills.ts rename to src/config/locales/de/technical/skills.ts diff --git a/src/config/locales/en/TextConfig.ts b/src/config/locales/en/TextConfig.ts index 089bfbb..1894060 100644 --- a/src/config/locales/en/TextConfig.ts +++ b/src/config/locales/en/TextConfig.ts @@ -52,6 +52,13 @@ export interface TextConfig { }>; }; + // Landing Hero Section + landingHero: { + title: string; + description: string; + tertiaryButtonText: string; + }; + // Landing About Me Section landingAboutMe: { title: string; diff --git a/src/config/locales/en/index.ts b/src/config/locales/en/index.ts index 01d84c0..e516834 100644 --- a/src/config/locales/en/index.ts +++ b/src/config/locales/en/index.ts @@ -1,18 +1,19 @@ import { navigation } from './navigation'; import { themeToggle } from '../../theme-toggle'; import { footer } from './footer'; -import { hero } from './hero'; -import { about } from './about'; -import { landingAboutMe } from './landing-aboutme'; -import { landingReasons } from './landing-reasons'; -import { landingExperience } from './landing-experience'; -import { landingWinnings } from './landing-winnings'; -import { landingProcesses } from './landing-processes'; -import { landingReferences } from './landing-references'; -import { services } from './services'; -import { skills } from './skills'; +import { hero } from './technical/hero'; +import { about } from './technical/about'; +import { landingAboutMe } from './landing/landing-aboutme'; +import { landingHero } from './landing/landing-hero'; +import { landingReasons } from './landing/landing-reasons'; +import { landingExperience } from './landing/landing-experience'; +import { landingWinnings } from './landing/landing-winnings'; +import { landingProcesses } from './landing/landing-processes'; +import { landingReferences } from './landing/landing-references'; +import { services } from './technical/services'; +import { skills } from './technical/skills'; import { projects } from './projects'; -import { certifications } from './certifications'; +import { certifications } from './technical/certifications'; import { contact } from './contact'; import { imprint } from './imprint'; import { privacyPolicy } from './privacy-policy'; @@ -29,6 +30,7 @@ export const en: TextConfig = { hero, about, landingAboutMe, + landingHero, landingReasons, landingExperience, landingWinnings, diff --git a/src/config/locales/en/landing-aboutme.ts b/src/config/locales/en/landing/landing-aboutme.ts similarity index 100% rename from src/config/locales/en/landing-aboutme.ts rename to src/config/locales/en/landing/landing-aboutme.ts diff --git a/src/config/locales/en/landing-experience.ts b/src/config/locales/en/landing/landing-experience.ts similarity index 100% rename from src/config/locales/en/landing-experience.ts rename to src/config/locales/en/landing/landing-experience.ts diff --git a/src/config/locales/en/landing/landing-hero.ts b/src/config/locales/en/landing/landing-hero.ts new file mode 100644 index 0000000..c040dad --- /dev/null +++ b/src/config/locales/en/landing/landing-hero.ts @@ -0,0 +1,5 @@ +export const landingHero = { + title: 'Accessible websites that reach more customers and ensure legal compliance', + description: 'I help you make your website accessible to everyone – compliant with accessibility laws.', + tertiaryButtonText: 'Get in Touch', +}; diff --git a/src/config/locales/en/landing-processes.ts b/src/config/locales/en/landing/landing-processes.ts similarity index 100% rename from src/config/locales/en/landing-processes.ts rename to src/config/locales/en/landing/landing-processes.ts diff --git a/src/config/locales/en/landing-reasons.ts b/src/config/locales/en/landing/landing-reasons.ts similarity index 100% rename from src/config/locales/en/landing-reasons.ts rename to src/config/locales/en/landing/landing-reasons.ts diff --git a/src/config/locales/en/landing-references.ts b/src/config/locales/en/landing/landing-references.ts similarity index 100% rename from src/config/locales/en/landing-references.ts rename to src/config/locales/en/landing/landing-references.ts diff --git a/src/config/locales/en/landing-winnings.ts b/src/config/locales/en/landing/landing-winnings.ts similarity index 100% rename from src/config/locales/en/landing-winnings.ts rename to src/config/locales/en/landing/landing-winnings.ts diff --git a/src/config/locales/en/about.ts b/src/config/locales/en/technical/about.ts similarity index 100% rename from src/config/locales/en/about.ts rename to src/config/locales/en/technical/about.ts diff --git a/src/config/locales/en/certifications.ts b/src/config/locales/en/technical/certifications.ts similarity index 71% rename from src/config/locales/en/certifications.ts rename to src/config/locales/en/technical/certifications.ts index 5d91c51..d10a981 100644 --- a/src/config/locales/en/certifications.ts +++ b/src/config/locales/en/technical/certifications.ts @@ -1,4 +1,4 @@ -import { certificationItems } from '../../certifications-data'; +import { certificationItems } from '../../../certifications-data'; export const certifications = { title: 'Certifications & Achievements', diff --git a/src/config/locales/en/hero.ts b/src/config/locales/en/technical/hero.ts similarity index 100% rename from src/config/locales/en/hero.ts rename to src/config/locales/en/technical/hero.ts diff --git a/src/config/locales/en/services.ts b/src/config/locales/en/technical/services.ts similarity index 100% rename from src/config/locales/en/services.ts rename to src/config/locales/en/technical/services.ts diff --git a/src/config/locales/en/skills.ts b/src/config/locales/en/technical/skills.ts similarity index 100% rename from src/config/locales/en/skills.ts rename to src/config/locales/en/technical/skills.ts diff --git a/src/contexts/LanguageContext.tsx b/src/contexts/LanguageContext.tsx index d00e9fe..5571c95 100644 --- a/src/contexts/LanguageContext.tsx +++ b/src/contexts/LanguageContext.tsx @@ -11,46 +11,29 @@ interface LanguageContextType { const LanguageContext = createContext(undefined); -const LANGUAGE_KEY = 'preferred-language'; - -function getBrowserLanguage(): Language { - const browserLang = navigator.language.toLowerCase(); - if (browserLang.startsWith('en')) { - return 'en'; - } - return 'de'; // Default to German for all other languages -} - -function getSavedLanguage(): Language | null { - const saved = localStorage.getItem(LANGUAGE_KEY); - if (saved === 'en' || saved === 'de') { - return saved; - } +/** Derive the language from the URL pathname prefix (/en/… or /de/…). */ +export function getLanguageFromPath(pathname: string): Language | null { + if (pathname.startsWith('/en')) return 'en'; + if (pathname.startsWith('/de')) return 'de'; return null; } -export function LanguageProvider({ children }: { children: ReactNode; }) { - const [language, setLanguageState] = useState(() => { - // First check localStorage, then browser language, then default to German - return getSavedLanguage() || getBrowserLanguage(); - }); +export function LanguageProvider({ initialLanguage = 'de', children }: Readonly<{ initialLanguage?: Language; children: ReactNode; }>) { + const [language, setLanguage] = useState(initialLanguage); const texts = language === 'de' ? de : en; - const setLanguage = (lang: Language) => { - setLanguageState(lang); - localStorage.setItem(LANGUAGE_KEY, lang); - // Update HTML lang attribute for accessibility and SEO + const handleSetLanguage = (lang: Language) => { + setLanguage(lang); document.documentElement.lang = lang; }; useEffect(() => { - // Set initial lang attribute document.documentElement.lang = language; }, [language]); return ( - + {children} ); diff --git a/src/hooks/usePageSeo.ts b/src/hooks/usePageSeo.ts index bd4ed13..4986e84 100644 --- a/src/hooks/usePageSeo.ts +++ b/src/hooks/usePageSeo.ts @@ -5,11 +5,12 @@ interface SeoProps { description: string; canonical?: string; noIndex?: boolean; + image?: string; } const BASE_URL = 'https://sascha-bach.de'; -export function usePageSeo({ title, description, canonical, noIndex }: SeoProps) { +export function usePageSeo({ title, description, canonical, noIndex, image }: SeoProps) { useEffect(() => { document.title = title; @@ -30,6 +31,12 @@ export function usePageSeo({ title, description, canonical, noIndex }: SeoProps) setMeta('twitter:title', title); setMeta('twitter:description', description); + if (image) { + const imageUrl = image.startsWith('http') ? image : `${BASE_URL}${image}`; + setMeta('og:image', imageUrl, true); + setMeta('twitter:image', imageUrl); + } + if (canonical) { setMeta('og:url', `${BASE_URL}${canonical}`, true); diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 5d3d9b2..3e3746e 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -6,12 +6,18 @@ import CertificationsSection from '../components/sections/CertificationsSection' import ProjectsSection from '../components/sections/ProjectsSection'; import ContactSection from '../components/sections/ContactSection'; import { usePageSeo } from '../hooks/usePageSeo'; +import { useLanguage } from '../contexts/LanguageContext'; export default function HomePage() { + const { language } = useLanguage(); usePageSeo({ - title: 'Sascha Bach | Technical Portfolio – Skills, Services & Projects', - description: 'Explore the technical portfolio of Sascha Bach: services, skills, certifications, and projects in React, TypeScript, and accessible web development.', - canonical: '/technical', + title: language === 'de' + ? 'Sascha Bach | Technisches Portfolio – Skills, Services & Projekte' + : 'Sascha Bach | Technical Portfolio – Skills, Services & Projects', + description: language === 'de' + ? 'Das technische Portfolio von Sascha Bach: Services, Fähigkeiten, Zertifizierungen und Projekte in React, TypeScript und barrierefreier Webentwicklung.' + : 'Explore the technical portfolio of Sascha Bach: services, skills, certifications, and projects in React, TypeScript, and accessible web development.', + canonical: `/${language}/technical`, }); return ( diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index e9a7c1c..169b49a 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -1,3 +1,4 @@ +import HeroSection from '../components/sections/HeroSection'; import LandingAboutMeSection from '../components/landing/LandingAboutMeSection'; import ReasonsSection from '../components/landing/ReasonsSection'; import ExperienceSection from '../components/landing/ExperienceSection'; @@ -7,16 +8,31 @@ import ProjectsSection from '../components/sections/ProjectsSection'; import ReferencesSection from '../components/landing/ReferencesSection'; import ContactSection from '../components/sections/ContactSection'; import { usePageSeo } from '../hooks/usePageSeo'; +import { useLanguage } from '../contexts/LanguageContext'; export default function LandingPage() { + const { language, texts } = useLanguage(); + const t = texts.landingHero; usePageSeo({ - title: 'Sascha Bach | Freelance Software Developer – Accessible Web Development', - description: 'Freelance software developer in Germany specializing in accessible web development. Building inclusive websites compliant with the Accessibility Act.', - canonical: '/', + title: language === 'de' + ? 'Sascha Bach | Freelance Softwareentwickler – Barrierefreie Webentwicklung' + : 'Sascha Bach | Freelance Software Developer – Accessible Web Development', + description: language === 'de' + ? 'Freelance Softwareentwickler in Deutschland spezialisiert auf barrierefreie Webentwicklung. Ich baue inklusive Websites konform zum BFSG mit React, TypeScript und modernen Frameworks.' + : 'Freelance software developer in Germany specializing in accessible web development. Building inclusive websites compliant with the Accessibility Act.', + canonical: `/${language}/`, }); return ( <> + diff --git a/src/scss/sections/hero-section.scss b/src/scss/sections/hero-section.scss index f5f0c2c..852bff4 100644 --- a/src/scss/sections/hero-section.scss +++ b/src/scss/sections/hero-section.scss @@ -47,7 +47,7 @@ } &__container { - max-width: 800px; + max-width: 900px; width: 100%; text-align: center; @include flex-center; diff --git a/src/scss/sections/landing-hero-section.scss b/src/scss/sections/landing-hero-section.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/scrollUtils.ts b/src/utils/scrollUtils.ts index 6e2faff..54aa508 100644 --- a/src/utils/scrollUtils.ts +++ b/src/utils/scrollUtils.ts @@ -3,13 +3,30 @@ import type { NavigateFunction } from 'react-router-dom'; // Offset to account for sticky navbar height (65px) plus some padding const SCROLL_OFFSET = 65; +function isScrollablePath(pathname: string): boolean { + return ( + pathname === '/de/' || + pathname === '/en/' || + pathname === '/de/technical' || + pathname === '/en/technical' || + // legacy fallbacks + pathname === '/' || + pathname === '/technical' + ); +} + +/** Returns /de/technical or /en/technical depending on the current path prefix. */ +function getTechnicalPath(currentPath: string): string { + if (currentPath.startsWith('/en')) return '/en/technical'; + return '/de/technical'; +} + export function scrollToSection( sectionId: string, currentPath: string, navigate?: NavigateFunction ): void { - const isScrollablePage = currentPath === '/technical' || currentPath === '/'; - if (isScrollablePage) { + if (isScrollablePath(currentPath)) { const element = document.getElementById(sectionId); if (element) { const elementPosition = element.getBoundingClientRect().top; @@ -21,11 +38,9 @@ export function scrollToSection( }); } } else if (navigate) { - // If we're on another page, navigate to homepage with hash - navigate(`/technical#${sectionId}`); + navigate(`${getTechnicalPath(currentPath)}#${sectionId}`); } else { - // Fallback: direct navigation without router - globalThis.location.href = `/technical#${sectionId}`; + globalThis.location.href = `${getTechnicalPath(currentPath)}#${sectionId}`; } } diff --git a/vercel.json b/vercel.json index 3cdefcc..ce49b4c 100644 --- a/vercel.json +++ b/vercel.json @@ -3,13 +3,13 @@ "outputDirectory": "dist", "framework": "vite", "rewrites": [ - { - "source": "/privacy-policy", - "destination": "/privacy-policy.html" - }, - { - "source": "/(.*)", - "destination": "/index.html" - } + { "source": "/", "destination": "/de/" }, + { "source": "/technical", "destination": "/de/technical" }, + { "source": "/de/", "destination": "/de/index.html" }, + { "source": "/de/technical", "destination": "/de/technical/index.html" }, + { "source": "/en/", "destination": "/en/index.html" }, + { "source": "/en/technical", "destination": "/en/technical/index.html" }, + { "source": "/privacy-policy", "destination": "/privacy-policy.html" }, + { "source": "/(.*)", "destination": "/index.html" } ] }