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.
This commit is contained in:
Sascha 2026-04-08 13:14:49 +02:00
parent 8ae3c33cd2
commit 0f4a379716
48 changed files with 474 additions and 212 deletions

View File

@ -17,16 +17,20 @@
name="keywords"
content="freelance software developer, accessible web development, barrierefreie Webentwicklung, React developer, TypeScript, web accessibility, BFSG, Germany"
/>
<link rel="canonical" href="https://sascha-bach.de/" />
<link rel="canonical" href="https://sascha-bach.de/de/" />
<!-- Hreflang for multi-language -->
<link rel="alternate" hreflang="en" href="https://sascha-bach.de/" />
<link rel="alternate" hreflang="de" href="https://sascha-bach.de/" />
<link rel="alternate" hreflang="x-default" href="https://sascha-bach.de/" />
<link rel="alternate" hreflang="de" href="https://sascha-bach.de/de/" />
<link rel="alternate" hreflang="en" href="https://sascha-bach.de/en/" />
<link
rel="alternate"
hreflang="x-default"
href="https://sascha-bach.de/de/"
/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://sascha-bach.de/" />
<meta property="og:url" content="https://sascha-bach.de/de/" />
<meta
property="og:title"
content="Sascha Bach | Freelance Software Developer"
@ -35,41 +39,88 @@
property="og:description"
content="Freelance software developer in Germany specializing in accessible web development. Building inclusive websites compliant with the Accessibility Act."
/>
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="de_DE" />
<meta property="og:site_name" content="Sascha Bach Portfolio" />
<meta property="og:image" content="https://sascha-bach.de/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image:alt"
content="Sascha Bach Freelance Software Developer specializing in accessible web development"
/>
<meta property="og:locale" content="de_DE" />
<meta property="og:locale:alternate" content="en_US" />
<meta property="og:site_name" content="Sascha Bach" />
<!-- Twitter -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Sascha Bach | Freelance Software Developer"
content="Sascha Bach | Freelance Software Developer | Accessibility WCAG Barrierefreiheitsstärkungsgesetz"
/>
<meta
name="twitter:description"
content="Freelance software developer in Germany specializing in accessible web development. Building inclusive websites compliant with the Accessibility Act."
content="Freelance software developer in Germany specializing in accessible web development. Building inclusive websites compliant with the Accessibility Act and WCAG."
/>
<meta name="twitter:image" content="https://sascha-bach.de/og-image.png" />
<meta
name="twitter:image:alt"
content="Sascha Bach Freelance Software Developer specializing in accessible web development"
/>
<!-- JSON-LD Structured Data -->
<!-- JSON-LD Structured Data: Person (primary entity) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"@id": "https://sascha-bach.de/#person",
"name": "Sascha Bach",
"jobTitle": "Freelance Software Developer",
"url": "https://sascha-bach.de",
"image": "https://sascha-bach.de/og-image.png",
"description": "Sascha Bach is a freelance software developer based in Germany specializing in accessible web development (BFSG, WCAG 2.1, European Accessibility Act) using React, TypeScript, and Angular.",
"knowsLanguage": ["de", "en"],
"knowsAbout": [
"Web Accessibility",
"BFSG",
"WCAG 2.1",
"React",
"TypeScript",
"Angular",
"Frontend Development"
],
"address": {
"@type": "PostalAddress",
"addressRegion": "Hessen",
"addressCountry": "DE"
},
"sameAs": [
"https://www.linkedin.com/in/saschabach/",
"https://codeberg.org/saschab"
],
"worksFor": {
"@id": "https://sascha-bach.de/#business"
}
}
</script>
<!-- JSON-LD Structured Data: ProfessionalService -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ProfessionalService",
"@id": "https://sascha-bach.de/#business",
"name": "Sascha Bach Freelance Software Developer",
"url": "https://sascha-bach.de",
"description": "Freelance software developer specializing in accessible web development, React, TypeScript, and modern frameworks.",
"founder": {
"@type": "Person",
"name": "Sascha Bach",
"jobTitle": "Freelance Software Developer",
"url": "https://sascha-bach.de",
"sameAs": [
"https://www.linkedin.com/in/saschabach/",
"https://codeberg.org/saschab"
]
"logo": "https://sascha-bach.de/og-image.png",
"image": "https://sascha-bach.de/og-image.png",
"description": "Freelance software developer specializing in accessible web development, React, TypeScript, Angular, and BFSG/WCAG compliance.",
"founder": { "@id": "https://sascha-bach.de/#person" },
"address": {
"@type": "PostalAddress",
"addressRegion": "Berlin",
"addressCountry": "DE"
},
"areaServed": "DE",
"knowsLanguage": ["en", "de"],
"areaServed": ["DE", "AT", "CH"],
"knowsLanguage": ["de", "en"],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Web Development Services",
@ -78,20 +129,96 @@
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Accessible Web Development"
"name": "Accessible Web Development (BFSG / WCAG 2.2)"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Frontend Development (React, TypeScript)"
"name": "Frontend Development (React, TypeScript, Angular)"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Responsive Web Design"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Performance Optimization"
}
}
]
}
}
</script>
<!-- JSON-LD Structured Data: WebSite -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"@id": "https://sascha-bach.de/#website",
"url": "https://sascha-bach.de",
"name": "Sascha Bach",
"publisher": { "@id": "https://sascha-bach.de/#person" }
}
</script>
<!-- JSON-LD Structured Data: FAQ (GEO) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Was ist das BFSG und wen betrifft es?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Das Barrierefreiheitsstärkungsgesetz (BFSG) verpflichtet Unternehmen ab dem 28. Juni 2025, ihre digitalen Produkte und Websites barrierefrei zu gestalten. Es gilt für Unternehmen mit mehr als 10 Mitarbeitern oder mehr als 2 Millionen Euro Jahresumsatz."
}
},
{
"@type": "Question",
"name": "What is accessible web development?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Accessible web development means building websites that can be used by everyone, including people with visual, auditory, motor, or cognitive disabilities. It follows the Web Content Accessibility Guidelines (WCAG 2.1) and ensures compatibility with assistive technologies like screen readers."
}
},
{
"@type": "Question",
"name": "Welche Technologien nutzt Sascha Bach für die Webentwicklung?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Sascha Bach entwickelt mit React, Angular, TypeScript, JavaScript und modernen CSS-Methoden. Der Fokus liegt auf barrierefreien, responsiven Webanwendungen nach WCAG 2.2 und BFSG-Standard."
}
},
{
"@type": "Question",
"name": "How does web accessibility improve SEO?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Accessible websites use semantic HTML, proper heading structures, descriptive alt texts, and keyboard-navigable interfaces — all of which search engines reward with better rankings. Google's crawlers are effectively assistive technologies themselves."
}
},
{
"@type": "Question",
"name": "Wie kann ich prüfen, ob meine Website barrierefrei ist?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Eine erste Überprüfung ist mit kostenlosen Tools wie dem WAVE Web Accessibility Evaluator oder Lighthouse in den Chrome DevTools möglich. Für eine vollständige BFSG-konforme Prüfung empfiehlt sich eine manuelle Analyse durch einen Experten (wie mich ;-) )."
}
}
]
}
</script>
</head>
<body>
<div id="root"></div>

