feat: enhance text configuration and localization for appointment booking

- Added new text configurations for appointment hints and links in TextConfig.ts and contact.ts.
- Updated imprint.ts with detailed privacy policy sections regarding online appointment booking, including data handling and legal basis.
- Introduced new appointment-related fields in personal.ts for scheduling.
- Implemented SEO enhancements across HomePage, ImprintPage, LandingPage, and PrivacyPolicy pages using usePageSeo hook.
- Created a prerendering script to generate static HTML for SEO optimization.
- Added .htaccess and robots.txt for server configuration and search engine indexing control.
- Developed a sitemap.xml for better search engine visibility.
- Improved styling for contact section and imprint page to accommodate new appointment features.
This commit is contained in:
Sascha 2026-03-26 21:19:46 +01:00
parent 54d3a810d2
commit bdbce1c3ad
23 changed files with 1547 additions and 133 deletions

View File

@ -3,10 +3,84 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sascha Bach Portfolio</title>
<!-- Primary Meta Tags -->
<title>Sascha Bach | Freelance Software Developer Accessible Web Development</title>
<meta name="description" content="Freelance software developer in Germany specializing in accessible web development. I build inclusive websites compliant with the Accessibility Act using React, TypeScript, and modern frameworks." />
<meta name="author" content="Sascha Bach" />
<meta 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/" />
<!-- 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/" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://sascha-bach.de/" />
<meta property="og:title" content="Sascha Bach | Freelance Software Developer" />
<meta 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" />
<!-- Twitter -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Sascha Bach | Freelance Software Developer" />
<meta name="twitter:description" content="Freelance software developer in Germany specializing in accessible web development. Building inclusive websites compliant with the Accessibility Act." />
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ProfessionalService",
"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"
]
},
"areaServed": "DE",
"knowsLanguage": ["en", "de"],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Web Development Services",
"itemListElement": [
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Accessible Web Development"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Frontend Development (React, TypeScript)"
}
}
]
}
}
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<noscript>
<h1>Sascha Bach Freelance Software Developer</h1>
<p>Freelance software developer in Germany specializing in accessible web development. Building inclusive websites compliant with the Accessibility Act using React, TypeScript, and modern frameworks.</p>
<p>Contact: freelancer [at] sascha-bach.de</p>
<p>Visit with JavaScript enabled for the full experience.</p>
</noscript>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

1002
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build && node prerender.js",
"build:no-prerender": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"deploy": "npm run build && node deploy.js", "deploy": "npm run build && node deploy.js",
@ -30,6 +31,7 @@
"eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0", "globals": "^16.3.0",
"puppeteer": "^24.40.0",
"sass": "^1.90.0", "sass": "^1.90.0",
"ssh2-sftp-client": "^12.0.1", "ssh2-sftp-client": "^12.0.1",
"typescript": "~5.8.3", "typescript": "~5.8.3",

101
prerender.js Normal file
View File

@ -0,0 +1,101 @@
/**
* Post-build prerendering script.
* Starts a local server from dist/, visits each route with Puppeteer,
* and saves the fully-rendered HTML so crawlers see real content.
*
* 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 puppeteer from 'puppeteer';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DIST = join(__dirname, 'dist');
const PORT = 4173;
const ROUTES = ['/', '/technical', '/imprint', '/privacy-policy'];
/** Minimal static file server that falls back to index.html (SPA). */
function startServer() {
const mimeTypes = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.pdf': 'application/pdf',
'.woff2': 'font/woff2',
'.woff': 'font/woff',
};
const server = createServer((req, res) => {
let filePath = join(DIST, req.url === '/' ? '/index.html' : req.url);
if (!existsSync(filePath) || !filePath.includes('.')) {
filePath = join(DIST, 'index.html');
}
const ext = '.' + filePath.split('.').pop();
const contentType = mimeTypes[ext] || 'application/octet-stream';
try {
const content = readFileSync(filePath);
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
} catch {
res.writeHead(404);
res.end('Not found');
}
});
return new Promise((resolve) => {
server.listen(PORT, () => {
console.log(` Server running on http://localhost:${PORT}`);
resolve(server);
});
});
}
async function prerender() {
console.log('\n🔍 Prerendering routes...\n');
const server = await startServer();
const browser = await puppeteer.launch({ headless: true });
for (const route of ROUTES) {
const page = await browser.newPage();
const url = `http://localhost:${PORT}${route}`;
console.log(` Rendering ${route} ...`);
await page.goto(url, { waitUntil: 'networkidle0', timeout: 15000 });
// Wait a bit for React to finish any async rendering
await new Promise((r) => setTimeout(r, 1500));
const html = await page.content();
await page.close();
// Write to dist/<route>/index.html
const outDir = route === '/' ? DIST : join(DIST, route);
if (!existsSync(outDir)) {
mkdirSync(outDir, { recursive: true });
}
const outFile = join(outDir, 'index.html');
writeFileSync(outFile, html, 'utf-8');
console.log(` ✓ Saved ${outFile.replace(DIST, 'dist')}`);
}
await browser.close();
server.close();
console.log('\n✅ Prerendering complete!\n');
}
prerender().catch((err) => {
console.error('Prerendering failed:', err);
process.exit(1);
});

4
public/.htaccess Normal file
View File

@ -0,0 +1,4 @@
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [QSA,L]

6
public/robots.txt Normal file
View File

@ -0,0 +1,6 @@
User-agent: *
Allow: /
Disallow: /imprint
Disallow: /privacy-policy
Sitemap: https://sascha-bach.de/sitemap.xml

18
public/sitemap.xml Normal file
View File

@ -0,0 +1,18 @@
<?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">
<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/" />
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<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" />
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>

View File

