Added homepage and moved old homepage to aboutme page

This commit is contained in:
becarta
2025-03-03 23:23:14 +01:00
parent 2e4284cba3
commit 9c61657071
16 changed files with 1769 additions and 533 deletions

View File

@@ -0,0 +1,183 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Language Persistence Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1 {
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.test-section {
margin-bottom: 30px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
.test-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.language-status {
margin-top: 15px;
padding: 10px;
background-color: #e9f7ef;
border-radius: 4px;
}
.navigation-links {
margin-top: 20px;
}
.navigation-links a {
display: inline-block;
margin-right: 15px;
padding: 8px 16px;
background-color: #2196F3;
color: white;
text-decoration: none;
border-radius: 4px;
}
.navigation-links a:hover {
background-color: #0b7dda;
}
</style>
</head>
<body>
<h1>Language Persistence Test</h1>
<div class="test-section">
<h2>Current Language Status</h2>
<div class="language-status">
<p><strong>URL Language:</strong> <span id="url-language">Checking...</span></p>
<p><strong>LocalStorage Language:</strong> <span id="localstorage-language">Checking...</span></p>
<p><strong>Cookie Language:</strong> <span id="cookie-language">Checking...</span></p>
</div>
</div>
<div class="test-section">
<h2>Test Language Selection</h2>
<p>Click on a language to test the language persistence:</p>
<div class="test-buttons">
<button onclick="changeLanguage('en')">English</button>
<button onclick="changeLanguage('nl')">Dutch</button>
<button onclick="changeLanguage('de')">German</button>
<button onclick="changeLanguage('fr')">French</button>
</div>
</div>
<div class="test-section">
<h2>Navigation Test</h2>
<p>Use these links to test language persistence during navigation:</p>
<div class="navigation-links">
<a href="/" id="home-link">Home</a>
<a href="/aboutme" id="aboutme-link">About Me</a>
</div>
</div>
<script>
// Function to get language from URL
function getLanguageFromURL() {
const pathSegments = window.location.pathname.split('/').filter(Boolean);
const supportedLanguages = ['en', 'nl', 'de', 'fr'];
if (pathSegments.length > 0 && supportedLanguages.includes(pathSegments[0])) {
return pathSegments[0];
}
return null;
}
// Function to get language from localStorage
function getStoredLanguage() {
return localStorage.getItem('preferredLanguage');
}
// Function to get language from cookie
function getLanguageFromCookie() {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.startsWith('preferredLanguage=')) {
return cookie.substring('preferredLanguage='.length);
}
}
return null;
}
// Function to update the language status display
function updateLanguageStatus() {
document.getElementById('url-language').textContent = getLanguageFromURL() || 'Not set';
document.getElementById('localstorage-language').textContent = getStoredLanguage() || 'Not set';
document.getElementById('cookie-language').textContent = getLanguageFromCookie() || 'Not set';
}
// Function to change language
function changeLanguage(langCode) {
// Store language in localStorage
localStorage.setItem('preferredLanguage', langCode);
// Store language in cookie
const expirationDate = new Date();
expirationDate.setFullYear(expirationDate.getFullYear() + 1);
document.cookie = `preferredLanguage=${langCode}; expires=${expirationDate.toUTCString()}; path=/; SameSite=Lax`;
// Update the language status display
updateLanguageStatus();
// Update navigation links with the selected language
updateNavigationLinks(langCode);
// Dispatch a custom event for language change
const event = new CustomEvent('languageChanged', {
detail: {
langCode,
previousLangCode: getLanguageFromURL() || 'en',
willReload: false
}
});
document.dispatchEvent(event);
// Show a success message
alert(`Language changed to ${langCode}. Navigation links have been updated.`);
}
// Function to update navigation links with the selected language
function updateNavigationLinks(langCode) {
const homeLink = document.getElementById('home-link');
const aboutmeLink = document.getElementById('aboutme-link');
homeLink.href = `/${langCode}/`;
aboutmeLink.href = `/${langCode}/aboutme`;
}
// Initialize the page
document.addEventListener('DOMContentLoaded', function() {
// Update the language status display
updateLanguageStatus();
// Update navigation links with the current language
const currentLang = getStoredLanguage() || getLanguageFromCookie() || 'en';
updateNavigationLinks(currentLang);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Language Switching Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1 {
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.test-section {
margin-bottom: 30px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
.test-button {
display: inline-block;
margin: 5px;
padding: 8px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
.test-button:hover {
background-color: #45a049;
}
.language-button {
display: inline-block;
margin: 5px;
padding: 8px 15px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
.language-button:hover {
background-color: #0b7dda;
}
.result {
margin-top: 10px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
}
code {
background-color: #f1f1f1;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
</style>
</head>
<body>
<h1>Language Switching Test</h1>
<div class="test-section">
<h2>Current URL Information</h2>
<div class="result" id="url-info">Loading...</div>
</div>
<div class="test-section">
<h2>Switch Language</h2>
<p>Click on a language to switch:</p>
<div>
<a href="#" class="language-button" data-lang="en">English</a>
<a href="#" class="language-button" data-lang="nl">Dutch</a>
<a href="#" class="language-button" data-lang="de">German</a>
<a href="#" class="language-button" data-lang="fr">French</a>
</div>
</div>
<div class="test-section">
<h2>Test Hash Navigation</h2>
<p>Click on a section to navigate:</p>
<div>
<a href="#services" class="test-button">Services</a>
<a href="#contact" class="test-button">Contact</a>
<a href="#about" class="test-button">About</a>
</div>
</div>
<div class="test-section">
<h2>Test Page Navigation</h2>
<p>Navigate to different pages:</p>
<div>
<a href="/" class="test-button">Home</a>
<a href="/aboutme" class="test-button">About Me</a>
</div>
</div>
<script>
// Display current URL information
function updateUrlInfo() {
const url = new URL(window.location.href);
const pathSegments = url.pathname.split('/').filter(Boolean);
const currentLang = pathSegments[0] || 'none';
const infoDiv = document.getElementById('url-info');
infoDiv.innerHTML = `
<p><strong>Full URL:</strong> ${url.href}</p>
<p><strong>Path:</strong> ${url.pathname}</p>
<p><strong>Hash:</strong> ${url.hash || 'none'}</p>
<p><strong>Current Language:</strong> ${currentLang}</p>
<p><strong>Path Segments:</strong> ${JSON.stringify(pathSegments)}</p>
`;
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
updateUrlInfo();
// Set up language buttons
document.querySelectorAll('.language-button').forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const lang = button.getAttribute('data-lang');
if (!lang) return;
// Get current URL information
const currentUrl = new URL(window.location.href);
const currentPath = currentUrl.pathname.replace(/\/$/, '');
const currentHash = currentUrl.hash;
const pathSegments = currentPath.split('/').filter(Boolean);
// Check if we're on a language-specific path
const supportedLanguages = ['en', 'nl', 'de', 'fr'];
const isLangPath = supportedLanguages.includes(pathSegments[0]);
// Extract the page path without language
let pagePath = '';
if (isLangPath && pathSegments.length > 1) {
// If we're on a language-specific path, get everything after the language code
pagePath = `/${pathSegments.slice(1).join('/')}`;
} else if (!isLangPath && pathSegments.length > 0) {
// If we're not on a language-specific path, use the current path
pagePath = `/${pathSegments.join('/')}`;
}
// Handle special case for root path
const isRootPath = pathSegments.length === 0 || (isLangPath && pathSegments.length === 1);
// Construct the new URL
let newUrl = isRootPath ? `/${lang}` : `/${lang}${pagePath}`;
// Append hash fragment if it exists
if (currentHash) {
newUrl += currentHash;
}
// Navigate to the new URL
window.location.href = newUrl;
});
});
// Update URL info when hash changes
window.addEventListener('hashchange', updateUrlInfo);
});
</script>
</body>
</html>

View File

@@ -1,5 +1,6 @@
--- ---
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import { supportedLanguages } from '~/i18n/translations';
interface Props { interface Props {
currentLang: string; currentLang: string;
@@ -7,8 +8,6 @@ interface Props {
const { currentLang } = Astro.props; const { currentLang } = Astro.props;
import { supportedLanguages } from '~/i18n/translations';
type SupportedLanguage = typeof supportedLanguages[number]; type SupportedLanguage = typeof supportedLanguages[number];
const languages = [ const languages = [
@@ -121,13 +120,13 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
} }
</style> </style>
<script> <script define:vars={{ supportedLanguages }}>
function setupLanguageDropdown() { function setupLanguageDropdown() {
const button = document.querySelector<HTMLButtonElement>('#menu-button'); const button = document.querySelector('#menu-button');
const menu = document.querySelector<HTMLDivElement>('#language-menu'); const menu = document.querySelector('#language-menu');
const chevronIcon = document.querySelector<HTMLElement>('#chevron-icon'); const chevronIcon = document.querySelector('#chevron-icon');
const selectedLanguageText = document.querySelector<HTMLElement>('#selected-language'); const selectedLanguageText = document.querySelector('#selected-language');
const languageButtons = document.querySelectorAll<HTMLButtonElement>('[data-lang-code]'); const languageButtons = document.querySelectorAll('[data-lang-code]');
if (!button || !menu || !chevronIcon || !selectedLanguageText) { if (!button || !menu || !chevronIcon || !selectedLanguageText) {
return; return;
@@ -184,7 +183,7 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
closeMenu(); closeMenu();
// Toggle menu // Toggle menu
button.addEventListener('click', (e: MouseEvent) => { button.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
if (isOpen) { if (isOpen) {
closeMenu(); closeMenu();
@@ -193,7 +192,7 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
// Focus the first menu item for better keyboard navigation // Focus the first menu item for better keyboard navigation
const firstMenuItem = menu.querySelector('button[role="menuitem"]'); const firstMenuItem = menu.querySelector('button[role="menuitem"]');
if (firstMenuItem) { if (firstMenuItem) {
(firstMenuItem as HTMLElement).focus(); firstMenuItem.focus();
} }
} }
}); });
@@ -205,7 +204,7 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
if (!langCode) return; if (!langCode) return;
// Update button text and icon // Update button text and icon
const langName = langButton.textContent?.trim(); const langName = langButton.textContent ? langButton.textContent.trim() : '';
const flagIcon = langButton.querySelector('svg'); const flagIcon = langButton.querySelector('svg');
if (langName && flagIcon) { if (langName && flagIcon) {
selectedLanguageText.textContent = langName; selectedLanguageText.textContent = langName;
@@ -218,27 +217,84 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
// Close menu // Close menu
closeMenu(); closeMenu();
// Get current path and redirect // Get current URL information
const currentPath = window.location.pathname.replace(/\/$/, ''); const currentUrl = new URL(window.location.href);
const currentPath = currentUrl.pathname.replace(/\/$/, '');
const currentHash = currentUrl.hash;
const pathSegments = currentPath.split('/').filter(Boolean); const pathSegments = currentPath.split('/').filter(Boolean);
const isBlogPost = pathSegments.length > 0 && !['en', 'nl', 'de'].includes(pathSegments[0]);
const pathWithoutLang = pathSegments.length > 1 ? `/${pathSegments.slice(1).join('/')}`.replace(/\/$/, '') : '';
// Redirect to new language path // Check if we're on a language-specific path
window.location.href = isBlogPost ? `/${langCode}` : `/${langCode}${pathWithoutLang || ''}`; const isLangPath = supportedLanguages.includes(pathSegments[0]);
// Get the previous language code
const previousLangCode = isLangPath ? pathSegments[0] : 'en';
// Extract the page path without language
let pagePath = '';
if (isLangPath && pathSegments.length > 1) {
// If we're on a language-specific path, get everything after the language code
pagePath = `/${pathSegments.slice(1).join('/')}`;
} else if (!isLangPath && pathSegments.length > 0) {
// If we're not on a language-specific path, use the current path
pagePath = `/${pathSegments.join('/')}`;
}
// Handle special case for root path
const isRootPath = pathSegments.length === 0 || (isLangPath && pathSegments.length === 1);
// Construct the new URL
let newUrl = isRootPath ? `/${langCode}` : `/${langCode}${pagePath}`;
// Clean up any potential double slashes
newUrl = newUrl.replace(/\/+/g, '/');
// Append hash fragment if it exists
if (currentHash) {
newUrl += currentHash;
}
// Store the language preference in localStorage and cookies
if (window.languageUtils) {
window.languageUtils.storeLanguagePreference(langCode);
} else {
// Fallback if languageUtils is not available
localStorage.setItem('preferredLanguage', langCode);
// Also set a cookie for server-side detection
const expirationDate = new Date();
expirationDate.setFullYear(expirationDate.getFullYear() + 1);
document.cookie = `preferredLanguage=${langCode}; expires=${expirationDate.toUTCString()}; path=/; SameSite=Lax`;
}
// Dispatch the language changed event
const reloadEvent = new CustomEvent('languageChanged', {
detail: {
langCode,
previousLangCode,
path: newUrl,
willReload: true
}
});
document.dispatchEvent(reloadEvent);
// Construct the full URL
const newFullUrl = `${window.location.origin}${newUrl}`;
// Reload the page to ensure all content is updated to the new language
window.location.href = newFullUrl;
}); });
}); });
// Close when clicking outside // Close when clicking outside
document.addEventListener('click', (e: MouseEvent) => { document.addEventListener('click', (e) => {
const target = e.target as HTMLElement; const target = e.target;
if (isOpen && !menu.contains(target) && !button.contains(target)) { if (isOpen && !menu.contains(target) && !button.contains(target)) {
closeMenu(); closeMenu();
} }
}); });
// Handle keyboard navigation // Handle keyboard navigation
document.addEventListener('keydown', (e: KeyboardEvent) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isOpen) { if (e.key === 'Escape' && isOpen) {
closeMenu(); closeMenu();
button.focus(); button.focus();
@@ -257,7 +313,7 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
newIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1; newIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1;
} }
(menuItems[newIndex] as HTMLElement).focus(); menuItems[newIndex].focus();
} }
}); });
} }
@@ -271,4 +327,10 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
// Re-run setup when the page content is updated (e.g., after navigation) // Re-run setup when the page content is updated (e.g., after navigation)
document.addEventListener('astro:page-load', setupLanguageDropdown); document.addEventListener('astro:page-load', setupLanguageDropdown);
// Listen for popstate events (browser back/forward buttons)
window.addEventListener('popstate', (_event) => {
// No need to manually update anything here as the browser will
// automatically load the correct URL, and Astro will handle the rendering
});
</script> </script>

View File

@@ -0,0 +1,112 @@
---
// This component handles the synchronization between localStorage and cookies
// for language persistence across page loads and navigation
// Extend Window interface to include languageUtils
declare global {
interface Window {
languageUtils?: {
getStoredLanguage: () => string | null;
storeLanguagePreference: (langCode: string) => void;
};
}
}
---
<script>
function setupLanguagePersistence() {
// Function to get language from localStorage
function getStoredLanguage() {
return localStorage.getItem('preferredLanguage');
}
// Function to set a cookie with the language preference
function setLanguageCookie(langCode) {
// Set cookie with a long expiration (1 year)
const expirationDate = new Date();
expirationDate.setFullYear(expirationDate.getFullYear() + 1);
document.cookie = `preferredLanguage=${langCode}; expires=${expirationDate.toUTCString()}; path=/; SameSite=Lax`;
}
// Function to get language from cookie
function getLanguageFromCookie() {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.startsWith('preferredLanguage=')) {
return cookie.substring('preferredLanguage='.length);
}
}
return null;
}
// Function to get language from URL
function getLanguageFromURL() {
const pathSegments = window.location.pathname.split('/').filter(Boolean);
const supportedLanguages = ['en', 'nl', 'de', 'fr'];
if (pathSegments.length > 0 && supportedLanguages.includes(pathSegments[0])) {
return pathSegments[0];
}
return null;
}
// On page load, sync language between URL, localStorage and cookie
const urlLanguage = getLanguageFromURL();
const storedLanguage = getStoredLanguage();
const cookieLanguage = getLanguageFromCookie();
// URL language takes precedence if it exists
if (urlLanguage) {
// If URL has a language, make sure localStorage and cookie match it
if (!storedLanguage || storedLanguage !== urlLanguage) {
localStorage.setItem('preferredLanguage', urlLanguage);
}
if (!cookieLanguage || cookieLanguage !== urlLanguage) {
setLanguageCookie(urlLanguage);
}
} else if (storedLanguage && !cookieLanguage) {
// If language is in localStorage but not in cookie, update cookie
setLanguageCookie(storedLanguage);
} else if (!storedLanguage && cookieLanguage) {
// If language is in cookie but not in localStorage, update localStorage
localStorage.setItem('preferredLanguage', cookieLanguage);
} else if (storedLanguage && cookieLanguage && storedLanguage !== cookieLanguage) {
// If both exist but are different, prefer localStorage
setLanguageCookie(storedLanguage);
}
// Listen for language changes and update the cookie
document.addEventListener('languageChanged', (event: CustomEvent) => {
if (event.detail && event.detail.langCode) {
setLanguageCookie(event.detail.langCode);
localStorage.setItem('preferredLanguage', event.detail.langCode);
}
});
// When localStorage changes (e.g., from another tab), update the cookie
window.addEventListener('storage', (event) => {
if (event.key === 'preferredLanguage' && event.newValue) {
setLanguageCookie(event.newValue);
}
});
// Make language utility functions available globally
window.languageUtils = {
getStoredLanguage: getStoredLanguage,
storeLanguagePreference: (langCode) => {
localStorage.setItem('preferredLanguage', langCode);
setLanguageCookie(langCode);
}
};
}
// Run setup when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupLanguagePersistence);
} else {
setupLanguagePersistence();
}
// Re-run setup when the page content is updated (e.g., after navigation)
document.addEventListener('astro:page-load', setupLanguagePersistence);
</script>

View File

@@ -159,6 +159,205 @@ import { UI } from 'astrowind:config';
onLoad(); onLoad();
onPageShow(); onPageShow();
}); });
// Handle smooth scrolling for anchor links across all pages
function setupSmoothScrolling() {
// Handle links that start with # (pure anchor links)
document.querySelectorAll('a[href^="#"]:not([href="#"])').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
window.scrollTo({
top: targetElement.offsetTop - 50, // Offset for header
behavior: 'smooth'
});
}
});
});
// Handle links that contain # but don't start with it (page + anchor)
document.querySelectorAll('a[href*="#"]:not([href^="#"])').forEach(anchor => {
anchor.addEventListener('click', function (e) {
const href = this.getAttribute('href');
const isHashLink = this.getAttribute('data-hash-link') === 'true';
// Check if this is a link to the current page
// First, extract the path part (before the hash)
const hrefPath = href.split('#')[0];
const currentPath = window.location.pathname;
// Consider it's the current page if:
// 1. The path matches exactly
// 2. The href is just a hash (like '/#services')
// 3. The href path is '/' and we're on the homepage
const isCurrentPage =
currentPath === hrefPath ||
hrefPath === '' ||
(hrefPath === '/' && (currentPath === '/' || currentPath.endsWith('/index.html')));
// For hash links, we want to update the URL and scroll to the element
if (isHashLink || isCurrentPage) {
e.preventDefault();
const hashIndex = href.indexOf('#');
if (hashIndex !== -1) {
const targetId = href.substring(hashIndex + 1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
// Update the URL with the hash fragment
history.pushState(null, null, href);
window.scrollTo({
top: targetElement.offsetTop - 50, // Offset for header
behavior: 'smooth'
});
} else {
// If the target element doesn't exist on the current page, navigate to the page
window.location.href = href;
}
} else {
// If there's no hash fragment, just navigate to the page
window.location.href = href;
}
}
});
});
}
// Handle language changes and hash navigation
function setupLanguageNavigation() {
// Initialize language preference from localStorage or default to 'en'
function getStoredLanguage() {
return localStorage.getItem('preferredLanguage') || 'en';
}
// Store language preference in localStorage
function storeLanguagePreference(langCode) {
localStorage.setItem('preferredLanguage', langCode);
console.log('Language preference stored:', langCode);
}
// Function to update URLs with the current language
function updateUrlsWithLanguage() {
const currentLang = getStoredLanguage();
const supportedLanguages = ['en', 'nl', 'de', 'fr'];
// Update all internal links to include the language prefix
document.querySelectorAll('a[href^="/"]:not([href^="//"])').forEach(link => {
const href = link.getAttribute('href');
if (!href) return;
// Skip hash-only links (e.g., "#services")
if (href.startsWith('#')) {
return;
}
// Extract hash fragment if present
let hashFragment = '';
let pathWithoutHash = href;
if (href.includes('#')) {
const parts = href.split('#');
pathWithoutHash = parts[0];
hashFragment = '#' + parts[1];
}
// Parse the URL path (without hash) to check for existing language code
const pathSegments = pathWithoutHash.split('/').filter(Boolean);
const hasLanguagePrefix = pathSegments.length > 0 && supportedLanguages.includes(pathSegments[0]);
// If it already has a language prefix but it's different from the current language,
// update it to the current language
if (hasLanguagePrefix && pathSegments[0] !== currentLang) {
// Replace the existing language prefix with the current one
pathSegments[0] = currentLang;
const newPath = '/' + pathSegments.join('/');
// Set the new href with the hash fragment (if any)
link.setAttribute('href', newPath + hashFragment);
return;
}
// If it doesn't have a language prefix, add the current language
if (!hasLanguagePrefix) {
// Create the new path with the language prefix
const newPath = pathWithoutHash === '/' ?
`/${currentLang}` :
`/${currentLang}${pathWithoutHash}`;
// Set the new href with the hash fragment (if any)
link.setAttribute('href', newPath + hashFragment);
}
});
}
// Listen for the custom languageChanged event
document.addEventListener('languageChanged', (event) => {
// Store the selected language in localStorage
if (event.detail && event.detail.langCode) {
storeLanguagePreference(event.detail.langCode);
console.log('Language changed:', event.detail);
// Always update all internal links with the new language
// regardless of whether we're doing a full page reload
updateUrlsWithLanguage();
}
});
// Process links when the page loads
updateUrlsWithLanguage();
// Process links after client-side navigation
document.addEventListener('astro:page-load', () => {
// Short delay to ensure DOM is fully updated
setTimeout(updateUrlsWithLanguage, 0);
});
// Also update links when the DOM content is loaded
document.addEventListener('DOMContentLoaded', updateUrlsWithLanguage);
// Check for hash in URL on page load and scroll to it
function scrollToHashOnLoad() {
if (window.location.hash) {
const targetId = window.location.hash.substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
// Use setTimeout to ensure the page has fully loaded
setTimeout(() => {
window.scrollTo({
top: targetElement.offsetTop - 50, // Offset for header
behavior: 'smooth'
});
}, 100);
}
}
}
scrollToHashOnLoad();
// Make language functions available globally
window.languageUtils = {
getStoredLanguage,
storeLanguagePreference
};
}
document.addEventListener('DOMContentLoaded', () => {
setupSmoothScrolling();
setupLanguageNavigation();
});
// Re-attach event listeners after page transitions
document.addEventListener('astro:after-swap', () => {
setupSmoothScrolling();
setupLanguageNavigation();
});
</script> </script>
<script is:inline> <script is:inline>