View File

@ -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/<route>/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)
})
}

BIN
public/og-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,18 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<!-- German landing -->
<url>
<loc>https://sascha-bach.de/</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://sascha-bach.de/" />
<xhtml:link rel="alternate" hreflang="de" href="https://sascha-bach.de/" />
<loc>https://sascha-bach.de/de/</loc>
<xhtml:link rel="alternate" hreflang="de" href="https://sascha-bach.de/de/" />
<xhtml:link rel="alternate" hreflang="en" href="https://sascha-bach.de/en/" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://sascha-bach.de/de/" />
<lastmod>2025-06-28</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<!-- English landing -->
<url>
<loc>https://sascha-bach.de/technical</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://sascha-bach.de/technical" />
<xhtml:link rel="alternate" hreflang="de" href="https://sascha-bach.de/technical" />
<loc>https://sascha-bach.de/en/</loc>
<xhtml:link rel="alternate" hreflang="de" href="https://sascha-bach.de/de/" />
<xhtml:link rel="alternate" hreflang="en" href="https://sascha-bach.de/en/" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://sascha-bach.de/de/" />
<lastmod>2025-06-28</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<!-- German technical portfolio -->
<url>
<loc>https://sascha-bach.de/de/technical</loc>
<xhtml:link rel="alternate" hreflang="de" href="https://sascha-bach.de/de/technical" />
<xhtml:link rel="alternate" hreflang="en" href="https://sascha-bach.de/en/technical" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://sascha-bach.de/de/technical" />
<lastmod>2025-06-28</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<!-- English technical portfolio -->
<url>
<loc>https://sascha-bach.de/en/technical</loc>
<xhtml:link rel="alternate" hreflang="de" href="https://sascha-bach.de/de/technical" />
<xhtml:link rel="alternate" hreflang="en" href="https://sascha-bach.de/en/technical" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://sascha-bach.de/de/technical" />
<lastmod>2025-06-28</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
</urlset>