@ -7,7 +7,6 @@ import saschaImage from '../../assets/sascha.png';
// Define feature card icon and color mappings as constants // Define feature card icon and color mappings as constants
const FEATURE_CARD_ICONS = [Code, Palette, ShieldCheck, Eye] as const; const FEATURE_CARD_ICONS = [Code, Palette, ShieldCheck, Eye] as const;
const FEATURE_CARD_COLORS = ['primary', 'secondary', 'tertiary', 'quaternary'] as const; const FEATURE_CARD_COLORS = ['primary', 'secondary', 'tertiary', 'quaternary'] as const;
type FeatureCardColor = typeof FEATURE_CARD_COLORS[number];
interface AboutSectionProps { interface AboutSectionProps {
name?: string; name?: string;

View File

@ -1,5 +1,5 @@
// eslint-disable-next-line @typescript-eslint/no-deprecated // eslint-disable-next-line @typescript-eslint/no-deprecated
import { Mail, Send, Globe, Linkedin } from 'lucide-react'; import { Mail, Send, Globe, Linkedin, CalendarClock } from 'lucide-react';
import { personalConfig } from '../../config/personal'; import { personalConfig } from '../../config/personal';
import { useLanguage } from '../../contexts/LanguageContext'; import { useLanguage } from '../../contexts/LanguageContext';
import { useScreenReaderAnnouncements } from '../../hooks/useScreenReaderAnnouncements'; import { useScreenReaderAnnouncements } from '../../hooks/useScreenReaderAnnouncements';
@ -89,6 +89,20 @@ export default function ContactSection(props: ContactSectionProps = {}) {
</div> </div>
</div> </div>
<div className="contact-section__appointment-hint">
<CalendarClock className="contact-section__appointment-icon" aria-hidden="true" />
<span className="contact-section__appointment-text">{texts.appointmentHintText}</span>
<a
href={personalConfig.appointmentUrl}
target="_blank"
rel="noopener noreferrer"
className="contact-section__appointment-link"
aria-label={`${texts.appointmentLinkText} (opens in new tab)`}
>
{texts.appointmentLinkText}
</a>
</div>
<button <button
onClick={handleEmailClick} onClick={handleEmailClick}
className="contact-section__email-button" className="contact-section__email-button"

View File

@ -20,4 +20,6 @@ export const contact = {
socialAnnouncement: '{platform}-Profil wird in neuem Tab geöffnet', socialAnnouncement: '{platform}-Profil wird in neuem Tab geöffnet',
codebergLinkText: 'Sascha Bach auf Codeberg', codebergLinkText: 'Sascha Bach auf Codeberg',
linkedinLinkText: 'Sascha Bach auf LinkedIn', linkedinLinkText: 'Sascha Bach auf LinkedIn',
appointmentHintText: 'Lieber einen Termin buchen?',
appointmentLinkText: 'Jetzt Termin vereinbaren',
}; };

View File

@ -116,10 +116,17 @@ export const imprint = {
name: 'Bitpalast GmbH', name: 'Bitpalast GmbH',
address: 'Postfach 19 15 64, D-14005 Berlin, Deutschland', address: 'Postfach 19 15 64, D-14005 Berlin, Deutschland',
website: 'https://preiswerter-webserver-de.bitpalast.net/', website: 'https://preiswerter-webserver-de.bitpalast.net/',
privacyPolicyLabel: 'Datenschutzerklärung:',
}, },
contactTitle: 'Kontaktaufnahme', contactTitle: 'Kontaktaufnahme',
contactText: contactText:
'Soweit Sie uns über E-Mail, Soziale Medien, Telefon, Fax, Post, unser Kontaktformular oder sonstwie ansprechen und uns hierbei personenbezogene Daten wie Ihren Namen, Ihre Telefonnummer oder Ihre E-Mail-Adresse zur Verfügung stellen oder weitere Angaben zur Ihrer Person oder Ihrem Anliegen machen, verarbeiten wir diese Daten zur Beantwortung Ihrer Anfrage im Rahmen des zwischen uns bestehenden vorvertraglichen oder vertraglichen Beziehungen.', 'Soweit Sie uns über E-Mail, Soziale Medien, Telefon, Fax, Post, unser Kontaktformular oder sonstwie ansprechen und uns hierbei personenbezogene Daten wie Ihren Namen, Ihre Telefonnummer oder Ihre E-Mail-Adresse zur Verfügung stellen oder weitere Angaben zur Ihrer Person oder Ihrem Anliegen machen, verarbeiten wir diese Daten zur Beantwortung Ihrer Anfrage im Rahmen des zwischen uns bestehenden vorvertraglichen oder vertraglichen Beziehungen.',
contactDataLabels: {
affectedData: 'Betroffene Daten:',
affectedPersons: 'Betroffene Personen:',
processingPurpose: 'Verarbeitungszweck:',
legalBasis: 'Rechtsgrundlage:',
},
contactDataCategories: { contactDataCategories: {
affectedData: [ affectedData: [
'Bestandsdaten (bspw. Namen, Adressen)', 'Bestandsdaten (bspw. Namen, Adressen)',
@ -134,6 +141,30 @@ export const imprint = {
legalBasis: legalBasis:
'Vertragserfüllung und vorvertragliche Anfragen, Art. 6 Abs. 1 lit. b DSGVO, berechtigtes Interesse, Art. 6 Abs. 1 lit. f DSGVO', 'Vertragserfüllung und vorvertragliche Anfragen, Art. 6 Abs. 1 lit. b DSGVO, berechtigtes Interesse, Art. 6 Abs. 1 lit. f DSGVO',
}, },
onlineAppointmentTitle: 'Online-Terminbuchung (Nextcloud)',
onlineAppointmentIntro:
'Auf unserer Website bieten wir die Möglichkeit, über einen externen Link Termine für Beratungsgespräche oder Meetings zu buchen. Dafür nutzen wir den Terminplaner-Dienst der Open-Source-Software Nextcloud, die auf unseren Servern bei unserem Webhosting-Provider betrieben wird.',
onlineAppointmentProvider:
'Anbieter der Infrastruktur: Die Server, auf denen die Nextcloud-Instanz läuft, werden bereitgestellt von der Bitpalast GmbH, Postfach 19 15 64, D-14005 Berlin, Deutschland. Verantwortlich für die Datenverarbeitung im Rahmen der Terminbuchung bleibt jedoch allein Sascha Bach (siehe Abschnitt „Wer bei uns für die Datenverarbeitung verantwortlich ist").',
onlineAppointmentDataTitle:
'Erhobene Daten: Bei der Buchung eines Termins werden folgende personenbezogene Daten von Ihnen eingegeben und verarbeitet:',
onlineAppointmentData: [
'Name (Vor- und Nachname)',
'E-Mail-Adresse',
'Ggf. Telefonnummer (sofern im Buchungsformular als optionales Feld hinterlegt)',
'Ggf. Kommentare oder Anmerkungen zum Termininhalt',
'Der gewählte Terminzeitraum',
],
onlineAppointmentPurpose:
'Zweck der Verarbeitung: Die Daten werden ausschließlich zum Zweck der Terminvereinbarung, der automatischen Terminbestätigung per E-Mail und der Erinnerung an den vereinbarten Termin verarbeitet. Ohne Bereitstellung dieser Daten ist die Nutzung der Buchungsfunktion nicht möglich.',
onlineAppointmentLegalBasis:
'Rechtsgrundlage: Die Verarbeitung der Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO (Erfüllung vorvertraglicher Maßnahmen). Die Buchung eines Termins stellt eine konkrete Anfrage zur Anbahnung oder Durchführung eines Vertragsverhältnisses dar.',
onlineAppointmentRetention:
'Speicherdauer: Die Daten werden nach Durchführung des Termins und Ablauf etwaiger gesetzlicher Aufbewahrungsfristen (z. B. aus dem Steuerrecht: 6 Jahre für Geschäftskorrespondenz, 10 Jahre für Buchungsbelege) gelöscht. Sofern kein Vertragsverhältnis zustande kommt, werden die Daten spätestens nach Ablauf von 3 Monaten gelöscht, es sei denn, Sie haben in eine längere Speicherung eingewilligt.',
onlineAppointmentThirdParty:
'Weitergabe: Eine Weitergabe der Daten an Dritte erfolgt nicht, außer dies ist zur Erfüllung des Termins gesetzlich erforderlich oder Sie haben hierzu ausdrücklich eingewilligt. Die Daten verbleiben auf Servern innerhalb der Europäischen Union (Deutschland).',
onlineAppointmentServerLocation:
'Serverstandort: Die Daten werden auf Servern in Deutschland gespeichert.',
securityTitle: 'Sicherheitsmaßnahmen', securityTitle: 'Sicherheitsmaßnahmen',
securityText: securityText:
'Wir treffen im Übrigen technische und organisatorische Sicherheitsmaßnahmen nach dem Stand der Technik, um die Vorschriften der Datenschutzgesetze einzuhalten und Ihre Daten gegen zufällige oder vorsätzliche Manipulationen, teilweisen oder vollständigen Verlust, Zerstörung oder gegen den unbefugten Zugriff Dritter zu schützen.', 'Wir treffen im Übrigen technische und organisatorische Sicherheitsmaßnahmen nach dem Stand der Technik, um die Vorschriften der Datenschutzgesetze einzuhalten und Ihre Daten gegen zufällige oder vorsätzliche Manipulationen, teilweisen oder vollständigen Verlust, Zerstörung oder gegen den unbefugten Zugriff Dritter zu schützen.',

View File

@ -174,6 +174,8 @@ export interface TextConfig {
socialAnnouncement: string; socialAnnouncement: string;
codebergLinkText: string; codebergLinkText: string;
linkedinLinkText: string; linkedinLinkText: string;
appointmentHintText: string;
appointmentLinkText: string;
}; };
// Imprint Page // Imprint Page
@ -227,15 +229,32 @@ export interface TextConfig {
name: string; name: string;
address: string; address: string;
website: string; website: string;
privacyPolicyLabel: string;
}; };
contactTitle: string; contactTitle: string;
contactText: string; contactText: string;
contactDataLabels: {
affectedData: string;
affectedPersons: string;
processingPurpose: string;
legalBasis: string;
};
contactDataCategories: { contactDataCategories: {
affectedData: string[]; affectedData: string[];
affectedPersons: string; affectedPersons: string;
processingPurpose: string; processingPurpose: string;
legalBasis: string; legalBasis: string;
}; };
onlineAppointmentTitle: string;
onlineAppointmentIntro: string;
onlineAppointmentProvider: string;
onlineAppointmentDataTitle: string;
onlineAppointmentData: string[];
onlineAppointmentPurpose: string;
onlineAppointmentLegalBasis: string;
onlineAppointmentRetention: string;
onlineAppointmentThirdParty: string;
onlineAppointmentServerLocation: string;
securityTitle: string; securityTitle: string;
securityText: string; securityText: string;
changesTitle: string; changesTitle: string;

View File

@ -20,4 +20,6 @@ export const contact = {
socialAnnouncement: 'Opening {platform} profile in new tab', socialAnnouncement: 'Opening {platform} profile in new tab',
codebergLinkText: 'Sascha Bach on Codeberg', codebergLinkText: 'Sascha Bach on Codeberg',
linkedinLinkText: 'Sascha Bach on LinkedIn', linkedinLinkText: 'Sascha Bach on LinkedIn',
appointmentHintText: 'Prefer to schedule a call?',
appointmentLinkText: 'Book an appointment',
}; };

View File

@ -1,152 +1,183 @@
export const imprint = { export const imprint = {
title: 'Impressum und Datenschutz', title: 'Imprint and Privacy Policy',
subtitle: 'Legal Information', subtitle: 'Legal Information',
companyInfoTitle: 'Angaben gemäß § 5 DDG', companyInfoTitle: 'Information pursuant to § 5 DDG',
contactTitle: 'Kontakt', contactTitle: 'Contact',
responsibilityTitle: 'Vertretungsberechtigt', responsibilityTitle: 'Authorized Representative',
disclaimerTitle: 'Haftungsausschluss', disclaimerTitle: 'Disclaimer',
contentLiabilityTitle: 'Haftung für Inhalte', contentLiabilityTitle: 'Liability for Content',
contentLiabilityText: [ contentLiabilityText: [
'Als Diensteanbieter sind wir gemäß § 7 Abs.1 DDG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht unter der Verpflichtung, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.', 'As a service provider, we are responsible for our own content on these pages in accordance with general laws pursuant to § 7 para. 1 DDG. According to §§ 8 to 10 TMG, however, we as a service provider are not obligated to monitor transmitted or stored third-party information or to investigate circumstances that indicate illegal activity.',
'Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.', 'Obligations to remove or block the use of information under general laws remain unaffected. However, liability in this regard only arises from the moment of knowledge of a specific legal violation. Upon becoming aware of such infringements, we will remove this content immediately.',
], ],
copyrightTitle: 'Urheberrecht', copyrightTitle: 'Copyright',
copyrightText: [ copyrightText: [
'Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.', 'The content and works created by the website operators on these pages are subject to German copyright law. The reproduction, editing, distribution, and any kind of exploitation outside the limits of copyright law require the written consent of the respective author or creator.',
'Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.', 'Downloads and copies of this website are only permitted for private and non-commercial use. Where the content on this website was not created by the operator, the copyrights of third parties are respected. Third-party content is identified as such. Should you nevertheless notice a copyright infringement, please notify us accordingly. Upon becoming aware of infringements, we will remove such content immediately.',
], ],
privacyTitle: 'Datenschutz', privacyTitle: 'Privacy',
privacyText: privacyText:
'Die Nutzung unserer Webseite ist in der Regel ohne Angabe personenbezogener Daten möglich. Soweit auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder E-Mail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis. Diese Daten werden ohne Ihre ausdrückliche Zustimmung nicht an Dritte weitergegeben.', 'The use of our website is generally possible without providing personal data. To the extent that personal data (such as name, address, or email addresses) is collected on our website, this is done, wherever possible, on a voluntary basis. This data will not be passed on to third parties without your explicit consent.',
detailedPrivacyPolicy: { detailedPrivacyPolicy: {
title: 'Datenschutzerklärung', title: 'Privacy Policy',
introduction: introduction:
'Mit dieser Datenschutzerklärung möchten wir Sie über Art, Umfang und Zweck der Verarbeitung von personenbezogenen Daten (im Folgenden auch nur als "Daten" bezeichnet) aufklären. Personenbezogene Daten sind alle Daten, die einen persönlichen Bezug zu Ihnen aufweisen, z. B. Name, Adresse, E-Mail-Adresse oder Ihr Nutzerverhalten. Die Datenschutzerklärung gilt für alle von uns vorgenommene Daten-Verarbeitungsvorgänge sowohl im Rahmen unserer Kerntätigkeit als auch für die von uns vorgehaltenen Online-Medien.', 'With this privacy policy, we would like to inform you about the nature, scope, and purpose of the processing of personal data (hereinafter simply referred to as "data"). Personal data is all data that has a personal reference to you, such as name, address, email address, or your user behavior. This privacy policy applies to all data processing operations carried out by us, both within the scope of our core activities and for the online media we maintain.',
responsibleTitle: responsibleTitle:
'Wer bei uns für die Datenverarbeitung verantwortlich ist', 'Who is responsible for data processing at our company',
responsibleText: 'Verantwortlich für die Datenverarbeitung ist:', responsibleText: 'Responsible for data processing is:',
responsibleContact: responsibleContact:
'Sascha Bach\nAlt-Tempelhof 15\n12099 Berlin\nDE\n01799364867\ninfo@sascha-bach.de', 'Sascha Bach\nAlt-Tempelhof 15\n12099 Berlin\nDE\n01799364867\ninfo@sascha-bach.de',
dataProtectionOfficerTitle: dataProtectionOfficerTitle:
'Kontaktdaten unseres Datenschutzbeauftragten', 'Contact details of our data protection officer',
dataProtectionOfficerText: dataProtectionOfficerText:
'Unseren Datenschutzbeauftragten können Sie per E-Mail unter info [at] sascha-bach.de oder unter unserer Postadresse mit dem Zusatz „an den Datenschutzbeauftragten" erreichen.', 'You can contact our data protection officer by email at info [at] sascha-bach.de or at our postal address with the addition "to the Data Protection Officer".',
rightsTitle: 'Ihre Rechte nach der DSGVO', rightsTitle: 'Your rights under the GDPR',
rightsIntro: rightsIntro:
'Nach der DSGVO stehen Ihnen die nachfolgend aufgeführten Rechte zu, die Sie jederzeit bei dem in Ziffer 1. dieser Datenschutzerklärung genannten Verantwortlichen geltend machen können:', 'Under the GDPR, you have the following rights, which you may assert at any time against the controller named in section 1 of this privacy policy:',
rights: [ rights: [
{ {
title: 'Recht auf Auskunft', title: 'Right to information',
text: 'Sie haben das Recht, von uns Auskunft darüber zu verlangen, ob und welche Daten wir von Ihnen verarbeiten.', text: 'You have the right to request information from us about whether and what data we process about you.',
}, },
{ {
title: 'Recht auf Berichtigung', title: 'Right to rectification',
text: 'Sie haben das Recht, die Berichtigung unrichtiger oder Vervollständigung unvollständiger Daten zu verlangen.', text: 'You have the right to request the correction of inaccurate data or the completion of incomplete data.',
}, },
{ {
title: 'Recht auf Löschung', title: 'Right to erasure',
text: 'Sie haben das Recht, die Löschung Ihrer Daten zu verlangen.', text: 'You have the right to request the deletion of your data.',
}, },
{ {
title: 'Recht auf Einschränkung', title: 'Right to restriction',
text: 'Sie haben in bestimmten Fällen das Recht zu verlangen, dass wir Ihre Daten nur noch eingeschränkt bearbeiten.', text: 'In certain cases, you have the right to request that we only process your data in a restricted manner.',
}, },
{ {
title: 'Recht auf Datenübertragbarkeit', title: 'Right to data portability',
text: 'Sie haben das Recht zu verlangen, dass wir Ihnen oder einem anderen Verantwortlichen Ihre Daten in einem strukturierten, gängigen und maschinenlesebaren Format übermitteln.', text: 'You have the right to request that we transmit your data to you or to another controller in a structured, commonly used, and machine-readable format.',
}, },
{ {
title: 'Beschwerderecht', title: 'Right to lodge a complaint',
text: 'Sie haben das Recht, sich bei einer Aufsichtsbehörde zu beschweren. Zuständig ist die Aufsichtsbehörde Ihres üblichen Aufenthaltsortes, Ihres Arbeitsplatzes oder unseres Firmensitzes.', text: 'You have the right to lodge a complaint with a supervisory authority. The supervisory authority of your usual place of residence, your place of work, or our company\'s registered office is responsible.',
}, },
], ],
revocationTitle: 'Widerrufsrecht', revocationTitle: 'Right of withdrawal',
revocationText: revocationText:
'Sie haben das Recht, die von Ihnen erteilte Einwilligung zur Datenverarbeitung jederzeit zu widerrufen.', 'You have the right to withdraw the consent you have given for data processing at any time.',
objectionTitle: 'Widerspruchsrecht', objectionTitle: 'Right to object',
objectionText: objectionText:
'Sie haben das Recht, jederzeit gegen die Verarbeitung Ihrer Daten, die wir auf unser berechtigtes Interesse nach Art. 6 Abs. 1 lit. f DSGVO stützen, Widerspruch einzulegen. Sofern Sie von Ihrem Widerspruchsrecht Gebrauch machen, bitten wir Sie um die Darlegung der Gründe. Wir werden Ihre personenbezogenen Daten dann nicht mehr verarbeiten, es sei denn, wir können Ihnen gegenüber nachweisen, dass zwingende schutzwürdige Gründe an der Datenverarbeitung Ihre Interessen und Rechte überwiegen.', 'You have the right to object at any time to the processing of your data that we base on our legitimate interest pursuant to Art. 6(1)(f) GDPR. If you exercise your right to object, please provide your reasons. We will then no longer process your personal data unless we can demonstrate compelling legitimate grounds for the processing that override your interests and rights.',
objectionHighlight: objectionHighlight:
'Unabhängig vom vorstehend Gesagten, haben Sie das jederzeitige Recht, der Verarbeitung Ihrer personenbezogenen Daten für Zwecke der Werbung und Datenanalyse zu widersprechen.', 'Regardless of the foregoing, you have the right at any time to object to the processing of your personal data for the purposes of advertising and data analysis.',
objectionContact: objectionContact:
'Ihren Widerspruch richten Sie bitte an die oben angegebene Kontaktadresse des Verantwortlichen.', 'Please direct your objection to the contact address of the controller provided above.',
dataDeletionTitle: 'Wann löschen wir Ihre Daten?', dataDeletionTitle: 'When do we delete your data?',
dataDeletionIntro: dataDeletionIntro:
'Wir löschen Ihre Daten dann, wenn wir diese nicht mehr brauchen oder Sie uns dies vorgeben. Das bedeutet, dass - sofern sich aus den einzelnen Datenschutzhinweisen dieser Datenschutzerklärung nichts anderes ergibt - wir Ihre Daten löschen,', 'We delete your data when we no longer need it or when you instruct us to do so. This means that unless otherwise stated in the individual privacy notices in this privacy policy we will delete your data,',
dataDeletionReasons: [ dataDeletionReasons: [
'wenn der Zweck der Datenverarbeitung weggefallen ist und damit die jeweilige in den einzelnen Datenschutzhinweisen genannte Rechtsgrundlage nicht mehr besteht, also bspw.', 'when the purpose of data processing has ceased to exist and thus the respective legal basis mentioned in the individual privacy notices no longer applies, e.g.',
'nach Beendigung der zwischen uns bestehenden vertraglichen oder mitgliedschaftlichen Beziehungen (Art. 6 Abs. 1 lit. a DSGVO) oder', 'after termination of the contractual or membership relationships existing between us (Art. 6(1)(a) GDPR), or',
'nach Wegfall unseres berechtigten Interesses an der weiteren Verarbeitung oder Speicherung Ihrer Daten (Art. 6 Abs. 1 lit. f DSGVO),', 'after the cessation of our legitimate interest in further processing or storage of your data (Art. 6(1)(f) GDPR),',
'wenn Sie von Ihrem Widerrufsrecht Gebrauch machen und keine anderweitige gesetzliche Rechtsgrundlage für die Verarbeitung im Sinne von Art. 6 Abs. 1 lit. b-f DSGVO eingreift,', 'when you exercise your right of withdrawal and no other statutory legal basis for the processing pursuant to Art. 6(1)(b)(f) GDPR applies,',
'wenn Sie vom Ihrem Widerspruchsrecht Gebrauch machen und der Löschung keine zwingenden schutzwürdigen Gründe entgegenstehen.', 'when you exercise your right to object and no compelling legitimate grounds for deletion can be opposed.',
], ],
retentionText: retentionText:
'Sofern wir (bestimmte Teile) Ihre(r) Daten jedoch noch für andere Zwecke vorhalten müssen, weil dies etwa steuerliche Aufbewahrungsfristen (in der Regel 6 Jahre für Geschäftskorrespondenz bzw. 10 Jahre für Buchungsbelege) oder die Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen aus vertraglichen Beziehungen (bis zu vier Jahren) erforderlich machen oder die Daten zum Schutz der Rechte einer anderen natürlichen oder juristischen Person gebraucht werden, löschen wir (den Teil) Ihre(r) Daten erst nach Ablauf dieser Fristen. Bis zum Ablauf dieser Fristen beschränken wir die Verarbeitung dieser Daten jedoch auf diese Zwecke (Erfüllung der Aufbewahrungspflichten).', 'However, if we (certain parts of) your data still need to be retained for other purposes, such as statutory retention periods (generally 6 years for business correspondence or 10 years for accounting records), the assertion, exercise, or defense of legal claims arising from contractual relationships (up to four years), or if the data is needed to protect the rights of another natural or legal person, we will only delete (the respective part of) your data after the expiry of these periods. Until these periods expire, we will restrict the processing of this data to these purposes (fulfillment of retention obligations).',
webhostingTitle: 'Webhosting', webhostingTitle: 'Web Hosting',
webhostingText: webhostingText:
'Wir bedienen uns zum Vorhalten unserer Internetseiten eines Anbieters, auf dessen Server unsere Internetseiten gespeichert und für den Abruf im Internet verfügbar gemacht werden (Hosting). Hierbei können von dem Anbieter all diejenigen über den von Ihnen genutzten Browser übertragenen Daten verarbeitet werden, die bei der Nutzung unserer Internetseiten anfallen. Hierzu gehören insbesondere Ihre IP-Adresse, die der Anbieter benötigt, um unser Online-Angebot an den von Ihnen genutzten Browser ausliefern zu können sowie sämtliche von Ihnen über unsere Internetseite getätigten Eingaben. Daneben kann der von uns genutzte Anbieter', 'We use a provider to host our websites, on whose servers our websites are stored and made available for retrieval on the internet (hosting). In doing so, the provider may process all data transmitted by you via the browser during use of our websites. This includes in particular your IP address, which the provider needs to deliver our online offering to the browser you are using, as well as all inputs made by you on our website. In addition, the provider we use may collect',
webhostingDataTypes: [ webhostingDataTypes: [
'das Datum und die Uhrzeit des Zugriffs auf unsere Internetseite', 'the date and time of access to our website',
'Zeitzonendifferenz zur Greenwich Mean Time (GMT)', 'time zone difference from Greenwich Mean Time (GMT)',
'Zugriffsstatus (HTTP-Status)', 'access status (HTTP status)',
'die übertragene Datenmenge', 'the amount of data transferred',
'der Internet-Service-Provider des zugreifenden Systems', 'the internet service provider of the accessing system',
'der von Ihnen verwendete Browsertyp und dessen Version', 'the browser type and version you are using',
'das von Ihnen verwendete Betriebssystem', 'the operating system you are using',
'die Internetseite, von welcher Sie gegebenenfalls auf unsere Internetseite gelangt sind', 'the website from which you may have reached our website',
'die Seiten bzw. Unterseiten, welche Sie auf unserer Internetseite besuchen.', 'the pages or sub-pages you visit on our website.',
], ],
webhostingPurpose: webhostingPurpose:
'erheben. Die vorgenannten Daten werden als Logfiles auf den Servern unseres Anbieters gespeichert. Dies ist erforderlich, um die Stabilität und Sicherheit des Betriebs unserer Internetseite zu gewährleisten.', 'The aforementioned data is stored as log files on our provider\'s servers. This is necessary to ensure the stability and security of our website\'s operation.',
webhostingDataCategories: { webhostingDataCategories: {
affectedData: 'Betroffene Daten:', affectedData: 'Data affected:',
affectedDataList: [ affectedDataList: [
'Inhaltsdaten (bspw. Posts, Fotos, Videos)', 'Content data (e.g. posts, photos, videos)',
'Nutzungsdaten (bspw. Zugriffszeiten, angeklickte Webseiten)', 'Usage data (e.g. access times, visited websites)',
'Kommunikationsdaten (bspw. Informationen über das genutzte Gerät, IP-Adresse)', 'Communication data (e.g. information about the device used, IP address)',
], ],
affectedPersons: 'Betroffene Personen: Nutzer unserer Internetpräsenz', affectedPersons: 'Affected persons: Users of our online presence',
processingPurpose: processingPurpose:
'Verarbeitungszweck: Ausspielen unserer Internetseiten, Gewährleistung des Betriebs unserer Internetseiten', 'Processing purpose: Provision of our website, ensuring the operation of our website',
legalBasis: legalBasis:
'Rechtsgrundlage: Berechtigtes Interesse, Art. 6 Abs. 1 lit. f DSGVO', 'Legal basis: Legitimate interest, Art. 6(1)(f) GDPR',
provider: 'Von uns beauftragte(r) Webhoster:', provider: 'Web host commissioned by us:',
}, },
hostingProvider: { hostingProvider: {
name: 'Bitpalast GmbH', name: 'Bitpalast GmbH',
address: 'Postfach 19 15 64, D-14005 Berlin, Deutschland', address: 'Postfach 19 15 64, D-14005 Berlin, Germany',
website: 'https://preiswerter-webserver-de.bitpalast.net/', website: 'https://preiswerter-webserver-de.bitpalast.net/',
privacyPolicyLabel: 'Privacy Policy:',
}, },
contactTitle: 'Kontaktaufnahme', contactTitle: 'Contact',
contactText: contactText:
'Soweit Sie uns über E-Mail, Soziale Medien, Telefon, Fax, Post, unser Kontaktformular oder sonstwie ansprechen und uns hierbei personenbezogene Daten wie Ihren Namen, Ihre Telefonnummer oder Ihre E-Mail-Adresse zur Verfügung stellen oder weitere Angaben zur Ihrer Person oder Ihrem Anliegen machen, verarbeiten wir diese Daten zur Beantwortung Ihrer Anfrage im Rahmen des zwischen uns bestehenden vorvertraglichen oder vertraglichen Beziehungen.', 'To the extent that you contact us via email, social media, telephone, fax, post, our contact form, or otherwise, and in doing so provide us with personal data such as your name, phone number, or email address, or make further statements about yourself or your request, we will process this data to respond to your inquiry within the scope of the pre-contractual or contractual relationship existing between us.',
contactDataLabels: {
affectedData: 'Data affected:',
affectedPersons: 'Persons affected:',
processingPurpose: 'Processing purpose:',
legalBasis: 'Legal basis:',
},
contactDataCategories: { contactDataCategories: {
affectedData: [ affectedData: [
'Bestandsdaten (bspw. Namen, Adressen)', 'Master data (e.g. names, addresses)',
'Kontakdaten (bspw. E-Mail-Adresse, Telefonnummer, Postanschrift)', 'Contact data (e.g. email address, phone number, postal address)',
'Inhaltsdaten (Texte, Fotos, Videos)', 'Content data (texts, photos, videos)',
'Vertragsdaten (bspw. Vertragsgegenstand, Vertragsdauer)', 'Contract data (e.g. subject matter of contract, duration of contract)',
], ],
affectedPersons: affectedPersons:
'Interessenten, Kunden, Geschäfts- und Vertragspartner', 'Prospective customers, clients, business and contractual partners',
processingPurpose: processingPurpose:
'Kommunikation sowie Beantwortung von Kontaktanfragen, Büro und Organisationsverfahren', 'Communication and responding to contact requests, office and organizational procedures',
legalBasis: legalBasis:
'Vertragserfüllung und vorvertragliche Anfragen, Art. 6 Abs. 1 lit. b DSGVO, berechtigtes Interesse, Art. 6 Abs. 1 lit. f DSGVO', 'Contract fulfillment and pre-contractual inquiries, Art. 6(1)(b) GDPR, legitimate interest, Art. 6(1)(f) GDPR',
}, },
securityTitle: 'Sicherheitsmaßnahmen', onlineAppointmentTitle: 'Online Appointment Booking (Nextcloud)',
onlineAppointmentIntro:
'On our website, we offer the option to book appointments for consultations or meetings via an external link. For this purpose, we use the appointment scheduler service of the open-source software Nextcloud, which is operated on our servers at our web hosting provider.',
onlineAppointmentProvider:
'Infrastructure provider: The servers on which the Nextcloud instance runs are provided by Bitpalast GmbH, Postfach 19 15 64, D-14005 Berlin, Germany. However, the sole responsibility for data processing in connection with appointment booking remains with Sascha Bach (see section "Who is responsible for data processing at our company").',
onlineAppointmentDataTitle:
'Data collected: When booking an appointment, the following personal data is entered and processed by you:',
onlineAppointmentData: [
'Name (first and last name)',
'Email address',
'Phone number if applicable (if included as an optional field in the booking form)',
'Comments or notes about the appointment content if applicable',
'The selected appointment time slot',
],
onlineAppointmentPurpose:
'Purpose of processing: The data is processed exclusively for the purpose of scheduling the appointment, sending automatic appointment confirmations by email, and reminding you of the scheduled appointment. Without providing this data, it is not possible to use the booking function.',
onlineAppointmentLegalBasis:
'Legal basis: The processing of data is based on Art. 6(1)(b) GDPR (performance of pre-contractual measures). Booking an appointment constitutes a specific request to initiate or carry out a contractual relationship.',
onlineAppointmentRetention:
'Retention period: Data will be deleted after the appointment has taken place and any applicable statutory retention periods have expired (e.g., under tax law: 6 years for business correspondence, 10 years for accounting records). If no contractual relationship is established, the data will be deleted no later than 3 months after the appointment, unless you have consented to longer storage.',
onlineAppointmentThirdParty:
'Disclosure: Data will not be disclosed to third parties unless this is legally required for the fulfillment of the appointment or you have explicitly consented to such disclosure. The data remains on servers within the European Union (Germany).',
onlineAppointmentServerLocation:
'Server location: The data is stored on servers in Germany.',
securityTitle: 'Security measures',
securityText: securityText:
'Wir treffen im Übrigen technische und organisatorische Sicherheitsmaßnahmen nach dem Stand der Technik, um die Vorschriften der Datenschutzgesetze einzuhalten und Ihre Daten gegen zufällige oder vorsätzliche Manipulationen, teilweisen oder vollständigen Verlust, Zerstörung oder gegen den unbefugten Zugriff Dritter zu schützen.', 'We also take technical and organizational security measures in accordance with the state of the art to comply with data protection laws and to protect your data against accidental or intentional manipulation, partial or complete loss, destruction, or unauthorized access by third parties.',
changesTitle: 'Aktualität und Änderung dieser Datenschutzerklärung', changesTitle: 'Currency and amendments to this privacy policy',
changesText: changesText:
'Diese Datenschutzerklärung ist aktuell gültig und hat den Stand September 2025. Aufgrund geänderter gesetzlicher bzw. behördlicher Vorgaben kann es notwendig werden, diese Datenschutzerklärung anzupassen.', 'This privacy policy is currently valid and was last updated in September 2025. Due to changes in legislation or official requirements, it may become necessary to amend this privacy policy.',
disclaimer: disclaimer:
'Diese Datenschutzerklärung wurde mit Hilfe des Datenschutz-Generators von SOS Recht erstellt. SOS Recht ist ein Angebot der Mueller.legal Rechtsanwälte Partnerschaft mit Sitz in Berlin.', 'This privacy policy was created with the help of the data protection generator by SOS Recht. SOS Recht is an offering of Mueller.legal Attorneys at Law, based in Berlin.',
}, },
address: { address: {
street: 'Alt-Tempelhof 15', street: 'Alt-Tempelhof 15',
city: '12099 Berlin', city: '12099 Berlin',
country: 'Deutschland', country: 'Germany',
}, },
contact: { contact: {
phone: '+49 (0) 179 9364867', phone: '+49 (0) 179 9364867',

View File

@ -10,6 +10,9 @@ export const personalConfig = {
full: 'freelancer [at] sascha-bach.de', full: 'freelancer [at] sascha-bach.de',
}, },
// Booking
appointmentUrl: 'https://nextcloud.sascha-bach.de/index.php/apps/calendar/appointment/N58i246RSTB2',
// Social Links // Social Links
social: { social: {
codeberg: { codeberg: {

56
src/hooks/usePageSeo.ts Normal file
View File

@ -0,0 +1,56 @@
import { useEffect } from 'react';
interface SeoProps {
title: string;
description: string;
canonical?: string;
noIndex?: boolean;
}
const BASE_URL = 'https://sascha-bach.de';
export function usePageSeo({ title, description, canonical, noIndex }: SeoProps) {
useEffect(() => {
document.title = title;
const setMeta = (name: string, content: string, isProperty = false) => {
const attr = isProperty ? 'property' : 'name';
let el = document.querySelector<HTMLMetaElement>(`meta[${attr}="${name}"]`);
if (!el) {
el = document.createElement('meta');
el.setAttribute(attr, name);
document.head.appendChild(el);
}
el.content = content;
};
setMeta('description', description);
setMeta('og:title', title, true);
setMeta('og:description', description, true);
setMeta('twitter:title', title);
setMeta('twitter:description', description);
if (canonical) {
setMeta('og:url', `${BASE_URL}${canonical}`, true);
let link = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');
if (!link) {
link = document.createElement('link');
link.rel = 'canonical';
document.head.appendChild(link);
}
link.href = `${BASE_URL}${canonical}`;
}
if (noIndex) {
setMeta('robots', 'noindex, nofollow');
}
return () => {
if (noIndex) {
const robotsMeta = document.querySelector('meta[name="robots"]');
robotsMeta?.remove();
}
};
}, [title, description, canonical, noIndex]);
}

View File

@ -5,8 +5,15 @@ import SkillsSection from '../components/sections/SkillsSection';
import CertificationsSection from '../components/sections/CertificationsSection'; import CertificationsSection from '../components/sections/CertificationsSection';
import ProjectsSection from '../components/sections/ProjectsSection'; import ProjectsSection from '../components/sections/ProjectsSection';
import ContactSection from '../components/sections/ContactSection'; import ContactSection from '../components/sections/ContactSection';
import { usePageSeo } from '../hooks/usePageSeo';
export default function HomePage() { export default function HomePage() {
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',
});
return ( return (
<> <>
<HeroSection /> <HeroSection />

View File

@ -1,26 +1,17 @@
import { useEffect } from 'react';
import { personalConfig } from '../config/personal'; import { personalConfig } from '../config/personal';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { usePageSeo } from '../hooks/usePageSeo';
export default function ImprintPage() { export default function ImprintPage() {
const { texts: allTexts } = useLanguage(); const { texts: allTexts } = useLanguage();
const texts = allTexts.imprint; const texts = allTexts.imprint;
useEffect(() => { usePageSeo({
// Prevent indexing of this page title: 'Imprint | Sascha Bach',
const metaRobots = document.createElement('meta'); description: 'Legal information and imprint for Sascha Bach, freelance software developer.',
metaRobots.name = 'robots'; canonical: '/imprint',
metaRobots.content = 'noindex, nofollow'; noIndex: true,
document.head.appendChild(metaRobots); });
// Cleanup on unmount
return () => {
const existingMeta = document.querySelector('meta[name="robots"]');
if (existingMeta) {
existingMeta.remove();
}
};
}, []);
return ( return (
<section className="imprint-page"> <section className="imprint-page">
@ -179,7 +170,7 @@ export default function ImprintPage() {
<p><strong>{texts.detailedPrivacyPolicy.hostingProvider.name}</strong></p> <p><strong>{texts.detailedPrivacyPolicy.hostingProvider.name}</strong></p>
<p>{texts.detailedPrivacyPolicy.hostingProvider.address}</p> <p>{texts.detailedPrivacyPolicy.hostingProvider.address}</p>
<p> <p>
Datenschutzerklärung: {' '} {texts.detailedPrivacyPolicy.hostingProvider.privacyPolicyLabel}{' '}
<a <a
href={texts.detailedPrivacyPolicy.hostingProvider.website} href={texts.detailedPrivacyPolicy.hostingProvider.website}
target="_blank" target="_blank"
@ -198,18 +189,36 @@ export default function ImprintPage() {
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.contactText}</p> <p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.contactText}</p>
<div className="imprint-page__data-categories"> <div className="imprint-page__data-categories">
<p className="imprint-page__category-title"><strong>Betroffene Daten:</strong></p> <p className="imprint-page__category-title"><strong>{texts.detailedPrivacyPolicy.contactDataLabels.affectedData}</strong></p>
<ul className="imprint-page__category-list"> <ul className="imprint-page__category-list">
{texts.detailedPrivacyPolicy.contactDataCategories.affectedData.map((item: string) => ( {texts.detailedPrivacyPolicy.contactDataCategories.affectedData.map((item: string) => (
<li key={item}>{item}</li> <li key={item}>{item}</li>
))} ))}
</ul> </ul>
<p><strong>Betroffene Personen: </strong>{texts.detailedPrivacyPolicy.contactDataCategories.affectedPersons}</p> <p><strong>{texts.detailedPrivacyPolicy.contactDataLabels.affectedPersons} </strong>{texts.detailedPrivacyPolicy.contactDataCategories.affectedPersons}</p>
<p><strong>Verarbeitungszweck: </strong>{texts.detailedPrivacyPolicy.contactDataCategories.processingPurpose}</p> <p><strong>{texts.detailedPrivacyPolicy.contactDataLabels.processingPurpose} </strong>{texts.detailedPrivacyPolicy.contactDataCategories.processingPurpose}</p>
<p><strong>Rechtsgrundlage: </strong>{texts.detailedPrivacyPolicy.contactDataCategories.legalBasis}</p> <p><strong>{texts.detailedPrivacyPolicy.contactDataLabels.legalBasis} </strong>{texts.detailedPrivacyPolicy.contactDataCategories.legalBasis}</p>
</div> </div>
</div> </div>
{/* Online Appointment Booking (Nextcloud) */}
<div className="imprint-page__privacy-section">
<h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.onlineAppointmentTitle}</h3>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.onlineAppointmentIntro}</p>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.onlineAppointmentProvider}</p>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.onlineAppointmentDataTitle}</p>
<ul className="imprint-page__category-list">
{texts.detailedPrivacyPolicy.onlineAppointmentData.map((item: string) => (
<li key={item}>{item}</li>
))}
</ul>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.onlineAppointmentPurpose}</p>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.onlineAppointmentLegalBasis}</p>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.onlineAppointmentRetention}</p>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.onlineAppointmentThirdParty}</p>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.onlineAppointmentServerLocation}</p>
</div>
{/* Security Measures */} {/* Security Measures */}
<div className="imprint-page__privacy-section"> <div className="imprint-page__privacy-section">
<h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.securityTitle}</h3> <h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.securityTitle}</h3>

View File

@ -6,8 +6,15 @@ import ProcessesSection from '../components/landing/ProcessesSection';
import ProjectsSection from '../components/sections/ProjectsSection'; import ProjectsSection from '../components/sections/ProjectsSection';
import ReferencesSection from '../components/landing/ReferencesSection'; import ReferencesSection from '../components/landing/ReferencesSection';
import ContactSection from '../components/sections/ContactSection'; import ContactSection from '../components/sections/ContactSection';
import { usePageSeo } from '../hooks/usePageSeo';
export default function LandingPage() { export default function LandingPage() {
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: '/',
});
return ( return (
<> <>
<LandingAboutMeSection /> <LandingAboutMeSection />

View File

@ -1,25 +1,16 @@
import { useEffect } from 'react';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { usePageSeo } from '../hooks/usePageSeo';
export default function PrivacyPolicy() { export default function PrivacyPolicy() {
const { texts: allTexts } = useLanguage(); const { texts: allTexts } = useLanguage();
const texts = allTexts.privacyPolicy; const texts = allTexts.privacyPolicy;
useEffect(() => { usePageSeo({
// Prevent indexing of this page title: 'Privacy Policy | Sascha Bach',
const metaRobots = document.createElement('meta'); description: 'Privacy policy for the website of Sascha Bach, freelance software developer.',
metaRobots.name = 'robots'; canonical: '/privacy-policy',
metaRobots.content = 'noindex, nofollow'; noIndex: true,
document.head.appendChild(metaRobots); });
// Cleanup on unmount
return () => {
const existingMeta = document.querySelector('meta[name="robots"]');
if (existingMeta) {
existingMeta.remove();
}
};
}, []);
return ( return (
<div className="privacy-policy" style={{ maxWidth: '800px', margin: '0 auto', padding: '2rem' }}> <div className="privacy-policy" style={{ maxWidth: '800px', margin: '0 auto', padding: '2rem' }}>

View File

@ -171,11 +171,4 @@
font-size: 1rem; font-size: 1rem;
} }
} }
// Reduce card height on larger screens
@media (min-width: 1200px) {
&__card {
height: 80%;
}
}
} }

View File

@ -108,7 +108,51 @@
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 1rem; font-size: 1rem;
line-height: 1.6; line-height: 1.6;
margin-bottom: 2rem; margin-bottom: 1.5rem;
}
&__appointment-hint {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.75rem 1rem;
margin-bottom: 1.5rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 0.75rem;
font-size: 0.9rem;
}
&__appointment-icon {
width: 1.125rem;
height: 1.125rem;
color: var(--color-primary);
flex-shrink: 0;
}
&__appointment-text {
color: var(--color-text-muted);
}
&__appointment-link {
color: var(--color-primary);
font-weight: 500;
text-decoration: underline;
text-decoration-color: transparent;
text-underline-offset: 3px;
transition: text-decoration-color var(--transition-fast);
white-space: nowrap;
&:hover {
text-decoration-color: var(--color-primary);
}
&:focus {
outline: 2px solid var(--color-focus-ring, #2563eb);
outline-offset: 2px;
border-radius: 2px;
}
} }
// Social Card - positioned below connect card // Social Card - positioned below connect card

View File

@ -137,7 +137,6 @@
line-height: 1.7; line-height: 1.7;
color: var(--color-text-muted); color: var(--color-text-muted);
margin-bottom: 1rem; margin-bottom: 1rem;
text-align: justify;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;