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:
parent
8ae3c33cd2
commit
0f4a379716
179
index.html
179
index.html
|
|
@ -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>
|
||||
|
|
|
|||
28
prerender.js
28
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/<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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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<React.SetStateAction<Theme>>; }>) {
|
||||
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 (
|
||||
<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>
|
||||
{/* 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>
|
||||
</main>
|
||||
<Footer />
|
||||
<BackToTopButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppRouter() {
|
||||
const [theme, setTheme] = useState<Theme>('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 (
|
||||
<Router>
|
||||
<div className="app">
|
||||
<a href="#main-content" className="skip-link">Skip to main content</a>
|
||||
<Navbar theme={theme} setTheme={setTheme} />
|
||||
<main id="main-content" className="app__main">
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/technical" element={<HomePage />} />
|
||||
<Route path="/imprint" element={<ImprintPage />} />
|
||||
<Route path="/privacy-policy" element={<PrivacyPolicy />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer />
|
||||
<BackToTopButton />
|
||||
</div>
|
||||
<AppShell theme={theme} setTheme={setTheme} />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -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'}`);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 = {}) {
|
|||
</header>
|
||||
|
||||
<nav className="hero-section__buttons" role="navigation" aria-label="Main actions">
|
||||
<button
|
||||
className="hero-section__button hero-section__button--primary"
|
||||
onClick={handleViewWork}
|
||||
aria-label="View my work and projects"
|
||||
type="button"
|
||||
>
|
||||
{primaryButtonText}
|
||||
</button>
|
||||
<button
|
||||
className="hero-section__button hero-section__button--secondary"
|
||||
onClick={handleDownloadResume}
|
||||
aria-label="Download my resume as PDF file"
|
||||
type="button"
|
||||
aria-describedby="resume-description"
|
||||
>
|
||||
<Download className="hero-section__button-icon" aria-hidden="true" />
|
||||
{secondaryButtonText}
|
||||
</button>
|
||||
<button
|
||||
className="hero-section__button hero-section__button--tertiary"
|
||||
onClick={handleGetInTouch}
|
||||
aria-label="Navigate to contact section to get in touch"
|
||||
type="button"
|
||||
>
|
||||
{tertiaryButtonText}
|
||||
</button>
|
||||
{showPrimaryButton && (
|
||||
<button
|
||||
className="hero-section__button hero-section__button--primary"
|
||||
onClick={handleViewWork}
|
||||
aria-label="View my work and projects"
|
||||
type="button"
|
||||
>
|
||||
{primaryButtonText}
|
||||
</button>
|
||||
)}
|
||||
{showSecondaryButton && (
|
||||
<button
|
||||
className="hero-section__button hero-section__button--secondary"
|
||||
onClick={handleDownloadResume}
|
||||
aria-label="Download my resume as PDF file"
|
||||
type="button"
|
||||
aria-describedby="resume-description"
|
||||
>
|
||||
<Download className="hero-section__button-icon" aria-hidden="true" />
|
||||
{secondaryButtonText}
|
||||
</button>
|
||||
)}
|
||||
{showTertiaryButton && (
|
||||
<button
|
||||
className="hero-section__button hero-section__button--tertiary"
|
||||
onClick={handleGetInTouch}
|
||||
aria-label="Navigate to contact section to get in touch"
|
||||
type="button"
|
||||
>
|
||||
{tertiaryButtonText}
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Hidden descriptions for screen readers */}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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; }>,
|
||||
};
|
||||
|
|
@ -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; }>,
|
||||
};
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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.',
|
||||
},
|
||||
{
|
||||
|
|
@ -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.',
|
||||
],
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { certificationItems } from '../../certifications-data';
|
||||
import { certificationItems } from '../../../certifications-data';
|
||||
|
||||
export const certifications = {
|
||||
title: 'Zertifizierungen & Erfolge',
|
||||
|
|
@ -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',
|
||||
|
|
@ -52,6 +52,13 @@ export interface TextConfig {
|
|||
}>;
|
||||
};
|
||||
|
||||
// Landing Hero Section
|
||||
landingHero: {
|
||||
title: string;
|
||||
description: string;
|
||||
tertiaryButtonText: string;
|
||||
};
|
||||
|
||||
// Landing About Me Section
|
||||
landingAboutMe: {
|
||||
title: string;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { certificationItems } from '../../certifications-data';
|
||||
import { certificationItems } from '../../../certifications-data';
|
||||
|
||||
export const certifications = {
|
||||
title: 'Certifications & Achievements',
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
}
|
||||
|
||||
&__container {
|
||||
max-width: 800px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
@include flex-center;
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
16
vercel.json
16
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" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue