Added homepage and moved old homepage to aboutme page
This commit is contained in:
183
public/test-language-persistence.html
Normal file
183
public/test-language-persistence.html
Normal 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>
|
177
public/test-language-switching.html
Normal file
177
public/test-language-switching.html
Normal 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>
|
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { supportedLanguages } from '~/i18n/translations';
|
||||
|
||||
interface Props {
|
||||
currentLang: string;
|
||||
@@ -7,8 +8,6 @@ interface Props {
|
||||
|
||||
const { currentLang } = Astro.props;
|
||||
|
||||
import { supportedLanguages } from '~/i18n/translations';
|
||||
|
||||
type SupportedLanguage = typeof supportedLanguages[number];
|
||||
|
||||
const languages = [
|
||||
@@ -121,13 +120,13 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
<script define:vars={{ supportedLanguages }}>
|
||||
function setupLanguageDropdown() {
|
||||
const button = document.querySelector<HTMLButtonElement>('#menu-button');
|
||||
const menu = document.querySelector<HTMLDivElement>('#language-menu');
|
||||
const chevronIcon = document.querySelector<HTMLElement>('#chevron-icon');
|
||||
const selectedLanguageText = document.querySelector<HTMLElement>('#selected-language');
|
||||
const languageButtons = document.querySelectorAll<HTMLButtonElement>('[data-lang-code]');
|
||||
const button = document.querySelector('#menu-button');
|
||||
const menu = document.querySelector('#language-menu');
|
||||
const chevronIcon = document.querySelector('#chevron-icon');
|
||||
const selectedLanguageText = document.querySelector('#selected-language');
|
||||
const languageButtons = document.querySelectorAll('[data-lang-code]');
|
||||
|
||||
if (!button || !menu || !chevronIcon || !selectedLanguageText) {
|
||||
return;
|
||||
@@ -184,7 +183,7 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
|
||||
closeMenu();
|
||||
|
||||
// Toggle menu
|
||||
button.addEventListener('click', (e: MouseEvent) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (isOpen) {
|
||||
closeMenu();
|
||||
@@ -193,7 +192,7 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
|
||||
// Focus the first menu item for better keyboard navigation
|
||||
const firstMenuItem = menu.querySelector('button[role="menuitem"]');
|
||||
if (firstMenuItem) {
|
||||
(firstMenuItem as HTMLElement).focus();
|
||||
firstMenuItem.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -205,7 +204,7 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
|
||||
if (!langCode) return;
|
||||
|
||||
// Update button text and icon
|
||||
const langName = langButton.textContent?.trim();
|
||||
const langName = langButton.textContent ? langButton.textContent.trim() : '';
|
||||
const flagIcon = langButton.querySelector('svg');
|
||||
if (langName && flagIcon) {
|
||||
selectedLanguageText.textContent = langName;
|
||||
@@ -218,27 +217,84 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
|
||||
// Close menu
|
||||
closeMenu();
|
||||
|
||||
// Get current path and redirect
|
||||
const currentPath = window.location.pathname.replace(/\/$/, '');
|
||||
// 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);
|
||||
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
|
||||
window.location.href = isBlogPost ? `/${langCode}` : `/${langCode}${pathWithoutLang || ''}`;
|
||||
// Check if we're on a language-specific path
|
||||
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
|
||||
document.addEventListener('click', (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target;
|
||||
if (isOpen && !menu.contains(target) && !button.contains(target)) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle keyboard navigation
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
closeMenu();
|
||||
button.focus();
|
||||
@@ -257,7 +313,7 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
|
||||
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)
|
||||
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>
|
112
src/components/LanguagePersistence.astro
Normal file
112
src/components/LanguagePersistence.astro
Normal 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>
|
@@ -159,6 +159,205 @@ import { UI } from 'astrowind:config';
|
||||
onLoad();
|
||||
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 is:inline>
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { SITE } from 'astrowind:config';
|
||||
import { getHomePermalink } from '~/utils/permalinks';
|
||||
import { getFooterData } from '~/navigation';
|
||||
|
||||
interface Link {
|
||||
text?: string;
|
||||
@@ -17,13 +18,50 @@ interface Links {
|
||||
|
||||
export interface Props {
|
||||
links?: Array<Links>;
|
||||
secondaryLinks: Array<Link>;
|
||||
socialLinks: Array<Link>;
|
||||
secondaryLinks?: Array<Link>;
|
||||
socialLinks?: Array<Link>;
|
||||
footNote?: 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']}>
|
||||
@@ -38,13 +76,17 @@ const { socialLinks = [], theme = 'light' } = Astro.props;
|
||||
<!-- Site Title with Terms & Privacy Links -->
|
||||
<div class="flex flex-col items-start space-y-2">
|
||||
<!-- 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}
|
||||
</a>
|
||||
|
||||
<!-- Terms & Privacy Policy Links -->
|
||||
<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>
|
||||
|
||||
|
@@ -8,6 +8,7 @@ import LanguageDropdown from '~/components/LanguageDropdown.astro';
|
||||
|
||||
import { getHomePermalink } from '~/utils/permalinks';
|
||||
import { trimSlash, getAsset } from '~/utils/permalinks';
|
||||
import { getHeaderData } from '~/navigation';
|
||||
|
||||
interface Link {
|
||||
text?: string;
|
||||
@@ -18,6 +19,7 @@ interface Link {
|
||||
|
||||
interface MenuLink extends Link {
|
||||
links?: Array<MenuLink>;
|
||||
isHashLink?: boolean;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
@@ -31,9 +33,42 @@ export interface Props {
|
||||
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 {
|
||||
id = 'header',
|
||||
links = [],
|
||||
links = headerData.links,
|
||||
isSticky = false,
|
||||
isDark = false,
|
||||
isFullWidth = false,
|
||||
@@ -41,8 +76,6 @@ const {
|
||||
showRssFeed = false,
|
||||
position = 'center',
|
||||
} = Astro.props;
|
||||
|
||||
const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
|
||||
---
|
||||
|
||||
<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" />
|
||||
</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">
|
||||
{links.map(({ text: text2, href: href2 }) => (
|
||||
{links.map(({ text: text2, href: href2, isHashLink }) => (
|
||||
<li>
|
||||
<a
|
||||
class:list={[
|
||||
@@ -115,6 +148,7 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
|
||||
{ 'aw-link-active': href2 === currentPath },
|
||||
]}
|
||||
href={href2}
|
||||
data-hash-link={isHashLink ? 'true' : undefined}
|
||||
>
|
||||
{text2}
|
||||
</a>
|
||||
@@ -129,6 +163,7 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
|
||||
{ 'aw-link-active': href === currentPath },
|
||||
]}
|
||||
href={href}
|
||||
data-hash-link={href?.includes('#') ? 'true' : undefined}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
|
@@ -64,6 +64,57 @@ export interface Translation {
|
||||
title: 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;
|
||||
@@ -76,7 +127,7 @@ export const translations: Record<string, Translation> = {
|
||||
en: {
|
||||
metadata: {
|
||||
title: 'About me',
|
||||
aboutUs: 'About us',
|
||||
aboutUs: 'About me',
|
||||
},
|
||||
navigation: {
|
||||
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.'
|
||||
],
|
||||
},
|
||||
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: {
|
||||
title: 'Work experience',
|
||||
experience: [
|
||||
@@ -317,7 +466,7 @@ export const translations: Record<string, Translation> = {
|
||||
nl: {
|
||||
metadata: {
|
||||
title: 'Over mij',
|
||||
aboutUs: 'Over ons',
|
||||
aboutUs: 'Over mij',
|
||||
},
|
||||
navigation: {
|
||||
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.'
|
||||
],
|
||||
},
|
||||
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: {
|
||||
title: 'Werkervaring',
|
||||
experience: [
|
||||
@@ -558,7 +805,7 @@ export const translations: Record<string, Translation> = {
|
||||
de: {
|
||||
metadata: {
|
||||
title: 'Über mich',
|
||||
aboutUs: 'Über uns',
|
||||
aboutUs: 'Über mich',
|
||||
},
|
||||
navigation: {
|
||||
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.'
|
||||
],
|
||||
},
|
||||
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: {
|
||||
title: 'Berufserfahrung',
|
||||
experience: [
|
||||
@@ -799,7 +1144,7 @@ export const translations: Record<string, Translation> = {
|
||||
fr: {
|
||||
metadata: {
|
||||
title: 'À propos de moi',
|
||||
aboutUs: 'À propos de nous',
|
||||
aboutUs: 'À propos de moi',
|
||||
},
|
||||
navigation: {
|
||||
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.'
|
||||
],
|
||||
},
|
||||
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: {
|
||||
title: 'Expérience professionnelle',
|
||||
experience: [
|
||||
|
@@ -12,6 +12,7 @@ import SiteVerification from '~/components/common/SiteVerification.astro';
|
||||
import Analytics from '~/components/common/Analytics.astro';
|
||||
import BasicScripts from '~/components/common/BasicScripts.astro';
|
||||
import StructuredData from '~/components/common/StructuredData.astro';
|
||||
import LanguagePersistence from '~/components/LanguagePersistence.astro';
|
||||
|
||||
// Comment the line below to disable View Transitions
|
||||
import { ClientRouter } from 'astro:transitions';
|
||||
@@ -59,5 +60,6 @@ const { language, textDirection } = I18N;
|
||||
<slot />
|
||||
|
||||
<BasicScripts />
|
||||
<LanguagePersistence />
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,28 +1,58 @@
|
||||
import { getPermalink, getAsset } from './utils/permalinks';
|
||||
import { getTranslation } from './i18n/translations';
|
||||
|
||||
export const headerData = {
|
||||
export const getHeaderData = (lang = 'en') => {
|
||||
const t = getTranslation(lang);
|
||||
|
||||
// For hash links on the homepage, we need special handling
|
||||
const homeHashLink = (hash) => {
|
||||
// Create an absolute path to the homepage with the language prefix
|
||||
// and then append the hash
|
||||
return getPermalink('/', 'page', lang) + hash;
|
||||
};
|
||||
|
||||
return {
|
||||
links: [
|
||||
{
|
||||
text: 'Home',
|
||||
href: getPermalink('/'),
|
||||
text: t.navigation.home,
|
||||
href: getPermalink('/', 'page', lang),
|
||||
},
|
||||
{ text: 'About', href: getPermalink('/en/#about')},
|
||||
{ text: 'Resume', href: getPermalink('/en/#resume') },
|
||||
{ text: 'Certifications', href: getPermalink('/en/#Certifications') },
|
||||
{ text: 'Skills', href: getPermalink('/en/#skills') },
|
||||
{ text: 'Blog', href: getPermalink('/blog') },
|
||||
{
|
||||
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();
|
||||
|
||||
export const getFooterData = (lang = 'en') => {
|
||||
return {
|
||||
secondaryLinks: [
|
||||
{ text: 'Terms', href: getPermalink('/terms') },
|
||||
{ text: 'Privacy Policy', href: getPermalink('/privacy') },
|
||||
{ text: 'Terms', href: getPermalink('/terms', 'page', lang) },
|
||||
{ text: 'Privacy Policy', href: getPermalink('/privacy', 'page', lang) },
|
||||
],
|
||||
socialLinks: [
|
||||
{ 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();
|
||||
|
141
src/pages/[lang]/aboutme.astro
Normal file
141
src/pages/[lang]/aboutme.astro
Normal 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>
|
@@ -1,24 +1,14 @@
|
||||
---
|
||||
export const prerender = false;
|
||||
import Layout from '~/layouts/PageLayout.astro';
|
||||
import Header from '~/components/widgets/Header.astro';
|
||||
import StructuredData from '~/components/common/StructuredData.astro';
|
||||
import Hero from '~/components/widgets/Hero.astro';
|
||||
import Hero from '~/components/widgets/Hero2.astro';
|
||||
import Features from '~/components/widgets/Features.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 BlogLatestPosts from '~/components/widgets/BlogLatestPosts.astro';
|
||||
import HomePageImage from '~/assets/images/richardbergsma.png'
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import Testimonials from '~/components/widgets/Testimonials.astro';
|
||||
import CallToAction from '~/components/widgets/CallToAction.astro';
|
||||
import Contact from '~/components/widgets/Contact.astro';
|
||||
import { getTranslation, supportedLanguages } from '~/i18n/translations';
|
||||
|
||||
interface IpApiResponse {
|
||||
countryCode: string;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return supportedLanguages.map(lang => ({
|
||||
params: { lang },
|
||||
@@ -30,181 +20,212 @@ if (!supportedLanguages.includes(lang)) {
|
||||
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 metadata = {
|
||||
title: 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"
|
||||
}
|
||||
}} />
|
||||
<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
|
||||
id="hero"
|
||||
tagline={t.navigation.home}
|
||||
title={t.hero.title}
|
||||
>
|
||||
<Fragment slot="subtitle">
|
||||
<strong class="text-3xl md:text-4xl">{t.hero.greeting}</strong><br /><br />{t.hero.subtitle}
|
||||
</Fragment>
|
||||
</Hero>
|
||||
subtitle={t.hero.subtitle}
|
||||
actions={[
|
||||
{
|
||||
variant: 'primary',
|
||||
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
|
||||
id="about"
|
||||
columns={2}
|
||||
items={[]}
|
||||
isReversed
|
||||
tagline={t.homepage?.approach?.tagline || "About My Approach"}
|
||||
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={{
|
||||
src: HomePageImage,
|
||||
alt: 'Richard Bergsma smiling in the mountains of Switzerland holding Revella',
|
||||
loading: 'lazy',
|
||||
src: '~/assets/images/hero-image.png',
|
||||
alt: 'Digital Transformation Image',
|
||||
}}
|
||||
>
|
||||
<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) => (
|
||||
<h3 class="text-2xl font-bold tracking-tight dark:text-white sm:text-3xl mb-2">
|
||||
{t.homepage?.approach?.missionTitle || "Mission Statement"}
|
||||
</h3>
|
||||
{(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 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',
|
||||
<!-- Testimonials Widget -->
|
||||
<Testimonials
|
||||
tagline={t.homepage?.testimonials?.tagline || "Testimonials"}
|
||||
title={t.homepage?.testimonials?.title || "What Clients Say About My Work"}
|
||||
testimonials={(t.homepage?.testimonials?.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',
|
||||
},
|
||||
]).map(item => ({
|
||||
...item,
|
||||
image: {
|
||||
src: '~/assets/images/default.png',
|
||||
alt: item.name,
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
|
||||
<!-- 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'
|
||||
}))}
|
||||
/>
|
||||
|
||||
<!-- BlogLatestPost Widget -->
|
||||
<BlogLatestPosts
|
||||
id="blog"
|
||||
title={t.blog.title}
|
||||
information={t.blog.information}
|
||||
<!-- CallToAction Widget -->
|
||||
<CallToAction
|
||||
callToAction={{
|
||||
text: t.homepage?.callToAction?.button || 'Contact Me',
|
||||
href: '#contact',
|
||||
icon: 'tabler:mail',
|
||||
}}
|
||||
>
|
||||
<Fragment slot="bg">
|
||||
<div class="absolute inset-0"></div>
|
||||
<Fragment slot="title">{t.homepage?.callToAction?.title || 'Ready to optimize your IT systems?'}</Fragment>
|
||||
<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>
|
||||
</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>
|
@@ -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
32
src/pages/aboutme.astro
Normal 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}`);
|
||||
---
|
@@ -1,77 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
---
|
||||
export const prerender = false;
|
||||
import { supportedLanguages } from '~/i18n/translations';
|
||||
|
||||
<script type="module">
|
||||
// Define the supported languages
|
||||
const supportedLangs = ['en', 'nl', 'de'];
|
||||
let chosenLang = 'nl'; // Default language
|
||||
// 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 language from the browser
|
||||
const userLang = navigator.language || (navigator.languages && navigator.languages[0]);
|
||||
// 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];
|
||||
|
||||
if (userLang) {
|
||||
// For example, "en-US" becomes "en"
|
||||
const preferredLang = userLang.split('-')[0];
|
||||
if (supportedLangs.includes(preferredLang)) {
|
||||
chosenLang = preferredLang;
|
||||
}
|
||||
}
|
||||
// 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';
|
||||
|
||||
// Construct the target URL based on the chosen language
|
||||
const targetURL = `/${chosenLang}`;
|
||||
console.log("Target URL:", targetURL);
|
||||
// Get the hash fragment if present
|
||||
const url = new URL(Astro.request.url);
|
||||
const hash = url.hash;
|
||||
|
||||
// Update the DOM with the computed language values:
|
||||
// Update the redirect message to display the chosen language.
|
||||
const messageEl = document.getElementById('redirect-message');
|
||||
if (messageEl) {
|
||||
messageEl.innerHTML = `You are being redirected to the <strong>${chosenLang.toUpperCase()}</strong> version of our site.`;
|
||||
}
|
||||
|
||||
// Update the href for the clickable link.
|
||||
const linkEl = document.getElementById('redirect-link');
|
||||
if (linkEl) {
|
||||
linkEl.href = targetURL;
|
||||
}
|
||||
|
||||
// Option 1: Dynamically add a meta refresh tag.
|
||||
const metaRefresh = document.createElement("meta");
|
||||
metaRefresh.httpEquiv = "refresh";
|
||||
metaRefresh.content = `0; url=${targetURL}`;
|
||||
document.head.appendChild(metaRefresh);
|
||||
|
||||
// Option 2: Alternatively, use JavaScript redirection:
|
||||
// window.location.href = targetURL;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
// Redirect to the language-specific homepage
|
||||
return Astro.redirect(`/${preferredLanguage}/${hash}`);
|
||||
---
|
@@ -10,7 +10,7 @@ const createPath = (...params: string[]) => {
|
||||
.map((el) => trimSlash(el))
|
||||
.filter((el) => !!el)
|
||||
.join('/');
|
||||
return '/' + paths + (SITE.trailingSlash && paths ? '/' : '');
|
||||
return '/' + paths + (SITE.trailingSlash && paths && !paths.includes('#') ? '/' : '');
|
||||
};
|
||||
|
||||
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 => {
|
||||
const url = String(new URL(path, SITE.site));
|
||||
let url = String(new URL(path, SITE.site));
|
||||
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('/')) {
|
||||
return url + '/';
|
||||
url = url + '/';
|
||||
}
|
||||
if (url.endsWith('/')) {
|
||||
url = url.slice(0, -1);
|
||||
}
|
||||
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;
|
||||
|
||||
if (
|
||||
slug.startsWith('https://') ||
|
||||
slug.startsWith('http://') ||
|
||||
slug.startsWith('://') ||
|
||||
slug.startsWith('#') ||
|
||||
slug.startsWith('javascript:')
|
||||
) {
|
||||
return slug;
|
||||
}
|
||||
|
||||
// Extract hash fragment if present
|
||||
let hashFragment = '';
|
||||
if (slug.includes('#')) {
|
||||
const parts = slug.split('#');
|
||||
slug = parts[0];
|
||||
hashFragment = '#' + parts[1];
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'home':
|
||||
permalink = getHomePermalink();
|
||||
permalink = getHomePermalink(lang);
|
||||
break;
|
||||
|
||||
case 'blog':
|
||||
permalink = getBlogPermalink();
|
||||
permalink = getBlogPermalink(lang);
|
||||
break;
|
||||
|
||||
case 'asset':
|
||||
@@ -83,14 +97,15 @@ export const getPermalink = (slug = '', type = 'page'): string => {
|
||||
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 =>
|
||||
@@ -101,7 +116,22 @@ export const getAsset = (path: string): string =>
|
||||
.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 = {}) => {
|
||||
|
Reference in New Issue
Block a user