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

View File

@@ -19,7 +19,7 @@
"hero": {
"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.",
"trusted": "Vertraut von Unternehmen in ganz Italien",
"trusted": "Vertraut von Unternehmen in ganz den Niederlanden",
"cta": {
"primary": "Heute starten",
"secondary": "Unsere Services ansehen"
@@ -123,7 +123,7 @@
"info": {
"email": "info@tiber365.it",
"phone": "+39 123 456 7890",
"address": "Rom, Italien"
"address": "Amsterdam, Niederlande"
},
"form": {
"name": "Name",

View File

@@ -19,7 +19,7 @@
"hero": {
"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.",
"trusted": "Trusted by businesses across Italy",
"trusted": "Trusted by businesses across the Netherlands",
"cta": {
"primary": "Get Started Today",
"secondary": "View Our Services"
@@ -123,7 +123,7 @@
"info": {
"email": "info@tiber365.it",
"phone": "+39 123 456 7890",
"address": "Rome, Italy"
"address": "Amsterdam, Netherlands"
},
"form": {
"name": "Name",

View File

@@ -19,7 +19,7 @@
"hero": {
"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.",
"trusted": "Fait confiance par les entreprises à travers l'Italie",
"trusted": "Fait confiance par les entreprises à travers les Pays-Bas",
"cta": {
"primary": "Commencer Aujourd'hui",
"secondary": "Voir Nos Services"
@@ -123,7 +123,7 @@
"info": {
"email": "info@tiber365.it",
"phone": "+39 123 456 7890",
"address": "Rome, Italie"
"address": "Amsterdam, Pays-Bas"
},
"form": {
"name": "Nom",

View File

@@ -23,7 +23,7 @@
"hero": {
"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.",
"trusted": "Vertrouwd door bedrijven in heel Italië",
"trusted": "Vertrouwd door bedrijven in heel Nederland",
"cta": {
"primary": "Begin Vandaag",
"secondary": "Bekijk Onze Diensten"
@@ -127,7 +127,7 @@
"info": {
"email": "info@tiber365.it",
"phone": "+39 123 456 7890",
"address": "Rome, Italië"
"address": "Amsterdam, Nederland"
},
"form": {
"name": "Naam",

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ const pageStructuredData = {
},
"serviceArea": {
"@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() {
// Get all blog posts to create redirects
const { getBlogPosts } = await import('../../utils/directus');
const posts = await getBlogPosts();
return posts
.filter((post) => typeof post.slug === 'string' && post.slug.trim() !== '')
.map((post) => ({
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>
<head>
<meta charset="utf-8">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0;url=/en/blog/{slug}">
<link rel="canonical" href="/en/blog/{slug}">
</head>
<body>
<p>Redirecting to <a href="/en/blog/{slug}">blog post</a>...</p>
</body>
</html>
<BaseLayout title={`${post.title} | ${t('blog.title')}`} description={post.content.replace(/<[^>]+>/g, '').substring(0, 160)}>
<Header />
<main class="flex-1">
<!-- Hero Section -->
<section class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 py-20">
<div class="container-custom">
<div class="max-w-4xl mx-auto">
<nav class="mb-6">
<a href="/en/blog" class="text-primary hover:underline flex items-center">
← {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>
<head>
<meta charset="utf-8">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0;url=/en/blog">
<link rel="canonical" href="/en/blog">
</head>
<body>
<p>Redirecting to <a href="/en/blog">blog</a>...</p>
</body>
</html>
<BaseLayout title={t('blog.title')} description={t('blog.description')}>
<Header />
<main class="flex-1">
<!-- Hero Section -->
<section class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 py-20">
<div class="container-custom">
<div class="text-center max-w-3xl mx-auto">
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
{t('blog.title')}
</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
return Astro.redirect('/en/');
---
import BaseLayout from '../layouts/BaseLayout.astro';
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 utilities;
/* CSS Variables for theming */
:root {
--color-background: 255 255 255;
--color-foreground: 15 23 42;
--color-primary: 59 130 246;
--color-primary-foreground: 255 255 255;
--color-secondary: 100 116 139;
--color-secondary-foreground: 255 255 255;
--color-background: 255 255 255;
--color-foreground: 15 23 42;
--color-muted: 248 250 252;
--color-muted-foreground: 100 116 139;
--color-border: 226 232 240;
@@ -41,7 +42,7 @@
h1, h2, h3, h4, h5, h6 {
@apply font-display;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
@@ -65,7 +66,7 @@
background-color: rgb(var(--color-secondary));
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgb(var(--color-secondary) / 0.8);
}
@@ -75,34 +76,31 @@
.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;
}
.btn-primary {
@apply btn;
background-color: rgb(var(--color-primary));
color: rgb(var(--color-primary-foreground));
}
.btn-primary:hover {
background-color: rgb(var(--color-primary) / 0.9);
}
.btn-secondary {
@apply btn;
background-color: rgb(var(--color-secondary));
color: rgb(var(--color-secondary-foreground));
}
.btn-secondary:hover {
background-color: rgb(var(--color-secondary) / 0.8);
}
.btn-outline {
@apply btn;
border: 1px solid rgb(var(--color-border));
background-color: transparent;
color: rgb(var(--color-foreground));
}
.btn-outline:hover {
background-color: rgb(var(--color-accent));
}
@@ -118,11 +116,11 @@
.bg-background {
background-color: rgb(var(--color-background));
}
.text-foreground {
color: rgb(var(--color-foreground));
}
.text-muted-foreground {
color: rgb(var(--color-muted-foreground));
}
@@ -130,7 +128,7 @@
.bg-muted {
background-color: rgb(var(--color-muted));
}
.bg-primary {
background-color: rgb(var(--color-primary));
}
@@ -138,15 +136,15 @@
.text-primary {
color: rgb(var(--color-primary));
}
.text-primary-foreground {
color: rgb(var(--color-primary-foreground));
}
.bg-accent {
background-color: rgb(var(--color-accent));
}
.border-border {
border-color: rgb(var(--color-border));
}
@@ -158,9 +156,57 @@
transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.animate-on-scroll.in-view {
opacity: 1;
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
}
});
}