View File

@ -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,29 +8,29 @@ 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';
function AppRouter() {
const [theme, setTheme] = useState<Theme>('light');
/** Syncs the LanguageContext whenever the URL prefix (/de/ or /en/) changes. */
function LanguageSyncer() {
const { setLanguage, language } = useLanguage();
const location = useLocation();
useEffect(() => {
// Check for saved theme preference or default to light mode
const savedTheme = localStorage.getItem('theme') as Theme | null;
if (savedTheme) {
setTheme(savedTheme);
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<React.SetStateAction<Theme>>; }>) {
const location = useLocation();
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);
const hash = location.hash.substring(1);
if (hash) {
setTimeout(() => {
const element = document.getElementById(hash);
@ -39,28 +39,28 @@ function AppRouter() {
}
}, 100);
}
};
// Check hash on initial load
handleHashChange();
// Listen for hash changes
globalThis.addEventListener('hashchange', handleHashChange);
return () => {
globalThis.removeEventListener('hashchange', handleHashChange);
};
}, []);
}, [location.hash]);
return (
<Router>
<div className="app">
<a href="#main-content" className="skip-link">Skip to main content</a>
<LanguageSyncer />
<Navbar theme={theme} setTheme={setTheme} />
<main id="main-content" className="app__main">
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/technical" element={<HomePage />} />
{/* Root redirects → German (primary market) */}
<Route path="/" element={<Navigate to="/de/" replace />} />
<Route path="/technical" element={<Navigate to="/de/technical" replace />} />
{/* German routes */}
<Route path="/de/" element={<LandingPage />} />
<Route path="/de/technical" element={<HomePage />} />
{/* English routes */}
<Route path="/en/" element={<LandingPage />} />
<Route path="/en/technical" element={<HomePage />} />
{/* Shared pages (noindex, language-independent) */}
<Route path="/imprint" element={<ImprintPage />} />
<Route path="/privacy-policy" element={<PrivacyPolicy />} />
</Routes>
@ -68,6 +68,27 @@ function AppRouter() {
<Footer />
<BackToTopButton />
</div>
);
}
function AppRouter() {
const [theme, setTheme] = useState<Theme>('light');
useEffect(() => {
const savedTheme = localStorage.getItem('theme') as Theme | null;
if (savedTheme) {
setTheme(savedTheme);
}
}, []);
useEffect(() => {
document.documentElement.dataset.theme = theme;
localStorage.setItem('theme', theme);
}, [theme]);
return (
<Router>
<AppShell theme={theme} setTheme={setTheme} />
</Router>
);
}

View File

@ -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 (
<LanguageProvider>
<LanguageProvider initialLanguage={initialLanguage}>
<AppRouter />
</LanguageProvider>
);