View File

@@ -2,6 +2,7 @@
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import { SITE } from 'astrowind:config'; import { SITE } from 'astrowind:config';
import { getHomePermalink } from '~/utils/permalinks'; import { getHomePermalink } from '~/utils/permalinks';
import { getFooterData } from '~/navigation';
interface Link { interface Link {
text?: string; text?: string;
@@ -17,13 +18,50 @@ interface Links {
export interface Props { export interface Props {
links?: Array<Links>; links?: Array<Links>;
secondaryLinks: Array<Link>; secondaryLinks?: Array<Link>;
socialLinks: Array<Link>; socialLinks?: Array<Link>;
footNote?: string; footNote?: string;
theme?: string; theme?: string;
} }
const { socialLinks = [], theme = 'light' } = Astro.props; import { supportedLanguages } from '~/i18n/translations';
// Define the type for supported languages
type SupportedLanguage = typeof supportedLanguages[number];
// Get current language from URL
const currentPath = `/${Astro.url.pathname.replace(/^\/+|\/+$/g, '')}`;
const pathSegments = currentPath.split('/').filter(Boolean);
// Check for language in URL path
let currentLang = pathSegments[0] && supportedLanguages.includes(pathSegments[0] as SupportedLanguage)
? pathSegments[0] as SupportedLanguage
: null;
// If no language in URL, check cookies
if (!currentLang) {
const cookies = Astro.request.headers.get('cookie') || '';
const cookieLanguage = cookies.split(';')
.map(cookie => cookie.trim())
.find(cookie => cookie.startsWith('preferredLanguage='))
?.split('=')[1];
if (cookieLanguage && supportedLanguages.includes(cookieLanguage as SupportedLanguage)) {
currentLang = cookieLanguage as SupportedLanguage;
} else {
// Default to English if no language is found
currentLang = 'en';
}
}
// Get translated footer data
const footerData = getFooterData(currentLang);
const {
secondaryLinks = footerData.secondaryLinks,
socialLinks = footerData.socialLinks,
theme = 'light'
} = Astro.props;
--- ---
<footer class:list={[{ dark: theme === 'dark' }, 'relative border-t border-gray-200 dark:border-slate-800 not-prose']}> <footer class:list={[{ dark: theme === 'dark' }, 'relative border-t border-gray-200 dark:border-slate-800 not-prose']}>
@@ -38,13 +76,17 @@ const { socialLinks = [], theme = 'light' } = Astro.props;
<!-- Site Title with Terms & Privacy Links --> <!-- Site Title with Terms & Privacy Links -->
<div class="flex flex-col items-start space-y-2"> <div class="flex flex-col items-start space-y-2">
<!-- Site Title --> <!-- Site Title -->
<a class="inline-block font-bold text-xl" href={getHomePermalink()}> <a class="inline-block font-bold text-xl" href={getHomePermalink(currentLang)}>
{SITE?.name} {SITE?.name}
</a> </a>
<!-- Terms & Privacy Policy Links --> <!-- Terms & Privacy Policy Links -->
<div class="flex items-center space-x-4 text-sm text-muted"> <div class="flex items-center space-x-4 text-sm text-muted">
{secondaryLinks.map(({ text, href }) => (
<a class="hover:text-gray-700 dark:hover:text-gray-200 transition duration-150 ease-in-out" href={href}>
{text}
</a>
))}
</div> </div>
</div> </div>

View File

@@ -8,6 +8,7 @@ import LanguageDropdown from '~/components/LanguageDropdown.astro';
import { getHomePermalink } from '~/utils/permalinks'; import { getHomePermalink } from '~/utils/permalinks';
import { trimSlash, getAsset } from '~/utils/permalinks'; import { trimSlash, getAsset } from '~/utils/permalinks';
import { getHeaderData } from '~/navigation';
interface Link { interface Link {
text?: string; text?: string;
@@ -18,6 +19,7 @@ interface Link {
interface MenuLink extends Link { interface MenuLink extends Link {
links?: Array<MenuLink>; links?: Array<MenuLink>;
isHashLink?: boolean;
} }
export interface Props { export interface Props {
@@ -31,9 +33,42 @@ export interface Props {
position?: string; position?: string;
} }
import { supportedLanguages } from '~/i18n/translations';
// Get current language from URL
const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
const pathSegments = currentPath.split('/').filter(Boolean);
// Define the type for supported languages
type SupportedLanguage = typeof supportedLanguages[number];
// Check for language in URL path
let currentLang = pathSegments[0] && supportedLanguages.includes(pathSegments[0] as SupportedLanguage)
? pathSegments[0] as SupportedLanguage
: null;
// If no language in URL, check cookies
if (!currentLang) {
const cookies = Astro.request.headers.get('cookie') || '';
const cookieLanguage = cookies.split(';')
.map(cookie => cookie.trim())
.find(cookie => cookie.startsWith('preferredLanguage='))
?.split('=')[1];
if (cookieLanguage && supportedLanguages.includes(cookieLanguage as SupportedLanguage)) {
currentLang = cookieLanguage as SupportedLanguage;
} else {
// Default to English if no language is found
currentLang = 'en';
}
}
// Get translated header data
const headerData = getHeaderData(currentLang);
const { const {
id = 'header', id = 'header',
links = [], links = headerData.links,
isSticky = false, isSticky = false,
isDark = false, isDark = false,
isFullWidth = false, isFullWidth = false,
@@ -41,8 +76,6 @@ const {
showRssFeed = false, showRssFeed = false,
position = 'center', position = 'center',
} = Astro.props; } = Astro.props;
const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
--- ---
<header <header
@@ -107,7 +140,7 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
<Icon name="tabler:chevron-down" class="w-3.5 h-3.5 ml-0.5 rtl:ml-0 rtl:mr-0.5 hidden md:inline" /> <Icon name="tabler:chevron-down" class="w-3.5 h-3.5 ml-0.5 rtl:ml-0 rtl:mr-0.5 hidden md:inline" />
</button> </button>
<ul class="dropdown-menu md:backdrop-blur-md dark:md:bg-dark rounded md:absolute pl-4 md:pl-0 md:hidden font-medium md:bg-white/90 md:min-w-[200px] drop-shadow-xl"> <ul class="dropdown-menu md:backdrop-blur-md dark:md:bg-dark rounded md:absolute pl-4 md:pl-0 md:hidden font-medium md:bg-white/90 md:min-w-[200px] drop-shadow-xl">
{links.map(({ text: text2, href: href2 }) => ( {links.map(({ text: text2, href: href2, isHashLink }) => (
<li> <li>
<a <a
class:list={[ class:list={[
@@ -115,6 +148,7 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
{ 'aw-link-active': href2 === currentPath }, { 'aw-link-active': href2 === currentPath },
]} ]}
href={href2} href={href2}
data-hash-link={isHashLink ? 'true' : undefined}
> >
{text2} {text2}
</a> </a>
@@ -129,6 +163,7 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
{ 'aw-link-active': href === currentPath }, { 'aw-link-active': href === currentPath },
]} ]}
href={href} href={href}
data-hash-link={href?.includes('#') ? 'true' : undefined}
> >
{text} {text}
</a> </a>

View File

@@ -64,6 +64,57 @@ export interface Translation {
title: string; title: string;
information: string; information: string;
}; };
homepage: {
actions: {
learnMore: string;
contactMe: string;
};
services: {
tagline: string;
title: string;
subtitle: string;
items: {
title: string;
description: string;
}[];
};
approach: {
tagline: string;
title: string;
missionTitle: string;
missionContent: string[];
items: {
title: string;
description: string;
}[];
};
testimonials: {
tagline: string;
title: string;
items: {
testimonial: string;
name: string;
description: string;
}[];
};
callToAction: {
title: string;
subtitle: string;
button: string;
};
contact: {
title: string;
subtitle: string;
nameLabel: string;
namePlaceholder: string;
emailLabel: string;
emailPlaceholder: string;
messageLabel: string;
messagePlaceholder: string;
disclaimer: string;
description: string;
};
};
} }
export const supportedLanguages = ['en', 'nl', 'de', 'fr'] as const; export const supportedLanguages = ['en', 'nl', 'de', 'fr'] as const;
@@ -76,7 +127,7 @@ export const translations: Record<string, Translation> = {
en: { en: {
metadata: { metadata: {
title: 'About me', title: 'About me',
aboutUs: 'About us', aboutUs: 'About me',
}, },
navigation: { navigation: {
home: 'Home', home: 'Home',
@@ -100,6 +151,104 @@ export const translations: Record<string, Translation> = {
'I hold certifications in Microsoft Teams Administration, Azure Fundamentals, and Nexthink Administration. My mission is to drive IT excellence by optimizing cloud solutions, automating processes, and providing outstanding technical support.' 'I hold certifications in Microsoft Teams Administration, Azure Fundamentals, and Nexthink Administration. My mission is to drive IT excellence by optimizing cloud solutions, automating processes, and providing outstanding technical support.'
], ],
}, },
homepage: {
actions: {
learnMore: 'Learn More',
contactMe: 'Contact Me',
},
services: {
tagline: 'Services',
title: 'How I Can Help Your Organization',
subtitle: 'I offer a range of specialized IT services to help businesses optimize their operations and digital infrastructure.',
items: [
{
title: 'Workflow Automation',
description: 'Streamline your business processes with Power Automate solutions that reduce manual effort and increase operational efficiency.',
},
{
title: 'Intelligent Chatbots',
description: 'Develop smart chatbots in Copilot Studio that enhance user interactions through natural language processing and automated responses.',
},
{
title: 'API Integrations',
description: 'Create seamless connections between your applications and services with custom API integrations for efficient data exchange.',
},
{
title: 'Microsoft 365 Management',
description: 'Optimize your Microsoft 365 environment with expert administration, security configurations, and service optimization.',
},
{
title: 'SharePoint Solutions',
description: 'Set up, manage, and optimize SharePoint Online and on-premise deployments for effective document management and collaboration.',
},
{
title: 'IT Infrastructure Oversight',
description: 'Manage global IT infrastructures, including servers, networks, and end-user devices to ensure reliable operations.',
},
],
},
approach: {
tagline: 'About My Approach',
title: 'Driving IT Excellence Through Innovation',
missionTitle: 'Mission Statement',
missionContent: [
'My mission is to drive IT excellence by optimizing cloud solutions, automating processes, and providing outstanding technical support. I believe in leveraging technology to solve real business challenges and create value through innovation.',
'With over 15 years of IT experience, I bring a wealth of knowledge in Microsoft technologies, automation tools, and system integration to help organizations transform their digital capabilities and achieve their strategic goals.'
],
items: [
{
title: 'User-Centric Solutions',
description: 'I focus on creating solutions that enhance user experience and productivity, ensuring technology serves people effectively.',
},
{
title: 'Continuous Improvement',
description: 'I stay current with emerging technologies and best practices to deliver cutting-edge solutions that evolve with your needs.',
},
{
title: 'Strategic Implementation',
description: 'I approach each project strategically, aligning technical solutions with business objectives for maximum impact.',
},
],
},
testimonials: {
tagline: 'Testimonials',
title: 'What Clients Say About My Work',
items: [
{
testimonial: 'Richard\'s expertise in Power Automate transformed our workflow processes, saving us countless hours and reducing errors significantly.',
name: 'Client Name',
description: 'Position, Company',
},
{
testimonial: 'The SharePoint implementation Richard delivered has revolutionized our document management and team collaboration capabilities.',
name: 'Client Name',
description: 'Position, Company',
},
{
testimonial: 'Richard\'s technical knowledge combined with his ability to understand our business needs resulted in solutions that truly addressed our challenges.',
name: 'Client Name',
description: 'Position, Company',
},
],
},
callToAction: {
title: 'Ready to optimize your IT systems?',
subtitle: 'Let\'s discuss how I can help your organization streamline processes, enhance collaboration, and drive digital transformation.',
button: 'Contact Me',
},
contact: {
title: 'Get in Touch',
subtitle: 'Have a project in mind or questions about my services? Reach out and let\'s start a conversation.',
nameLabel: 'Name',
namePlaceholder: 'Your name',
emailLabel: 'Email',
emailPlaceholder: 'Your email address',
messageLabel: 'Message',
messagePlaceholder: 'Your message',
disclaimer: 'By submitting this form, you agree to our privacy policy and allow us to use your information to contact you about our services.',
description: 'I\'ll respond to your message as soon as possible. You can also connect with me on LinkedIn or GitHub.',
},
},
resume: { resume: {
title: 'Work experience', title: 'Work experience',
experience: [ experience: [
@@ -317,7 +466,7 @@ export const translations: Record<string, Translation> = {
nl: { nl: {
metadata: { metadata: {
title: 'Over mij', title: 'Over mij',
aboutUs: 'Over ons', aboutUs: 'Over mij',
}, },
navigation: { navigation: {
home: 'Home', home: 'Home',
@@ -341,6 +490,104 @@ export const translations: Record<string, Translation> = {
'Ik ben gecertificeerd in Microsoft Teams Administration, Azure Fundamentals en Nexthink Administration. Mijn missie is IT-excellentie te bevorderen door het optimaliseren van cloudoplossingen, het automatiseren van processen en het leveren van uitstekende technische ondersteuning.' 'Ik ben gecertificeerd in Microsoft Teams Administration, Azure Fundamentals en Nexthink Administration. Mijn missie is IT-excellentie te bevorderen door het optimaliseren van cloudoplossingen, het automatiseren van processen en het leveren van uitstekende technische ondersteuning.'
], ],
}, },
homepage: {
actions: {
learnMore: 'Meer informatie',
contactMe: 'Neem contact op',
},
services: {
tagline: 'Diensten',
title: 'Hoe ik uw organisatie kan helpen',
subtitle: 'Ik bied een reeks gespecialiseerde IT-diensten om bedrijven te helpen hun activiteiten en digitale infrastructuur te optimaliseren.',
items: [
{
title: 'Workflow Automatisering',
description: 'Stroomlijn uw bedrijfsprocessen met Power Automate-oplossingen die handmatige inspanning verminderen en de operationele efficiëntie verhogen.',
},
{
title: 'Intelligente Chatbots',
description: 'Ontwikkel slimme chatbots in Copilot Studio die gebruikersinteracties verbeteren door natuurlijke taalverwerking en geautomatiseerde antwoorden.',
},
{
title: 'API-integraties',
description: 'Creëer naadloze verbindingen tussen uw applicaties en diensten met aangepaste API-integraties voor efficiënte gegevensuitwisseling.',
},
{
title: 'Microsoft 365 Beheer',
description: 'Optimaliseer uw Microsoft 365-omgeving met deskundig beheer, beveiligingsconfiguraties en service-optimalisatie.',
},
{
title: 'SharePoint Oplossingen',
description: 'Opzetten, beheren en optimaliseren van SharePoint Online en on-premise implementaties voor effectief documentbeheer en samenwerking.',
},
{
title: 'IT-infrastructuur Toezicht',
description: 'Beheer van wereldwijde IT-infrastructuren, inclusief servers, netwerken en eindgebruikersapparaten om betrouwbare operaties te garanderen.',
},
],
},
approach: {
tagline: 'Over Mijn Aanpak',
title: 'IT-excellentie stimuleren door innovatie',
missionTitle: 'Missie',
missionContent: [
'Mijn missie is om IT-excellentie te stimuleren door cloudoplossingen te optimaliseren, processen te automatiseren en uitstekende technische ondersteuning te bieden. Ik geloof in het benutten van technologie om echte zakelijke uitdagingen op te lossen en waarde te creëren door innovatie.',
'Met meer dan 15 jaar IT-ervaring breng ik een schat aan kennis in Microsoft-technologieën, automatiseringstools en systeemintegratie om organisaties te helpen hun digitale mogelijkheden te transformeren en hun strategische doelen te bereiken.'
],
items: [
{
title: 'Gebruikersgerichte Oplossingen',
description: 'Ik focus op het creëren van oplossingen die de gebruikerservaring en productiviteit verbeteren, zodat technologie mensen effectief dient.',
},
{
title: 'Continue Verbetering',
description: 'Ik blijf op de hoogte van opkomende technologieën en best practices om geavanceerde oplossingen te leveren die meegroeien met uw behoeften.',
},
{
title: 'Strategische Implementatie',
description: 'Ik benader elk project strategisch, waarbij ik technische oplossingen afstem op bedrijfsdoelstellingen voor maximale impact.',
},
],
},
testimonials: {
tagline: 'Getuigenissen',
title: 'Wat klanten zeggen over mijn werk',
items: [
{
testimonial: 'De expertise van Richard in Power Automate heeft onze werkstroomprocessen getransformeerd, waardoor we talloze uren hebben bespaard en fouten aanzienlijk zijn verminderd.',
name: 'Klantnaam',
description: 'Functie, Bedrijf',
},
{
testimonial: 'De SharePoint-implementatie die Richard heeft geleverd, heeft onze documentbeheer- en teamsamenwerkingsmogelijkheden revolutionair veranderd.',
name: 'Klantnaam',
description: 'Functie, Bedrijf',
},
{
testimonial: 'De technische kennis van Richard in combinatie met zijn vermogen om onze zakelijke behoeften te begrijpen, resulteerde in oplossingen die echt onze uitdagingen aanpakten.',
name: 'Klantnaam',
description: 'Functie, Bedrijf',
},
],
},
callToAction: {
title: 'Klaar om uw IT-systemen te optimaliseren?',
subtitle: 'Laten we bespreken hoe ik uw organisatie kan helpen processen te stroomlijnen, samenwerking te verbeteren en digitale transformatie te stimuleren.',
button: 'Neem contact op',
},
contact: {
title: 'Neem contact op',
subtitle: 'Heeft u een project in gedachten of vragen over mijn diensten? Neem contact op en laten we een gesprek beginnen.',
nameLabel: 'Naam',
namePlaceholder: 'Uw naam',
emailLabel: 'E-mail',
emailPlaceholder: 'Uw e-mailadres',
messageLabel: 'Bericht',
messagePlaceholder: 'Uw bericht',
disclaimer: 'Door dit formulier in te dienen, gaat u akkoord met ons privacybeleid en staat u ons toe uw gegevens te gebruiken om contact met u op te nemen over onze diensten.',
description: 'Ik zal zo snel mogelijk op uw bericht reageren. U kunt ook verbinding maken met mij op LinkedIn of GitHub.',
},
},
resume: { resume: {
title: 'Werkervaring', title: 'Werkervaring',
experience: [ experience: [
@@ -558,7 +805,7 @@ export const translations: Record<string, Translation> = {
de: { de: {
metadata: { metadata: {
title: 'Über mich', title: 'Über mich',
aboutUs: 'Über uns', aboutUs: 'Über mich',
}, },
navigation: { navigation: {
home: 'Start', home: 'Start',
@@ -582,6 +829,104 @@ export const translations: Record<string, Translation> = {
'Ich besitze Zertifizierungen in Microsoft Teams Administration, Azure Fundamentals und Nexthink Administration. Meine Mission ist es, IT-Exzellenz durch die Optimierung von Cloud-Lösungen, die Automatisierung von Prozessen und die Bereitstellung hervorragender technischer Unterstützung voranzutreiben.' 'Ich besitze Zertifizierungen in Microsoft Teams Administration, Azure Fundamentals und Nexthink Administration. Meine Mission ist es, IT-Exzellenz durch die Optimierung von Cloud-Lösungen, die Automatisierung von Prozessen und die Bereitstellung hervorragender technischer Unterstützung voranzutreiben.'
], ],
}, },
homepage: {
actions: {
learnMore: 'Mehr erfahren',
contactMe: 'Kontaktieren Sie mich',
},
services: {
tagline: 'Dienstleistungen',
title: 'Wie ich Ihrer Organisation helfen kann',
subtitle: 'Ich biete eine Reihe spezialisierter IT-Dienstleistungen an, um Unternehmen bei der Optimierung ihrer Abläufe und digitalen Infrastruktur zu unterstützen.',
items: [
{
title: 'Workflow-Automatisierung',
description: 'Optimieren Sie Ihre Geschäftsprozesse mit Power Automate-Lösungen, die den manuellen Aufwand reduzieren und die betriebliche Effizienz steigern.',
},
{
title: 'Intelligente Chatbots',
description: 'Entwickeln Sie smarte Chatbots in Copilot Studio, die Benutzerinteraktionen durch natürliche Sprachverarbeitung und automatisierte Antworten verbessern.',
},
{
title: 'API-Integrationen',
description: 'Schaffen Sie nahtlose Verbindungen zwischen Ihren Anwendungen und Diensten mit benutzerdefinierten API-Integrationen für effizienten Datenaustausch.',
},
{
title: 'Microsoft 365 Management',
description: 'Optimieren Sie Ihre Microsoft 365-Umgebung mit fachkundiger Administration, Sicherheitskonfigurationen und Service-Optimierung.',
},
{
title: 'SharePoint-Lösungen',
description: 'Einrichten, Verwalten und Optimieren von SharePoint Online und On-Premise-Implementierungen für effektives Dokumentenmanagement und Zusammenarbeit.',
},
{
title: 'IT-Infrastrukturüberwachung',
description: 'Verwaltung globaler IT-Infrastrukturen, einschließlich Server, Netzwerke und Endbenutzergeräte, um zuverlässige Betriebsabläufe zu gewährleisten.',
},
],
},
approach: {
tagline: 'Über meinen Ansatz',
title: 'IT-Exzellenz durch Innovation vorantreiben',
missionTitle: 'Leitbild',
missionContent: [
'Meine Mission ist es, IT-Exzellenz durch die Optimierung von Cloud-Lösungen, die Automatisierung von Prozessen und die Bereitstellung hervorragender technischer Unterstützung voranzutreiben. Ich glaube daran, Technologie zu nutzen, um echte geschäftliche Herausforderungen zu lösen und durch Innovation Mehrwert zu schaffen.',
'Mit über 15 Jahren IT-Erfahrung bringe ich einen reichen Schatz an Wissen in Microsoft-Technologien, Automatisierungstools und Systemintegration mit, um Organisationen dabei zu helfen, ihre digitalen Fähigkeiten zu transformieren und ihre strategischen Ziele zu erreichen.'
],
items: [
{
title: 'Nutzerzentrierte Lösungen',
description: 'Ich konzentriere mich auf die Entwicklung von Lösungen, die die Benutzererfahrung und Produktivität verbessern und sicherstellen, dass Technologie den Menschen effektiv dient.',
},
{
title: 'Kontinuierliche Verbesserung',
description: 'Ich bleibe auf dem Laufenden mit aufkommenden Technologien und Best Practices, um innovative Lösungen zu liefern, die mit Ihren Bedürfnissen wachsen.',
},
{
title: 'Strategische Implementierung',
description: 'Ich gehe strategisch an jedes Projekt heran und stimme technische Lösungen mit Geschäftszielen ab, um maximale Wirkung zu erzielen.',
},
],
},
testimonials: {
tagline: 'Referenzen',
title: 'Was Kunden über meine Arbeit sagen',
items: [
{
testimonial: 'Richards Expertise in Power Automate hat unsere Workflow-Prozesse transformiert, uns unzählige Stunden gespart und Fehler erheblich reduziert.',
name: 'Kundenname',
description: 'Position, Unternehmen',
},
{
testimonial: 'Die von Richard gelieferte SharePoint-Implementierung hat unsere Dokumentenmanagement- und Teamkollaborationsfähigkeiten revolutioniert.',
name: 'Kundenname',
description: 'Position, Unternehmen',
},
{
testimonial: 'Richards technisches Wissen in Kombination mit seiner Fähigkeit, unsere Geschäftsanforderungen zu verstehen, führte zu Lösungen, die unsere Herausforderungen wirklich adressierten.',
name: 'Kundenname',
description: 'Position, Unternehmen',
},
],
},
callToAction: {
title: 'Bereit, Ihre IT-Systeme zu optimieren?',
subtitle: 'Lassen Sie uns besprechen, wie ich Ihrer Organisation helfen kann, Prozesse zu optimieren, die Zusammenarbeit zu verbessern und die digitale Transformation voranzutreiben.',
button: 'Kontaktieren Sie mich',
},
contact: {
title: 'Kontakt aufnehmen',
subtitle: 'Haben Sie ein Projekt im Sinn oder Fragen zu meinen Dienstleistungen? Nehmen Sie Kontakt auf und lassen Sie uns ein Gespräch beginnen.',
nameLabel: 'Name',
namePlaceholder: 'Ihr Name',
emailLabel: 'E-Mail',
emailPlaceholder: 'Ihre E-Mail-Adresse',
messageLabel: 'Nachricht',
messagePlaceholder: 'Ihre Nachricht',
disclaimer: 'Durch das Absenden dieses Formulars stimmen Sie unserer Datenschutzrichtlinie zu und erlauben uns, Ihre Informationen zu verwenden, um Sie über unsere Dienstleistungen zu kontaktieren.',
description: 'Ich werde so schnell wie möglich auf Ihre Nachricht antworten. Sie können sich auch über LinkedIn oder GitHub mit mir verbinden.',
},
},
resume: { resume: {
title: 'Berufserfahrung', title: 'Berufserfahrung',
experience: [ experience: [
@@ -799,7 +1144,7 @@ export const translations: Record<string, Translation> = {
fr: { fr: {
metadata: { metadata: {
title: 'À propos de moi', title: 'À propos de moi',
aboutUs: 'À propos de nous', aboutUs: 'À propos de moi',
}, },
navigation: { navigation: {
home: 'Accueil', home: 'Accueil',
@@ -823,6 +1168,104 @@ export const translations: Record<string, Translation> = {
'Je possède des certifications en administration Microsoft Teams, Azure Fundamentals et administration Nexthink. Ma mission est de promouvoir l\'excellence IT en optimisant les solutions cloud, en automatisant les processus et en fournissant un support technique exceptionnel.' 'Je possède des certifications en administration Microsoft Teams, Azure Fundamentals et administration Nexthink. Ma mission est de promouvoir l\'excellence IT en optimisant les solutions cloud, en automatisant les processus et en fournissant un support technique exceptionnel.'
], ],
}, },
homepage: {
actions: {
learnMore: 'En savoir plus',
contactMe: 'Me contacter',
},
services: {
tagline: 'Services',
title: 'Comment je peux aider votre organisation',
subtitle: 'Je propose une gamme de services IT spécialisés pour aider les entreprises à optimiser leurs opérations et leur infrastructure numérique.',
items: [
{
title: 'Automatisation des flux de travail',
description: 'Rationalisez vos processus métier avec des solutions Power Automate qui réduisent l\'effort manuel et augmentent l\'efficacité opérationnelle.',
},
{
title: 'Chatbots intelligents',
description: 'Développez des chatbots intelligents dans Copilot Studio qui améliorent les interactions utilisateur grâce au traitement du langage naturel et aux réponses automatisées.',
},
{
title: 'Intégrations API',
description: 'Créez des connexions transparentes entre vos applications et services avec des intégrations API personnalisées pour un échange de données efficace.',
},
{
title: 'Gestion Microsoft 365',
description: 'Optimisez votre environnement Microsoft 365 avec une administration experte, des configurations de sécurité et une optimisation des services.',
},
{
title: 'Solutions SharePoint',
description: 'Configurez, gérez et optimisez les déploiements SharePoint Online et on-premise pour une gestion efficace des documents et une collaboration optimale.',
},
{
title: 'Supervision de l\'infrastructure IT',
description: 'Gérez les infrastructures IT mondiales, y compris les serveurs, les réseaux et les appareils des utilisateurs finaux pour assurer des opérations fiables.',
},
],
},
approach: {
tagline: 'À propos de mon approche',
title: 'Favoriser l\'excellence IT par l\'innovation',
missionTitle: 'Énoncé de mission',
missionContent: [
'Ma mission est de favoriser l\'excellence IT en optimisant les solutions cloud, en automatisant les processus et en fournissant un support technique exceptionnel. Je crois en l\'utilisation de la technologie pour résoudre de véritables défis commerciaux et créer de la valeur grâce à l\'innovation.',
'Avec plus de 15 ans d\'expérience en IT, j\'apporte une richesse de connaissances en technologies Microsoft, outils d\'automatisation et intégration de systèmes pour aider les organisations à transformer leurs capacités numériques et à atteindre leurs objectifs stratégiques.'
],
items: [
{
title: 'Solutions centrées sur l\'utilisateur',
description: 'Je me concentre sur la création de solutions qui améliorent l\'expérience utilisateur et la productivité, en veillant à ce que la technologie serve efficacement les personnes.',
},
{
title: 'Amélioration continue',
description: 'Je reste à jour avec les technologies émergentes et les meilleures pratiques pour fournir des solutions de pointe qui évoluent avec vos besoins.',
},
{
title: 'Implémentation stratégique',
description: 'J\'aborde chaque projet de manière stratégique, en alignant les solutions techniques sur les objectifs commerciaux pour un impact maximal.',
},
],
},
testimonials: {
tagline: 'Témoignages',
title: 'Ce que les clients disent de mon travail',
items: [
{
testimonial: 'L\'expertise de Richard en Power Automate a transformé nos processus de flux de travail, nous faisant gagner d\'innombrables heures et réduisant considérablement les erreurs.',
name: 'Nom du client',
description: 'Poste, Entreprise',
},
{
testimonial: 'L\'implémentation SharePoint livrée par Richard a révolutionné nos capacités de gestion documentaire et de collaboration d\'équipe.',
name: 'Nom du client',
description: 'Poste, Entreprise',
},
{
testimonial: 'Les connaissances techniques de Richard combinées à sa capacité à comprendre nos besoins commerciaux ont abouti à des solutions qui ont véritablement répondu à nos défis.',
name: 'Nom du client',
description: 'Poste, Entreprise',
},
],
},
callToAction: {
title: 'Prêt à optimiser vos systèmes IT ?',
subtitle: 'Discutons de la façon dont je peux aider votre organisation à rationaliser les processus, améliorer la collaboration et favoriser la transformation numérique.',
button: 'Me contacter',
},
contact: {
title: 'Prendre contact',
subtitle: 'Vous avez un projet en tête ou des questions sur mes services ? Contactez-moi et commençons une conversation.',
nameLabel: 'Nom',
namePlaceholder: 'Votre nom',
emailLabel: 'Email',
emailPlaceholder: 'Votre adresse email',
messageLabel: 'Message',
messagePlaceholder: 'Votre message',
disclaimer: 'En soumettant ce formulaire, vous acceptez notre politique de confidentialité et nous autorisez à utiliser vos informations pour vous contacter au sujet de nos services.',
description: 'Je répondrai à votre message dès que possible. Vous pouvez également me contacter sur LinkedIn ou GitHub.',
},
},
resume: { resume: {
title: 'Expérience professionnelle', title: 'Expérience professionnelle',
experience: [ experience: [

View File

@@ -12,6 +12,7 @@ import SiteVerification from '~/components/common/SiteVerification.astro';
import Analytics from '~/components/common/Analytics.astro'; import Analytics from '~/components/common/Analytics.astro';
import BasicScripts from '~/components/common/BasicScripts.astro'; import BasicScripts from '~/components/common/BasicScripts.astro';
import StructuredData from '~/components/common/StructuredData.astro'; import StructuredData from '~/components/common/StructuredData.astro';
import LanguagePersistence from '~/components/LanguagePersistence.astro';
// Comment the line below to disable View Transitions // Comment the line below to disable View Transitions
import { ClientRouter } from 'astro:transitions'; import { ClientRouter } from 'astro:transitions';
@@ -59,5 +60,6 @@ const { language, textDirection } = I18N;
<slot /> <slot />
<BasicScripts /> <BasicScripts />
<LanguagePersistence />
</body> </body>
</html> </html>

View File

@@ -1,28 +1,58 @@
import { getPermalink, getAsset } from './utils/permalinks'; import { getPermalink, getAsset } from './utils/permalinks';
import { getTranslation } from './i18n/translations';
export const headerData = { export const getHeaderData = (lang = 'en') => {
links: [ const t = getTranslation(lang);
{
text: 'Home', // For hash links on the homepage, we need special handling
href: getPermalink('/'), const homeHashLink = (hash) => {
}, // Create an absolute path to the homepage with the language prefix
{ text: 'About', href: getPermalink('/en/#about')}, // and then append the hash
{ text: 'Resume', href: getPermalink('/en/#resume') }, return getPermalink('/', 'page', lang) + hash;
{ text: 'Certifications', href: getPermalink('/en/#Certifications') }, };
{ text: 'Skills', href: getPermalink('/en/#skills') },
{ text: 'Blog', href: getPermalink('/blog') }, return {
] links: [
{
text: t.navigation.home,
href: getPermalink('/', 'page', lang),
},
{
text: t.homepage?.services?.tagline || 'Services',
href: homeHashLink('#services'),
},
{ text: t.homepage?.contact?.title || 'Contact', href: homeHashLink('#contact') },
{
text: t.metadata?.aboutUs || 'About Me',
links: [
{ text: t.navigation.about, href: getPermalink('/aboutme', 'page', lang), isHashLink: false },
{ text: t.navigation.resume, href: getPermalink('/aboutme', 'page', lang) + '#resume', isHashLink: true },
{ text: t.navigation.certifications, href: getPermalink('/aboutme', 'page', lang) + '#certifications', isHashLink: true },
{ text: t.navigation.skills, href: getPermalink('/aboutme', 'page', lang) + '#skills', isHashLink: true },
{ text: t.navigation.education, href: getPermalink('/aboutme', 'page', lang) + '#education', isHashLink: true },
]
},
{ text: t.navigation.blog, href: getPermalink('/blog', 'page', lang) },
]
};
}; };
export const footerData = { // For backward compatibility
export const headerData = getHeaderData();
secondaryLinks: [ export const getFooterData = (lang = 'en') => {
{ text: 'Terms', href: getPermalink('/terms') }, return {
{ text: 'Privacy Policy', href: getPermalink('/privacy') }, secondaryLinks: [
], { text: 'Terms', href: getPermalink('/terms', 'page', lang) },
socialLinks: [ { text: 'Privacy Policy', href: getPermalink('/privacy', 'page', lang) },
{ ariaLabel: 'LinkedIn', icon: 'tabler:brand-linkedin', href: 'https://www.linkedin.com/in/rrpbergsma' }, ],
{ ariaLabel: 'Github', icon: 'tabler:brand-github', href: 'https://github.com/rrpbergsma' }, socialLinks: [
{ ariaLabel: 'RSS', icon: 'tabler:rss', href: getAsset('/rss.xml') }, { ariaLabel: 'LinkedIn', icon: 'tabler:brand-linkedin', href: 'https://www.linkedin.com/in/rrpbergsma' },
], { ariaLabel: 'Github', icon: 'tabler:brand-github', href: 'https://github.com/rrpbergsma' },
{ ariaLabel: 'RSS', icon: 'tabler:rss', href: getAsset('/rss.xml') },
],
};
}; };
// For backward compatibility
export const footerData = getFooterData();

View File

@@ -0,0 +1,141 @@
---
export const prerender = false;
import Layout from '~/layouts/PageLayout.astro';
import StructuredData from '~/components/common/StructuredData.astro';
import Hero from '~/components/widgets/Hero.astro';
import Content from '~/components/widgets/Content.astro';
import CompactSteps from '~/components/widgets/CompactSteps.astro';
import WorkExperience from '~/components/widgets/WorkExperience.astro';
import CompactCertifications from '~/components/widgets/CompactCertifications.astro';
import CompactSkills from '~/components/widgets/CompactSkills.astro';
import HomePageImage from '~/assets/images/richardbergsma.png'
import { getTranslation, supportedLanguages } from '~/i18n/translations';
export async function getStaticPaths() {
return supportedLanguages.map(lang => ({
params: { lang },
}));
}
const { lang } = Astro.params;
if (!supportedLanguages.includes(lang)) {
return Astro.redirect('/en/aboutme');
}
const t = getTranslation(lang);
const metadata = {
title: 'About Me - ' + t.metadata.title,
};
---
<Layout metadata={metadata}>
<Fragment slot="announcement"></Fragment>
<!-- Person Structured Data for SEO -->
<StructuredData slot="structured-data" data={{
"@context": "https://schema.org",
"@type": "Person",
"name": "Richard Bergsma",
"jobTitle": "IT Systems and Automation Manager",
"description": t.hero.subtitle,
"image": Astro.url.origin + "/images/richardbergsma.png",
"url": Astro.url.origin,
"sameAs": [
"https://www.linkedin.com/in/rrpbergsma",
"https://github.com/rrpbergsma"
],
"knowsAbout": t.skills.items.map(skill => skill.title),
"worksFor": {
"@type": "Organization",
"name": "COFRA Holding C.V.",
"location": "Amsterdam"
}
}} />
<!-- Hero Widget -->
<Hero
id="hero"
title="About Me"
>
<Fragment slot="subtitle">
<strong class="text-3xl md:text-4xl">{t.hero.greeting}</strong><br /><br />{t.hero.subtitle}
</Fragment>
</Hero>
<!-- Content Widget -->
<Content
id="about"
columns={2}
items={[]}
image={{
src: HomePageImage,
alt: 'Richard Bergsma smiling in the mountains of Switzerland holding Revella',
loading: 'lazy',
}}
>
<Fragment slot="content">
<h2 class="text-3xl font-bold tracking-tight sm:text-4xl mb-2">{t.about.title}</h2>
{t.about.content.map((paragraph) => (
<p>{paragraph}</p>
<br />
))}
</Fragment>
<Fragment slot="bg">
<div class="absolute inset-0 bg-blue-50 dark:bg-transparent"></div>
</Fragment>
</Content>
<!-- Work Experience - Modern Timeline Layout -->
<WorkExperience
id="resume"
title={t.resume.title}
compact={true}
items={t.resume.experience.map(exp => ({
title: exp.title,
company: exp.company,
date: exp.period,
location: exp.location,
description: exp.description,
icon: 'tabler:briefcase',
}))}
/>
<!-- Certifications - Compact Layout -->
<CompactCertifications
id="certifications"
title={t.certifications.title}
subtitle={t.certifications.subtitle}
testimonials={t.certifications.items.map((cert) => ({
name: cert.name,
issueDate: cert.issueDate,
description: cert.description,
linkUrl: cert.linkUrl,
image: cert.image
}))}
/>
<!-- Skills - Compact Layout -->
<CompactSkills
id="skills"
title={t.skills.title}
subtitle={t.skills.subtitle}
defaultIcon="tabler:point-filled"
items={t.skills.items.map(item => ({
title: item.title,
description: item.description,
}))}
/>
<!-- Education - Compact Layout -->
<CompactSteps
id="education"
title={t.education.title}
items={t.education.items.map(item => ({
title: item.title,
icon: 'tabler:school'
}))}
/>
</Layout>

View File

@@ -1,24 +1,14 @@
--- ---
export const prerender = false; export const prerender = false;
import Layout from '~/layouts/PageLayout.astro'; import Layout from '~/layouts/PageLayout.astro';
import Header from '~/components/widgets/Header.astro'; import Hero from '~/components/widgets/Hero2.astro';
import StructuredData from '~/components/common/StructuredData.astro'; import Features from '~/components/widgets/Features.astro';
import Hero from '~/components/widgets/Hero.astro';
import Content from '~/components/widgets/Content.astro'; import Content from '~/components/widgets/Content.astro';
import CompactSteps from '~/components/widgets/CompactSteps.astro'; import Testimonials from '~/components/widgets/Testimonials.astro';
import WorkExperience from '~/components/widgets/WorkExperience.astro'; import CallToAction from '~/components/widgets/CallToAction.astro';
import CompactCertifications from '~/components/widgets/CompactCertifications.astro'; import Contact from '~/components/widgets/Contact.astro';
import CompactSkills from '~/components/widgets/CompactSkills.astro';
import BlogLatestPosts from '~/components/widgets/BlogLatestPosts.astro';
import HomePageImage from '~/assets/images/richardbergsma.png'
import fetch from 'node-fetch';
import { getTranslation, supportedLanguages } from '~/i18n/translations'; import { getTranslation, supportedLanguages } from '~/i18n/translations';
interface IpApiResponse {
countryCode: string;
}
export async function getStaticPaths() { export async function getStaticPaths() {
return supportedLanguages.map(lang => ({ return supportedLanguages.map(lang => ({
params: { lang }, params: { lang },
@@ -30,181 +20,212 @@ if (!supportedLanguages.includes(lang)) {
return Astro.redirect('/en/'); return Astro.redirect('/en/');
} }
// Geo-location based redirect
if (Astro.request.headers.get('host') === '365devnet.nl') {
try {
const ip = Astro.clientAddress;
const response = await fetch(`http://ip-api.com/json/${ip}`);
const data = await response.json() as IpApiResponse;
if (data?.countryCode === 'NL') {
return Astro.redirect('/nl/');
}
} catch (error) {
console.error('Geo-location error:', error);
// Fallback: Redirect to default language (English)
return Astro.redirect('/en/');
}
}
const t = getTranslation(lang); const t = getTranslation(lang);
const metadata = { const metadata = {
title: t.metadata.title, title: t.metadata.title,
}; };
--- ---
<Layout metadata={metadata}> <Layout metadata={metadata}>
<Fragment slot="announcement"></Fragment>
<!-- Person Structured Data for SEO -->
<StructuredData slot="structured-data" data={{
"@context": "https://schema.org",
"@type": "Person",
"name": "Richard Bergsma",
"jobTitle": "IT Systems and Automation Manager",
"description": t.hero.subtitle,
"image": Astro.url.origin + "/images/richardbergsma.png",
"url": Astro.url.origin,
"sameAs": [
"https://www.linkedin.com/in/rrpbergsma",
"https://github.com/rrpbergsma"
],
"knowsAbout": t.skills.items.map(skill => skill.title),
"worksFor": {
"@type": "Organization",
"name": "COFRA Holding C.V.",
"location": "Amsterdam"
}
}} />
<Fragment slot="header">
<Header
links={[
{ text: t.navigation.home, href: '#hero' },
{ text: t.navigation.about, href: '#about' },
{ text: t.navigation.resume, href: '#resume' },
{ text: t.navigation.certifications, href: '#certifications' },
{ text: t.navigation.skills, href: '#skills' },
{ text: t.navigation.education, href: '#education' },
{ text: t.navigation.blog, href: '#blog' },
]}
isSticky
showToggleTheme
/>
</Fragment>
<script>
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
window.scrollTo({
top: targetElement.offsetTop - 50,
behavior: 'smooth'
});
}
});
});
</script>
<!-- Hero Widget --> <!-- Hero Widget -->
<Hero <Hero
id="hero" tagline={t.navigation.home}
title={t.hero.title} title={t.hero.title}
> subtitle={t.hero.subtitle}
<Fragment slot="subtitle"> actions={[
<strong class="text-3xl md:text-4xl">{t.hero.greeting}</strong><br /><br />{t.hero.subtitle} {
</Fragment> variant: 'primary',
</Hero> text: t.homepage?.actions?.learnMore || 'Learn More',
href: '#services',
icon: 'tabler:arrow-down',
},
{ text: t.homepage?.actions?.contactMe || 'Contact Me', href: '#contact' },
]}
image={{
src: '~/assets/images/richardbergsma.png',
alt: 'Richard Bergsma - IT Systems and Automation Manager',
}}
/>
<!-- Features Widget -->
<Features
id="services"
tagline={t.homepage?.services?.tagline || "Services"}
title={t.homepage?.services?.title || "How I Can Help Your Organization"}
subtitle={t.homepage?.services?.subtitle || "I offer a range of specialized IT services to help businesses optimize their operations and digital infrastructure."}
items={t.homepage?.services?.items || [
{
title: 'Workflow Automation',
description:
'Streamline your business processes with Power Automate solutions that reduce manual effort and increase operational efficiency.',
icon: 'tabler:settings-automation',
},
{
title: 'Intelligent Chatbots',
description:
'Develop smart chatbots in Copilot Studio that enhance user interactions through natural language processing and automated responses.',
icon: 'tabler:message-chatbot',
},
{
title: 'API Integrations',
description:
'Create seamless connections between your applications and services with custom API integrations for efficient data exchange.',
icon: 'tabler:api',
},
{
title: 'Microsoft 365 Management',
description:
'Optimize your Microsoft 365 environment with expert administration, security configurations, and service optimization.',
icon: 'tabler:brand-office',
},
{
title: 'SharePoint Solutions',
description:
'Set up, manage, and optimize SharePoint Online and on-premise deployments for effective document management and collaboration.',
icon: 'tabler:share',
},
{
title: 'IT Infrastructure Oversight',
description:
'Manage global IT infrastructures, including servers, networks, and end-user devices to ensure reliable operations.',
icon: 'tabler:server',
},
].map(item => ({...item, icon: item.icon || 'tabler:check'}))}
/>
<!-- Content Widget --> <!-- Content Widget -->
<Content <Content
id="about" isReversed
columns={2} tagline={t.homepage?.approach?.tagline || "About My Approach"}
items={[]} title={t.homepage?.approach?.title || "Driving IT Excellence Through Innovation"}
items={t.homepage?.approach?.items || [
{
title: 'User-Centric Solutions',
description:
'I focus on creating solutions that enhance user experience and productivity, ensuring technology serves people effectively.',
},
{
title: 'Continuous Improvement',
description:
'I stay current with emerging technologies and best practices to deliver cutting-edge solutions that evolve with your needs.',
},
{
title: 'Strategic Implementation',
description:
'I approach each project strategically, aligning technical solutions with business objectives for maximum impact.',
},
]}
image={{ image={{
src: HomePageImage, src: '~/assets/images/hero-image.png',
alt: 'Richard Bergsma smiling in the mountains of Switzerland holding Revella', alt: 'Digital Transformation Image',
loading: 'lazy',
}} }}
> >
<Fragment slot="content"> <Fragment slot="content">
<h2 class="text-3xl font-bold tracking-tight sm:text-4xl mb-2">{t.about.title}</h2> <h3 class="text-2xl font-bold tracking-tight dark:text-white sm:text-3xl mb-2">
{t.about.content.map((paragraph) => ( {t.homepage?.approach?.missionTitle || "Mission Statement"}
<p>{paragraph}</p> </h3>
<br /> {(t.homepage?.approach?.missionContent || [
'My mission is to drive IT excellence by optimizing cloud solutions, automating processes, and providing outstanding technical support. I believe in leveraging technology to solve real business challenges and create value through innovation.',
'With over 15 years of IT experience, I bring a wealth of knowledge in Microsoft technologies, automation tools, and system integration to help organizations transform their digital capabilities and achieve their strategic goals.'
]).map((paragraph) => (
<>
<p>{paragraph}</p>
<br />
</>
))} ))}
</Fragment> </Fragment>
<Fragment slot="bg">
<div class="absolute inset-0 bg-blue-50 dark:bg-transparent"></div>
</Fragment>
</Content> </Content>
<!-- Work Experience - Modern Timeline Layout --> <!-- Testimonials Widget -->
<WorkExperience <Testimonials
id="resume" tagline={t.homepage?.testimonials?.tagline || "Testimonials"}
title={t.resume.title} title={t.homepage?.testimonials?.title || "What Clients Say About My Work"}
compact={true} testimonials={(t.homepage?.testimonials?.items || [
items={t.resume.experience.map(exp => ({ {
title: exp.title, testimonial:
company: exp.company, "Richard's expertise in Power Automate transformed our workflow processes, saving us countless hours and reducing errors significantly.",
date: exp.period, name: 'Client Name',
location: exp.location, description: 'Position, Company',
description: exp.description, },
icon: 'tabler:briefcase', {
testimonial:
"The SharePoint implementation Richard delivered has revolutionized our document management and team collaboration capabilities.",
name: 'Client Name',
description: 'Position, Company',
},
{
testimonial:
"Richard's technical knowledge combined with his ability to understand our business needs resulted in solutions that truly addressed our challenges.",
name: 'Client Name',
description: 'Position, Company',
},
]).map(item => ({
...item,
image: {
src: '~/assets/images/default.png',
alt: item.name,
}
}))} }))}
/> />
<!-- Certifications - Compact Layout --> <!-- CallToAction Widget -->
<CompactCertifications <CallToAction
id="certifications" callToAction={{
title={t.certifications.title} text: t.homepage?.callToAction?.button || 'Contact Me',
subtitle={t.certifications.subtitle} href: '#contact',
testimonials={t.certifications.items.map((cert) => ({ icon: 'tabler:mail',
name: cert.name, }}
issueDate: cert.issueDate,
description: cert.description,
linkUrl: cert.linkUrl,
image: cert.image
}))}
/>
<!-- Skills - Compact Layout -->
<CompactSkills
id="skills"
title={t.skills.title}
subtitle={t.skills.subtitle}
defaultIcon="tabler:point-filled"
items={t.skills.items.map(item => ({
title: item.title,
description: item.description,
}))}
/>
<!-- Education - Compact Layout -->
<CompactSteps
id="education"
title={t.education.title}
items={t.education.items.map(item => ({
title: item.title,
icon: 'tabler:school'
}))}
/>
<!-- BlogLatestPost Widget -->
<BlogLatestPosts
id="blog"
title={t.blog.title}
information={t.blog.information}
> >
<Fragment slot="bg"> <Fragment slot="title">{t.homepage?.callToAction?.title || 'Ready to optimize your IT systems?'}</Fragment>
<div class="absolute inset-0"></div> <Fragment slot="subtitle">
{t.homepage?.callToAction?.subtitle || 'Let\'s discuss how I can help your organization streamline processes, enhance collaboration, and drive digital transformation.'}
</Fragment> </Fragment>
</BlogLatestPosts> </CallToAction>
<!-- Contact Widget -->
<Contact
id="contact"
title={t.homepage?.contact?.title || "Get in Touch"}
subtitle={t.homepage?.contact?.subtitle || "Have a project in mind or questions about my services? Reach out and let's start a conversation."}
inputs={[
{
type: 'text',
name: 'name',
label: t.homepage?.contact?.nameLabel || 'Name',
placeholder: t.homepage?.contact?.namePlaceholder || 'Your name',
},
{
type: 'email',
name: 'email',
label: t.homepage?.contact?.emailLabel || 'Email',
placeholder: t.homepage?.contact?.emailPlaceholder || 'Your email address',
},
]}
textarea={{
label: t.homepage?.contact?.messageLabel || 'Message',
placeholder: t.homepage?.contact?.messagePlaceholder || 'Your message',
rows: 8,
}}
disclaimer={{
label: t.homepage?.contact?.disclaimer ||
'By submitting this form, you agree to our privacy policy and allow us to use your information to contact you about our services.',
}}
description={t.homepage?.contact?.description || "I'll respond to your message as soon as possible. You can also connect with me on LinkedIn or GitHub."}
/>
<div class="flex justify-center space-x-4 mt-8 mb-12">
<a href="https://www.linkedin.com/in/rrpbergsma" class="text-gray-500 hover:text-blue-600" target="_blank" rel="noopener noreferrer">
<span class="sr-only">LinkedIn</span>
<svg class="h-8 w-8" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.454C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z"/>
</svg>
</a>
<a href="https://github.com/rrpbergsma" class="text-gray-500 hover:text-gray-900 dark:hover:text-white" target="_blank" rel="noopener noreferrer">
<span class="sr-only">GitHub</span>
<svg class="h-8 w-8" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/>
</svg>
</a>
</div>
</Layout> </Layout>

View File

@@ -1,228 +0,0 @@
---
import Features2 from '~/components/widgets/Features2.astro';
import Features3 from '~/components/widgets/Features3.astro';
import Hero from '~/components/widgets/Hero.astro';
import Stats from '~/components/widgets/Stats.astro';
import Steps2 from '~/components/widgets/Steps2.astro';
import Layout from '~/layouts/PageLayout.astro';
const metadata = {
title: 'About us',
};
---
<Layout metadata={metadata}>
<!-- Hero Widget ******************* -->
<Hero
tagline="About us"
image={{
src: 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
alt: 'Caos Image',
}}
>
<Fragment slot="title">
Elevate your online presence with our <br />
<span class="text-accent dark:text-white highlight"> Beautiful Website Templates</span>
</Fragment>
<Fragment slot="subtitle">
Donec efficitur, ipsum quis congue luctus, mauris magna convallis mauris, eu auctor nisi lectus non augue. Donec
quis lorem non massa vulputate efficitur ac at turpis. Sed tincidunt ex a nunc convallis, et lobortis nisi tempus.
Suspendisse vitae nisi eget tortor luctus maximus sed non lectus.
</Fragment>
</Hero>
<!-- Stats Widget ****************** -->
<Stats
title="Statistics about us"
stats={[
{ title: 'Offices', amount: '4' },
{ title: 'Employees', amount: '248' },
{ title: 'Templates', amount: '12' },
{ title: 'Awards', amount: '24' },
]}
/>
<!-- Features3 Widget ************** -->
<Features3
title="Our templates"
subtitle="Etiam scelerisque, enim eget vestibulum luctus, nibh mauris blandit nulla, nec vestibulum risus justo ut enim. Praesent lacinia diam et ante imperdiet euismod."
columns={3}
isBeforeContent={true}
items={[
{
title: 'Educational',
description:
'Morbi faucibus luctus quam, sit amet aliquet felis tempor id. Cras augue massa, ornare quis dignissim a, molestie vel nulla.',
icon: 'tabler:template',
},
{
title: 'Interior Design',
description:
'Vivamus porttitor, tortor convallis aliquam pretium, turpis enim consectetur elit, vitae egestas purus erat ac nunc nulla.',
icon: 'tabler:template',
},
{
title: 'Photography',
description:
'Duis sed lectus in nisl vehicula porttitor eget quis odio. Aliquam erat volutpat. Nulla eleifend nulla id sem fermentum.',
icon: 'tabler:template',
},
]}
/>
<!-- Features3 Widget ************** -->
<Features3
columns={3}
isAfterContent={true}
items={[
{
title: 'E-commerce',
description:
'Rutrum non odio at vehicula. Proin ipsum justo, dignissim in vehicula sit amet, dignissim id quam. Sed ac tincidunt sapien.',
icon: 'tabler:template',
},
{
title: 'Blog',
description:
'Nullam efficitur volutpat sem sed fringilla. Suspendisse et enim eu orci volutpat laoreet ac vitae libero.',
icon: 'tabler:template',
},
{
title: 'Business',
description:
'Morbi et elit finibus, facilisis justo ut, pharetra ipsum. Donec efficitur, ipsum quis congue luctus, mauris magna.',
icon: 'tabler:template',
},
{
title: 'Branding',
description:
'Suspendisse vitae nisi eget tortor luctus maximus sed non lectus. Cras malesuada pretium placerat. Nullam venenatis dolor a ante rhoncus.',
icon: 'tabler:template',
},
{
title: 'Medical',
description:
'Vestibulum malesuada lacus id nibh posuere feugiat. Nam volutpat nulla a felis ultrices, id suscipit mauris congue. In hac habitasse platea dictumst.',
icon: 'tabler:template',
},
{
title: 'Fashion Design',
description:
'Maecenas eu tellus eget est scelerisque lacinia et a diam. Aliquam velit lorem, vehicula id fermentum et, rhoncus et purus.',
icon: 'tabler:template',
},
]}
image={{
src: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1740&q=80',
alt: 'Colorful Image',
}}
/>
<!-- Steps2 Widget ****************** -->
<Steps2
title="Our values"
subtitle="Maecenas eu tellus eget est scelerisque lacinia et a diam. Aliquam velit lorem, vehicula id fermentum et, rhoncus et purus. Nulla facilisi. Vestibulum malesuada lacus."
items={[
{
title: 'Customer-centric approach',
description:
'Donec id nibh neque. Quisque et fermentum tortor. Fusce vitae dolor a mauris dignissim commodo. Ut eleifend luctus condimentum.',
},
{
title: 'Constant Improvement',
description:
'Phasellus laoreet fermentum venenatis. Vivamus dapibus pulvinar arcu eget mattis. Fusce eget mauris leo.',
},
{
title: 'Ethical Practices',
description:
'Vestibulum imperdiet libero et lectus molestie, et maximus augue porta. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.',
},
]}
/>
<!-- Steps2 Widget ****************** -->
<Steps2
title="Achievements"
subtitle="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sagittis, quam nec venenatis lobortis, mi risus tempus nulla, sed porttitor est nibh at nulla."
isReversed={true}
callToAction={{
text: 'See more',
href: '/',
}}
items={[
{
title: 'Global reach',
description: 'Nam malesuada urna in enim imperdiet tincidunt. Phasellus non tincidunt nisi, at elementum mi.',
icon: 'tabler:globe',
},
{
title: 'Positive customer feedback and reviews',
description:
'Cras semper nulla leo, eget laoreet erat cursus sed. Praesent faucibus massa in purus iaculis dictum.',
icon: 'tabler:message-star',
},
{
title: 'Awards and recognition as industry experts',
description:
'Phasellus lacinia cursus velit, eu malesuada magna pretium eu. Etiam aliquet tellus purus, blandit lobortis ex rhoncus vitae.',
icon: 'tabler:award',
},
]}
/>
<!-- Features2 Widget ************** -->
<Features2
title="Our locations"
tagline="Find us"
columns={4}
items={[
{
title: 'EE.UU',
description: '1234 Lorem Ipsum St, 12345, Miami',
},
{
title: 'Spain',
description: '5678 Lorem Ipsum St, 56789, Madrid',
},
{
title: 'Australia',
description: '9012 Lorem Ipsum St, 90123, Sydney',
},
{
title: 'Brazil',
description: '3456 Lorem Ipsum St, 34567, São Paulo',
},
]}
/>
<!-- Features2 Widget ************** -->
<Features2
title="Technical Support"
tagline="Contact us"
columns={2}
items={[
{
title: 'Chat with us',
description:
'Integer luctus laoreet libero, auctor varius purus rutrum sit amet. Ut nec molestie nisi, quis eleifend mi.',
icon: 'tabler:messages',
},
{
title: 'Call us',
description:
'Mauris faucibus finibus orci, in posuere elit viverra non. In hac habitasse platea dictumst. Cras lobortis metus a hendrerit congue.',
icon: 'tabler:headset',
},
]}
/>
</Layout>

32
src/pages/aboutme.astro Normal file
View File

@@ -0,0 +1,32 @@
---
export const prerender = false;
import { supportedLanguages } from '~/i18n/translations';
// Check for language preference in cookies (set by client-side JS)
const cookies = Astro.request.headers.get('cookie') || '';
const cookieLanguage = cookies.split(';')
.map(cookie => cookie.trim())
.find(cookie => cookie.startsWith('preferredLanguage='))
?.split('=')[1];
// Get the user's preferred language from the browser if no cookie
const acceptLanguage = Astro.request.headers.get('accept-language') || '';
// Define the type for supported languages
type SupportedLanguage = typeof supportedLanguages[number];
// Use cookie language if available, otherwise detect from browser
const preferredLanguage =
(cookieLanguage && supportedLanguages.includes(cookieLanguage as SupportedLanguage))
? cookieLanguage
: acceptLanguage
.split(',')
.map(lang => lang.split(';')[0].trim().substring(0, 2))
.find(lang => supportedLanguages.includes(lang as SupportedLanguage)) || 'en';
// Get the hash fragment if present
const url = new URL(Astro.request.url);
const hash = url.hash;
// Redirect to the language-specific about me page
return Astro.redirect(`/${preferredLanguage}/aboutme${hash}`);
---

View File

@@ -1,77 +1,32 @@
<!DOCTYPE html> ---
<html lang="en"> export const prerender = false;
<head> import { supportedLanguages } from '~/i18n/translations';
<meta charset="UTF-8" />
<title>Redirecting...</title>
<style>
/* Example minimal styling */
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: sans-serif;
background-color: #f9fafb;
color: #374151;
}
.container {
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1 class="text-2xl font-bold mb-4">Redirecting...</h1>
<p id="redirect-message">
You are being redirected to the <strong></strong> version of our site.
</p>
<p class="mt-4">
If you are not redirected automatically, please
<a id="redirect-link" href="" class="text-blue-500 underline">click here</a>.
</p>
</div>
<script type="module">
// Define the supported languages
const supportedLangs = ['en', 'nl', 'de'];
let chosenLang = 'nl'; // Default language
// Get the user's language from the browser // Check for language preference in cookies (set by client-side JS)
const userLang = navigator.language || (navigator.languages && navigator.languages[0]); const cookies = Astro.request.headers.get('cookie') || '';
const cookieLanguage = cookies.split(';')
if (userLang) { .map(cookie => cookie.trim())
// For example, "en-US" becomes "en" .find(cookie => cookie.startsWith('preferredLanguage='))
const preferredLang = userLang.split('-')[0]; ?.split('=')[1];
if (supportedLangs.includes(preferredLang)) {
chosenLang = preferredLang;
}
}
// Construct the target URL based on the chosen language
const targetURL = `/${chosenLang}`;
console.log("Target URL:", targetURL);
// Update the DOM with the computed language values: // Get the user's preferred language from the browser if no cookie
// Update the redirect message to display the chosen language. const acceptLanguage = Astro.request.headers.get('accept-language') || '';
const messageEl = document.getElementById('redirect-message'); // Define the type for supported languages
if (messageEl) { type SupportedLanguage = typeof supportedLanguages[number];
messageEl.innerHTML = `You are being redirected to the <strong>${chosenLang.toUpperCase()}</strong> version of our site.`;
}
// Update the href for the clickable link. // Use cookie language if available, otherwise detect from browser
const linkEl = document.getElementById('redirect-link'); const preferredLanguage =
if (linkEl) { (cookieLanguage && supportedLanguages.includes(cookieLanguage as SupportedLanguage))
linkEl.href = targetURL; ? cookieLanguage
} : acceptLanguage
.split(',')
.map(lang => lang.split(';')[0].trim().substring(0, 2))
.find(lang => supportedLanguages.includes(lang as SupportedLanguage)) || 'en';
// Option 1: Dynamically add a meta refresh tag. // Get the hash fragment if present
const metaRefresh = document.createElement("meta"); const url = new URL(Astro.request.url);
metaRefresh.httpEquiv = "refresh"; const hash = url.hash;
metaRefresh.content = `0; url=${targetURL}`;
document.head.appendChild(metaRefresh);
// Option 2: Alternatively, use JavaScript redirection: // Redirect to the language-specific homepage
// window.location.href = targetURL; return Astro.redirect(`/${preferredLanguage}/${hash}`);
</script> ---
</body>
</html>

View File

@@ -10,7 +10,7 @@ const createPath = (...params: string[]) => {
.map((el) => trimSlash(el)) .map((el) => trimSlash(el))
.filter((el) => !!el) .filter((el) => !!el)
.join('/'); .join('/');
return '/' + paths + (SITE.trailingSlash && paths ? '/' : ''); return '/' + paths + (SITE.trailingSlash && paths && !paths.includes('#') ? '/' : '');
}; };
const BASE_PATHNAME = SITE.base || '/'; const BASE_PATHNAME = SITE.base || '/';
@@ -29,36 +29,50 @@ export const POST_PERMALINK_PATTERN = trimSlash(APP_BLOG?.post?.permalink || `${
/** */ /** */
export const getCanonical = (path = ''): string | URL => { export const getCanonical = (path = ''): string | URL => {
const url = String(new URL(path, SITE.site)); let url = String(new URL(path, SITE.site));
if (SITE.trailingSlash == false && path && url.endsWith('/')) { if (SITE.trailingSlash == false && path && url.endsWith('/')) {
return url.slice(0, -1); url = url.slice(0, -1);
} else if (SITE.trailingSlash == true && path && !url.endsWith('/')) { } else if (SITE.trailingSlash == true && path && !url.endsWith('/')) {
return url + '/'; url = url + '/';
}
if (url.endsWith('/')) {
url = url.slice(0, -1);
} }
return url; return url;
}; };
/** */ /** */
export const getPermalink = (slug = '', type = 'page'): string => { export const getPermalink = (slug = '', type = 'page', lang = ''): string => {
if (slug.startsWith('#')) {
return slug;
}
let permalink: string; let permalink: string;
if ( if (
slug.startsWith('https://') || slug.startsWith('https://') ||
slug.startsWith('http://') || slug.startsWith('http://') ||
slug.startsWith('://') || slug.startsWith('://') ||
slug.startsWith('#') ||
slug.startsWith('javascript:') slug.startsWith('javascript:')
) { ) {
return slug; return slug;
} }
// Extract hash fragment if present
let hashFragment = '';
if (slug.includes('#')) {
const parts = slug.split('#');
slug = parts[0];
hashFragment = '#' + parts[1];
}
switch (type) { switch (type) {
case 'home': case 'home':
permalink = getHomePermalink(); permalink = getHomePermalink(lang);
break; break;
case 'blog': case 'blog':
permalink = getBlogPermalink(); permalink = getBlogPermalink(lang);
break; break;
case 'asset': case 'asset':
@@ -83,14 +97,15 @@ export const getPermalink = (slug = '', type = 'page'): string => {
break; break;
} }
return definitivePermalink(permalink); // Append hash fragment after creating the permalink
return definitivePermalink(permalink, lang) + hashFragment;
}; };
/** */ /** */
export const getHomePermalink = (): string => getPermalink('/'); export const getHomePermalink = (lang = ''): string => getPermalink('/', 'page', lang);
/** */ /** */
export const getBlogPermalink = (): string => getPermalink(BLOG_BASE); export const getBlogPermalink = (lang = ''): string => getPermalink(BLOG_BASE, 'page', lang);
/** */ /** */
export const getAsset = (path: string): string => export const getAsset = (path: string): string =>
@@ -101,7 +116,22 @@ export const getAsset = (path: string): string =>
.join('/'); .join('/');
/** */ /** */
const definitivePermalink = (permalink: string): string => createPath(BASE_PATHNAME, permalink); const definitivePermalink = (permalink: string, lang = ''): string => {
// Don't add language prefix to hash-only links
if (permalink.startsWith('#')) {
return permalink;
}
// Don't add language prefix to external links
if (permalink.startsWith('http://') || permalink.startsWith('https://') || permalink.startsWith('//')) {
return permalink;
}
if (lang && ['en', 'nl', 'de', 'fr'].includes(lang)) {
return createPath(BASE_PATHNAME, lang, permalink);
}
return createPath(BASE_PATHNAME, permalink);
};
/** */ /** */
export const applyGetPermalinks = (menu: object = {}) => { export const applyGetPermalinks = (menu: object = {}) => {