Update site content and structure, including localization adjustments for addresses, removal of unused files, and enhancements to the layout and styling for better user experience.

This commit is contained in:
2025-07-24 19:18:12 +02:00
parent 37a6e0ab31
commit 32301a18e9
60 changed files with 667 additions and 229 deletions

File diff suppressed because one or more lines are too long

4
dist/404.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{i as e}from"./theme.BcH1Etvo.js";document.addEventListener("DOMContentLoaded",()=>{e(),function(){if("undefined"==typeof window)return;const e=new IntersectionObserver(e=>{e.forEach(e=>{e.isIntersecting&&e.target.classList.add("in-view")})},{threshold:.1,rootMargin:"0px 0px -50px 0px"});document.querySelectorAll(".animate-on-scroll").forEach(o=>e.observe(o))}(),function(){"PerformanceObserver"in window&&(new PerformanceObserver(e=>{const o=e.getEntries(),n=o[o.length-1];console.log("LCP:",n.startTime),n.startTime<2500?console.log("✅ LCP is good"):console.log("⚠️ LCP needs improvement")}).observe({entryTypes:["largest-contentful-paint"]}),new PerformanceObserver(e=>{e.getEntries().forEach(e=>{const o=e;console.log("FID:",o.processingStart-o.startTime),o.processingStart-o.startTime<100?console.log("✅ FID is good"):console.log("⚠️ FID needs improvement")})}).observe({entryTypes:["first-input"]}),new PerformanceObserver(e=>{let o=0;e.getEntries().forEach(e=>{e.hadRecentInput||(o+=e.value)}),console.log("CLS:",o),o<.1?console.log("✅ CLS is good"):console.log("⚠️ CLS needs improvement")}).observe({entryTypes:["layout-shift"]}));window.addEventListener("load",()=>{const e=performance.now();console.log("Page load time:",e);const o=performance.getEntriesByType("navigation")[0];o&&(console.log("DOM Content Loaded:",o.domContentLoadedEventEnd-o.domContentLoadedEventStart),console.log("Load Complete:",o.loadEventEnd-o.loadEventStart))})}(),"serviceWorker"in navigator&&navigator.serviceWorker.register("/sw.js").then(e=>{console.log("SW registered: ",e)}).catch(e=>{console.log("SW registration failed: ",e)})});

View File

@@ -0,0 +1 @@
import{i as e}from"./theme.BcH1Etvo.js";document.addEventListener("DOMContentLoaded",()=>{e(),function(){if("undefined"==typeof window)return;const e=new IntersectionObserver(e=>{e.forEach(e=>{e.isIntersecting&&e.target.classList.add("in-view")})},{threshold:.1,rootMargin:"0px 0px -50px 0px"});document.querySelectorAll(".animate-on-scroll").forEach(t=>e.observe(t))}(),function(){"PerformanceObserver"in window&&(new PerformanceObserver(e=>{const t=e.getEntries(),o=t[t.length-1];console.log("LCP:",o.startTime),o.startTime<2500?console.log("✅ LCP is good"):console.log("⚠️ LCP needs improvement")}).observe({entryTypes:["largest-contentful-paint"]}),new PerformanceObserver(e=>{e.getEntries().forEach(e=>{const t=e;console.log("FID:",t.processingStart-t.startTime),t.processingStart-t.startTime<100?console.log("✅ FID is good"):console.log("⚠️ FID needs improvement")})}).observe({entryTypes:["first-input"]}),new PerformanceObserver(e=>{let t=0;e.getEntries().forEach(e=>{e.hadRecentInput||(t+=e.value)}),console.log("CLS:",t),t<.1?console.log("✅ CLS is good"):console.log("⚠️ CLS needs improvement")}).observe({entryTypes:["layout-shift"]}));window.addEventListener("load",()=>{const e=performance.now();console.log("Page load time:",e);const t=performance.getEntriesByType("navigation")[0];t&&(console.log("DOM Content Loaded:",t.domContentLoadedEventEnd-t.domContentLoadedEventStart),console.log("Load Complete:",t.loadEventEnd-t.loadEventStart))})}(),function(){const e=new Set;function t(t){if(!e.has(t))try{const o=document.createElement("link");o.rel="prefetch",o.href=t,o.as="document",document.head.appendChild(o),e.add(t),console.log(`Preloaded: ${t}`)}catch(o){console.warn(`Failed to preload ${t}:`,o)}}function o(e){const o=e.target.closest("a");if(!o)return;const n=o.getAttribute("href");if(!n)return;if(n.startsWith("http")||n.startsWith("mailto:")||n.startsWith("tel:")||n.startsWith("#"))return;let r;try{r=new URL(n,window.location.origin).href}catch{return}r!==window.location.href&&setTimeout(()=>{t(r)},100)}function n(){document.addEventListener("mouseenter",o,{capture:!0,passive:!0}),document.addEventListener("touchstart",o,{capture:!0,passive:!0})}n(),new MutationObserver(e=>{e.forEach(e=>{"childList"===e.type&&e.addedNodes.length>0&&n()})}).observe(document.body,{childList:!0,subtree:!0})}(),"serviceWorker"in navigator&&navigator.serviceWorker.register("/sw.js").then(e=>{console.log("SW registered: ",e)}).catch(e=>{console.log("SW registration failed: ",e)})});