BIN
src/assets/og-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -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'}`);
}
};

View File

@ -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 (
<div className={`navbar${isLandingPage ? ' navbar--landing' : ''}`}>
<Link
to={isLandingPage ? '/' : '/technical'}
to={isLandingPage ? location.pathname : technicalPath}
className="navbar__name"
onClick={(e) => {
e.preventDefault();

View File

@ -79,8 +79,8 @@ export default function AboutSection(props: AboutSectionProps = {}) {
{greeting ?? `Hi, I'm ${name}!`}
</h3>
<div className="about-section__bio-text">
{bio.map((paragraph, index) => (
<p key={index}>{paragraph}</p>
{bio.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>
))}
</div>
</div>

View File

@ -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) {
announce('Resume download failed, opening in new tab');
try {
window.open(resumePDF, '_blank');
} catch {
announce('Unable to access resume file');
}
announce('Resume download failed, opening in new tab');
window.open(resumePDF, '_blank');
}
};
@ -88,6 +89,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
</header>
<nav className="hero-section__buttons" role="navigation" aria-label="Main actions">
{showPrimaryButton && (
<button
className="hero-section__button hero-section__button--primary"
onClick={handleViewWork}
@ -96,6 +98,8 @@ export default function HeroSection(props: HeroSectionProps = {}) {
>
{primaryButtonText}
</button>
)}
{showSecondaryButton && (
<button
className="hero-section__button hero-section__button--secondary"
onClick={handleDownloadResume}
@ -106,6 +110,8 @@ export default function HeroSection(props: HeroSectionProps = {}) {
<Download className="hero-section__button-icon" aria-hidden="true" />
{secondaryButtonText}
</button>
)}
{showTertiaryButton && (
<button
className="hero-section__button hero-section__button--tertiary"
onClick={handleGetInTouch}
@ -114,6 +120,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
>
{tertiaryButtonText}
</button>
)}
</nav>
{/* Hidden descriptions for screen readers */}

View File

@ -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

View File

@ -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,

View File

@ -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; }>,
};

View File

@ -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; }>,
};

View File

@ -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',
};

View File

@ -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.',
},
{

View File

@ -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.',
],

View File

@ -1,4 +1,4 @@
import { certificationItems } from '../../certifications-data';
import { certificationItems } from '../../../certifications-data';
export const certifications = {
title: 'Zertifizierungen & Erfolge',

View File

@ -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',

View File

@ -52,6 +52,13 @@ export interface TextConfig {
}>;
};
// Landing Hero Section
landingHero: {
title: string;
description: string;
tertiaryButtonText: string;
};
// Landing About Me Section
landingAboutMe: {
title: string;

View File

@ -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,

View File

@ -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',
};

View File

@ -1,4 +1,4 @@
import { certificationItems } from '../../certifications-data';
import { certificationItems } from '../../../certifications-data';
export const certifications = {
title: 'Certifications & Achievements',

View File

@ -11,46 +11,29 @@ interface LanguageContextType {
const LanguageContext = createContext<LanguageContextType | undefined>(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<Language>(() => {
// 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<Language>(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 (
<LanguageContext.Provider value={{ language, setLanguage, texts }}>
<LanguageContext.Provider value={{ language, setLanguage: handleSetLanguage, texts }}>
{children}
</LanguageContext.Provider>
);

View File

@ -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);

View File

@ -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 (

View File

@ -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 (
<>
<HeroSection
title={t.title}
description={t.description}
tertiaryButtonText={t.tertiaryButtonText}
showPrimaryButton={false}
showSecondaryButton={false}
hideStats={true}
/>
<LandingAboutMeSection />
<ReasonsSection />
<WinningsSection />

View File

@ -47,7 +47,7 @@
}
&__container {
max-width: 800px;
max-width: 900px;
width: 100%;
text-align: center;
@include flex-center;

View File

@ -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}`;
}
}

View File

@ -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" }
]
}