File diff suppressed because one or more lines are too long

1
dist/_astro/about.DJBbvL2M.css vendored Normal file

File diff suppressed because one or more lines are too long

24
dist/_headers vendored
View File

@@ -1,24 +0,0 @@
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://fonts.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://support.tiber365.it; frame-ancestors 'none';
/*
Cache-Control: public, max-age=31536000, immutable
/sw.js
Cache-Control: public, max-age=0, must-revalidate
/manifest.json
Cache-Control: public, max-age=31536000, immutable
/favicon.svg
Cache-Control: public, max-age=31536000, immutable
/images/*
Cache-Control: public, max-age=31536000, immutable
/sitemap.xml
Cache-Control: public, max-age=3600

48
dist/_redirects vendored
View File

@@ -1,7 +1,41 @@
# Redirect language routes to root # Security headers for all pages
/en/* / 301 /*
/nl/* / 301 X-Frame-Options: DENY
/it/* / 301 X-Content-Type-Options: nosniff
/en / 301 Referrer-Policy: strict-origin-when-cross-origin
/nl / 301 Permissions-Policy: camera=(), microphone=(), geolocation=()
/it / 301 Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://fonts.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://support.tiber365.it; frame-ancestors 'none';
# Cache control for static assets
/*.js
Cache-Control: public, max-age=31536000, immutable
/*.css
Cache-Control: public, max-age=31536000, immutable
/*.svg
Cache-Control: public, max-age=31536000, immutable
/*.png
Cache-Control: public, max-age=31536000, immutable
/*.jpg
Cache-Control: public, max-age=31536000, immutable
/*.ico
Cache-Control: public, max-age=31536000, immutable
/sw.js
Cache-Control: public, max-age=0, must-revalidate
/manifest.json
Cache-Control: public, max-age=31536000, immutable
/favicon.svg
Cache-Control: public, max-age=31536000, immutable
/images/*
Cache-Control: public, max-age=31536000, immutable
/sitemap.xml
Cache-Control: public, max-age=3600

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
dist/de/index.html vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
dist/en/index.html vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
dist/fr/index.html vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

25
dist/index.html vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
dist/nl/index.html vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,19 +7,19 @@
"astro > cssesc": { "astro > cssesc": {
"src": "../../cssesc/cssesc.js", "src": "../../cssesc/cssesc.js",
"file": "astro___cssesc.js", "file": "astro___cssesc.js",
"fileHash": "adbc09e7", "fileHash": "3fe2d173",
"needsInterop": true "needsInterop": true
}, },
"astro > aria-query": { "astro > aria-query": {
"src": "../../aria-query/lib/index.js", "src": "../../aria-query/lib/index.js",
"file": "astro___aria-query.js", "file": "astro___aria-query.js",
"fileHash": "dec3cd63", "fileHash": "5253c813",
"needsInterop": true "needsInterop": true
}, },
"astro > axobject-query": { "astro > axobject-query": {
"src": "../../axobject-query/lib/index.js", "src": "../../axobject-query/lib/index.js",
"file": "astro___axobject-query.js", "file": "astro___axobject-query.js",
"fileHash": "a84c99eb", "fileHash": "355d5bcf",
"needsInterop": true "needsInterop": true
} }
}, },

View File

@@ -1,24 +0,0 @@
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://fonts.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://support.tiber365.it; frame-ancestors 'none';
/*
Cache-Control: public, max-age=31536000, immutable
/sw.js
Cache-Control: public, max-age=0, must-revalidate
/manifest.json
Cache-Control: public, max-age=31536000, immutable
/favicon.svg
Cache-Control: public, max-age=31536000, immutable
/images/*
Cache-Control: public, max-age=31536000, immutable
/sitemap.xml
Cache-Control: public, max-age=3600

View File

@@ -1,7 +1,41 @@
# Redirect language routes to root # Security headers for all pages
/en/* / 301 /*
/nl/* / 301 X-Frame-Options: DENY
/it/* / 301 X-Content-Type-Options: nosniff
/en / 301 Referrer-Policy: strict-origin-when-cross-origin
/nl / 301 Permissions-Policy: camera=(), microphone=(), geolocation=()
/it / 301 Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://fonts.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://support.tiber365.it; frame-ancestors 'none';
# Cache control for static assets
/*.js
Cache-Control: public, max-age=31536000, immutable
/*.css
Cache-Control: public, max-age=31536000, immutable
/*.svg
Cache-Control: public, max-age=31536000, immutable
/*.png
Cache-Control: public, max-age=31536000, immutable
/*.jpg
Cache-Control: public, max-age=31536000, immutable
/*.ico
Cache-Control: public, max-age=31536000, immutable
/sw.js
Cache-Control: public, max-age=0, must-revalidate
/manifest.json
Cache-Control: public, max-age=31536000, immutable
/favicon.svg
Cache-Control: public, max-age=31536000, immutable
/images/*
Cache-Control: public, max-age=31536000, immutable
/sitemap.xml
Cache-Control: public, max-age=3600

View File

@@ -19,7 +19,7 @@
"hero": { "hero": {
"title": "Professionelle IT-Services für Ihr Unternehmen", "title": "Professionelle IT-Services für Ihr Unternehmen",
"subtitle": "Wir unterstützen Freelancer und kleine Unternehmen mit zuverlässigem Microsoft 365 Support, Netzwerklösungen, Webhosting und maßgeschneiderten IT-Projekten.", "subtitle": "Wir unterstützen Freelancer und kleine Unternehmen mit zuverlässigem Microsoft 365 Support, Netzwerklösungen, Webhosting und maßgeschneiderten IT-Projekten.",
"trusted": "Vertraut von Unternehmen in ganz Italien", "trusted": "Vertraut von Unternehmen in ganz den Niederlanden",
"cta": { "cta": {
"primary": "Heute starten", "primary": "Heute starten",
"secondary": "Unsere Services ansehen" "secondary": "Unsere Services ansehen"
@@ -123,7 +123,7 @@
"info": { "info": {
"email": "info@tiber365.it", "email": "info@tiber365.it",
"phone": "+39 123 456 7890", "phone": "+39 123 456 7890",
"address": "Rom, Italien" "address": "Amsterdam, Niederlande"
}, },
"form": { "form": {
"name": "Name", "name": "Name",

View File

@@ -19,7 +19,7 @@
"hero": { "hero": {
"title": "Professional IT Services for Your Business", "title": "Professional IT Services for Your Business",
"subtitle": "Empowering freelancers and small businesses with reliable Microsoft 365 support, networking solutions, web hosting, and custom IT projects.", "subtitle": "Empowering freelancers and small businesses with reliable Microsoft 365 support, networking solutions, web hosting, and custom IT projects.",
"trusted": "Trusted by businesses across Italy", "trusted": "Trusted by businesses across the Netherlands",
"cta": { "cta": {
"primary": "Get Started Today", "primary": "Get Started Today",
"secondary": "View Our Services" "secondary": "View Our Services"
@@ -123,7 +123,7 @@
"info": { "info": {
"email": "info@tiber365.it", "email": "info@tiber365.it",
"phone": "+39 123 456 7890", "phone": "+39 123 456 7890",
"address": "Rome, Italy" "address": "Amsterdam, Netherlands"
}, },
"form": { "form": {
"name": "Name", "name": "Name",

View File

@@ -19,7 +19,7 @@
"hero": { "hero": {
"title": "Services IT Professionnels pour Votre Entreprise", "title": "Services IT Professionnels pour Votre Entreprise",
"subtitle": "Nous aidons les freelances et petites entreprises avec un support Microsoft 365 fiable, des solutions réseau, de l'hébergement web et des projets IT personnalisés.", "subtitle": "Nous aidons les freelances et petites entreprises avec un support Microsoft 365 fiable, des solutions réseau, de l'hébergement web et des projets IT personnalisés.",
"trusted": "Fait confiance par les entreprises à travers l'Italie", "trusted": "Fait confiance par les entreprises à travers les Pays-Bas",
"cta": { "cta": {
"primary": "Commencer Aujourd'hui", "primary": "Commencer Aujourd'hui",
"secondary": "Voir Nos Services" "secondary": "Voir Nos Services"
@@ -123,7 +123,7 @@
"info": { "info": {
"email": "info@tiber365.it", "email": "info@tiber365.it",
"phone": "+39 123 456 7890", "phone": "+39 123 456 7890",
"address": "Rome, Italie" "address": "Amsterdam, Pays-Bas"
}, },
"form": { "form": {
"name": "Nom", "name": "Nom",

View File

@@ -23,7 +23,7 @@
"hero": { "hero": {
"title": "Professionele IT Services voor Uw Bedrijf", "title": "Professionele IT Services voor Uw Bedrijf",
"subtitle": "Ondersteuning van freelancers en kleine bedrijven met betrouwbare Microsoft 365 ondersteuning, netwerkoplossingen, webhosting en aangepaste IT-projecten.", "subtitle": "Ondersteuning van freelancers en kleine bedrijven met betrouwbare Microsoft 365 ondersteuning, netwerkoplossingen, webhosting en aangepaste IT-projecten.",
"trusted": "Vertrouwd door bedrijven in heel Italië", "trusted": "Vertrouwd door bedrijven in heel Nederland",
"cta": { "cta": {
"primary": "Begin Vandaag", "primary": "Begin Vandaag",
"secondary": "Bekijk Onze Diensten" "secondary": "Bekijk Onze Diensten"
@@ -127,7 +127,7 @@
"info": { "info": {
"email": "info@tiber365.it", "email": "info@tiber365.it",
"phone": "+39 123 456 7890", "phone": "+39 123 456 7890",
"address": "Rome, Italië" "address": "Amsterdam, Nederland"
}, },
"form": { "form": {
"name": "Naam", "name": "Naam",

View File

@@ -65,11 +65,11 @@ const structuredData = {
}, },
"address": { "address": {
"@type": "PostalAddress", "@type": "PostalAddress",
"addressCountry": "IT" "addressCountry": "NL"
}, },
"serviceArea": { "serviceArea": {
"@type": "Country", "@type": "Country",
"name": "Italy" "name": "Netherlands"
} }
}; };
--- ---
@@ -163,11 +163,13 @@ const structuredData = {
import { initScrollAnimations } from '../utils/animations'; import { initScrollAnimations } from '../utils/animations';
import { initTheme } from '../utils/theme'; import { initTheme } from '../utils/theme';
import { initPerformanceMonitoring } from '../utils/performance'; import { initPerformanceMonitoring } from '../utils/performance';
import { initPreloading } from '../utils/preload';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initTheme(); initTheme();
initScrollAnimations(); initScrollAnimations();
initPerformanceMonitoring(); initPerformanceMonitoring();
initPreloading();
// Register service worker // Register service worker
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {

View File

@@ -136,7 +136,7 @@
"info": { "info": {
"email": "info@tiber365.it", "email": "info@tiber365.it",
"phone": "+39 123 456 7890", "phone": "+39 123 456 7890",
"address": "Italy" "address": "Netherlands"
} }
}, },
"cta": { "cta": {

View File

@@ -41,7 +41,7 @@ const pageStructuredData = {
}, },
"serviceArea": { "serviceArea": {
"@type": "Country", "@type": "Country",
"name": "Italy" "name": "Netherlands"
} }
} }
}; };

View File

@@ -1,28 +1,70 @@
--- ---
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
import { getBlogPostBySlug, getBlogPosts } from '../../utils/directus';
import { useTranslations } from '../../utils/i18n';
export async function getStaticPaths() { export async function getStaticPaths() {
// Get all blog posts to create redirects
const { getBlogPosts } = await import('../../utils/directus');
const posts = await getBlogPosts(); const posts = await getBlogPosts();
return posts return posts
.filter((post) => typeof post.slug === 'string' && post.slug.trim() !== '') .filter((post) => typeof post.slug === 'string' && post.slug.trim() !== '')
.map((post) => ({ .map((post) => ({
params: { slug: post.slug }, params: { slug: post.slug },
props: { slug: post.slug }, props: { post },
})); }));
} }
const { slug } = Astro.props; const { post } = Astro.props;
const t = await useTranslations('en');
--- ---
<!DOCTYPE html>
<html> <BaseLayout title={`${post.title} | ${t('blog.title')}`} description={post.content.replace(/<[^>]+>/g, '').substring(0, 160)}>
<head> <Header />
<meta charset="utf-8">
<title>Redirecting...</title> <main class="flex-1">
<meta http-equiv="refresh" content="0;url=/en/blog/{slug}"> <!-- Hero Section -->
<link rel="canonical" href="/en/blog/{slug}"> <section class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 py-20">
</head> <div class="container-custom">
<body> <div class="max-w-4xl mx-auto">
<p>Redirecting to <a href="/en/blog/{slug}">blog post</a>...</p> <nav class="mb-6">
</body> <a href="/en/blog" class="text-primary hover:underline flex items-center">
</html> ← {t('blog.backToBlog')}
</a>
</nav>
<header class="text-center">
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
{post.title}
</h1>
<p class="text-xl text-gray-600 dark:text-gray-300">
{new Date(post.date_created).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
</header>
</div>
</div>
</section>
<!-- Article Content -->
<section class="py-16">
<div class="container-custom">
<article class="max-w-4xl mx-auto">
<div class="prose prose-lg dark:prose-invert max-w-none prose-headings:text-gray-900 dark:prose-headings:text-white prose-p:text-gray-700 dark:prose-p:text-gray-300 prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-strong:text-gray-900 dark:prose-strong:text-white prose-code:text-primary prose-code:bg-gray-100 dark:prose-code:bg-gray-800 prose-code:px-1 prose-code:py-0.5 prose-code:rounded" set:html={post.content}></div>
<!-- Back to Blog Button -->
<div class="mt-12 pt-8 border-t border-border text-center">
<a href="/en/blog" class="btn btn-primary">
← {t('blog.backToBlog')}
</a>
</div>
</article>
</div>
</section>
</main>
<Footer />
</BaseLayout>

View File

@@ -1,15 +1,97 @@
--- ---
// Static redirect to English blog import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
import { getBlogPosts } from '../../utils/directus';
import { useTranslations } from '../../utils/i18n';
const t = await useTranslations('en');
let posts = [];
let error = null;
function stripHtml(html) {
return html.replace(/<[^>]+>/g, '');
}
try {
posts = await getBlogPosts();
} catch (e) {
console.error('Error in blog page:', e);
error = e.message;
}
--- ---
<!DOCTYPE html>
<html> <BaseLayout title={t('blog.title')} description={t('blog.description')}>
<head> <Header />
<meta charset="utf-8">
<title>Redirecting...</title> <main class="flex-1">
<meta http-equiv="refresh" content="0;url=/en/blog"> <!-- Hero Section -->
<link rel="canonical" href="/en/blog"> <section class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 py-20">
</head> <div class="container-custom">
<body> <div class="text-center max-w-3xl mx-auto">
<p>Redirecting to <a href="/en/blog">blog</a>...</p> <h1 class="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
</body> {t('blog.title')}
</html> </h1>
<p class="text-xl text-gray-600 dark:text-gray-300">
{t('blog.description')}
</p>
</div>
</div>
</section>
<!-- Blog Posts Section -->
<section class="py-16">
<div class="container-custom">
{error ? (
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6 mb-8">
<p class="text-red-800 dark:text-red-200">
{t('blog.error')}
</p>
{import.meta.env.DEV && (
<pre class="mt-2 text-sm text-red-600 dark:text-red-400">{error}</pre>
)}
</div>
) : posts.length === 0 ? (
<div class="text-center py-12">
<p class="text-gray-600 dark:text-gray-400 text-lg">
{t('blog.noPosts')}
</p>
</div>
) : (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{posts.map((post) => (
<article class="card hover:shadow-lg transition-shadow duration-300 group">
<div class="flex flex-col h-full">
<div class="flex-1">
<h2 class="text-xl font-bold mb-3 group-hover:text-primary transition-colors">
<a href={`/en/blog/${post.slug}`} class="hover:underline">
{post.title}
</a>
</h2>
<p class="text-muted-foreground text-sm mb-4">
{new Date(post.date_created).toLocaleDateString('en-US')}
</p>
<div class="text-foreground text-sm mb-6 line-clamp-4">
{stripHtml(post.content).substring(0, 200)}{stripHtml(post.content).length > 200 ? '...' : ''}
</div>
</div>
<div class="pt-4 border-t border-border">
<a
href={`/en/blog/${post.slug}`}
class="inline-flex items-center text-primary font-medium hover:underline transition-colors"
>
{t('blog.readMore')} →
</a>
</div>
</div>
</article>
))}
</div>
)}
</div>
</section>
</main>
<Footer />
</BaseLayout>

View File

@@ -1,4 +1,25 @@
--- ---
// Redirect to English version import BaseLayout from '../layouts/BaseLayout.astro';
return Astro.redirect('/en/'); import Header from '../components/Header.astro';
--- import Footer from '../components/Footer.astro';
import Hero from '../components/Hero.astro';
import Services from '../components/Services.astro';
import Testimonials from '../components/Testimonials.astro';
import CTA from '../components/CTA.astro';
import { useTranslations } from '../utils/i18n';
const t = await useTranslations('en');
---
<BaseLayout title={t('home.title')} description={t('home.description')}>
<Header />
<main class="flex-1">
<Hero />
<Services />
<Testimonials />
<CTA />
</main>
<Footer />
</BaseLayout>

View File

@@ -2,13 +2,14 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* CSS Variables for theming */
:root { :root {
--color-background: 255 255 255;
--color-foreground: 15 23 42;
--color-primary: 59 130 246; --color-primary: 59 130 246;
--color-primary-foreground: 255 255 255; --color-primary-foreground: 255 255 255;
--color-secondary: 100 116 139; --color-secondary: 100 116 139;
--color-secondary-foreground: 255 255 255; --color-secondary-foreground: 255 255 255;
--color-background: 255 255 255;
--color-foreground: 15 23 42;
--color-muted: 248 250 252; --color-muted: 248 250 252;
--color-muted-foreground: 100 116 139; --color-muted-foreground: 100 116 139;
--color-border: 226 232 240; --color-border: 226 232 240;
@@ -41,7 +42,7 @@
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
@apply font-display; @apply font-display;
} }
/* Smooth scrolling */ /* Smooth scrolling */
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
@@ -65,7 +66,7 @@
background-color: rgb(var(--color-secondary)); background-color: rgb(var(--color-secondary));
border-radius: 9999px; border-radius: 9999px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background-color: rgb(var(--color-secondary) / 0.8); background-color: rgb(var(--color-secondary) / 0.8);
} }
@@ -75,34 +76,31 @@
.btn { .btn {
@apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50; @apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50;
} }
.btn-primary { .btn-primary {
@apply btn;
background-color: rgb(var(--color-primary)); background-color: rgb(var(--color-primary));
color: rgb(var(--color-primary-foreground)); color: rgb(var(--color-primary-foreground));
} }
.btn-primary:hover { .btn-primary:hover {
background-color: rgb(var(--color-primary) / 0.9); background-color: rgb(var(--color-primary) / 0.9);
} }
.btn-secondary { .btn-secondary {
@apply btn;
background-color: rgb(var(--color-secondary)); background-color: rgb(var(--color-secondary));
color: rgb(var(--color-secondary-foreground)); color: rgb(var(--color-secondary-foreground));
} }
.btn-secondary:hover { .btn-secondary:hover {
background-color: rgb(var(--color-secondary) / 0.8); background-color: rgb(var(--color-secondary) / 0.8);
} }
.btn-outline { .btn-outline {
@apply btn;
border: 1px solid rgb(var(--color-border)); border: 1px solid rgb(var(--color-border));
background-color: transparent; background-color: transparent;
color: rgb(var(--color-foreground)); color: rgb(var(--color-foreground));
} }
.btn-outline:hover { .btn-outline:hover {
background-color: rgb(var(--color-accent)); background-color: rgb(var(--color-accent));
} }
@@ -118,11 +116,11 @@
.bg-background { .bg-background {
background-color: rgb(var(--color-background)); background-color: rgb(var(--color-background));
} }
.text-foreground { .text-foreground {
color: rgb(var(--color-foreground)); color: rgb(var(--color-foreground));
} }
.text-muted-foreground { .text-muted-foreground {
color: rgb(var(--color-muted-foreground)); color: rgb(var(--color-muted-foreground));
} }
@@ -130,7 +128,7 @@
.bg-muted { .bg-muted {
background-color: rgb(var(--color-muted)); background-color: rgb(var(--color-muted));
} }
.bg-primary { .bg-primary {
background-color: rgb(var(--color-primary)); background-color: rgb(var(--color-primary));
} }
@@ -138,15 +136,15 @@
.text-primary { .text-primary {
color: rgb(var(--color-primary)); color: rgb(var(--color-primary));
} }
.text-primary-foreground { .text-primary-foreground {
color: rgb(var(--color-primary-foreground)); color: rgb(var(--color-primary-foreground));
} }
.bg-accent { .bg-accent {
background-color: rgb(var(--color-accent)); background-color: rgb(var(--color-accent));
} }
.border-border { .border-border {
border-color: rgb(var(--color-border)); border-color: rgb(var(--color-border));
} }
@@ -158,9 +156,57 @@
transform: translateY(20px); transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out; transition: opacity 0.6s ease-out, transform 0.6s ease-out;
} }
.animate-on-scroll.in-view { .animate-on-scroll.in-view {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
/* Preloading styles */
.link-preloading {
position: relative;
transition: all 0.2s ease;
}
.link-preloading::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background-color: rgb(var(--color-primary));
transition: width 0.3s ease;
}
.link-preloading:hover::after {
width: 100%;
}
/* Preload indicator */
.preload-indicator {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, rgb(var(--color-primary)), transparent);
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 9999;
pointer-events: none;
}
.preload-indicator.active {
transform: translateX(100%);
}
/* Smooth transitions for page changes */
.page-transition {
transition: opacity 0.2s ease-in-out;
}
.page-transition.loading {
opacity: 0.7;
}
} }

133
src/utils/preload.ts Normal file
View File

@@ -0,0 +1,133 @@
// Preload utility for improving perceived performance
export function initPreloading() {
// Track preloaded URLs to avoid duplicate requests
const preloadedUrls = new Set<string>();
// Function to preload a URL
function preloadUrl(url: string) {
if (preloadedUrls.has(url)) return;
try {
// Create a link element for preloading
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
link.as = 'document';
// Add to head
document.head.appendChild(link);
// Mark as preloaded
preloadedUrls.add(url);
console.log(`Preloaded: ${url}`);
} catch (error) {
console.warn(`Failed to preload ${url}:`, error);
}
}
// Function to handle link hover
function handleLinkHover(event: Event) {
const target = event.target as HTMLElement;
const link = target.closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href) return;
// Skip external links, anchors, and special protocols
if (href.startsWith('http') || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('#')) {
return;
}
// Convert relative URLs to absolute
let url: string;
try {
url = new URL(href, window.location.origin).href;
} catch {
return;
}
// Skip if it's the current page
if (url === window.location.href) return;
// Preload with a small delay to avoid preloading on accidental hovers
setTimeout(() => {
preloadUrl(url);
}, 100);
}
// Add event listeners to all links
function addPreloadListeners() {
// Use event delegation for better performance
document.addEventListener('mouseenter', handleLinkHover, {
capture: true,
passive: true
});
// Also preload on touchstart for mobile devices
document.addEventListener('touchstart', handleLinkHover, {
capture: true,
passive: true
});
}
// Initialize preloading
addPreloadListeners();
// Re-add listeners when new content is loaded (for SPA-like behavior)
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// New content added, ensure listeners are active
addPreloadListeners();
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
return {
preloadUrl,
preloadedUrls
};
}
// Preload specific important pages immediately
export function preloadCriticalPages() {
const criticalPages = [
'/en/',
'/nl/',
'/de/',
'/fr/',
'/en/about',
'/nl/about',
'/de/about',
'/fr/about',
'/en/contact',
'/nl/contact',
'/de/contact',
'/fr/contact',
'/en/blog',
'/nl/blog',
'/de/blog',
'/fr/blog'
];
criticalPages.forEach(page => {
const url = new URL(page, window.location.origin).href;
if (url !== window.location.href) {
setTimeout(() => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
link.as = 'document';
document.head.appendChild(link);
}, 1000); // Delay to not interfere with initial page load
}
});
}