feat: SEO, PWA, and performance optimizations

- Add sitemap.xml endpoint and update robots.txt for SEO - Improve
manifest.json with richer metadata and categories - Add meta tags for
social sharing and accessibility - Preload critical assets and fonts for
faster loading - Optimize login background image and resource hints -
Enhance service worker for better caching strategies - Add security
headers to server responses - Update Vite config for chunking and
dependency optimization - Add logboek.md for project tracking
This commit is contained in:
2025-10-07 14:26:41 +02:00
parent 5f0cae604d
commit 716c05c256
15 changed files with 458 additions and 17 deletions

View File

@@ -15,33 +15,70 @@ import { build, files, version } from '$service-worker';
// This gives `self` the correct types
const self = globalThis.self as unknown as ServiceWorkerGlobalScope;
// Create a unique cache name for this deployment
// Create cache names for this deployment
const CACHE = `cache-${version}`;
const RUNTIME_CACHE = `runtime-${version}`;
const IMAGE_CACHE = `images-${version}`;
const ASSETS = [
...build, // the app itself
...files // everything in `static`
];
// Assets to precache for better performance
const CRITICAL_ASSETS = [
'/cafe-bg-compressed.jpg',
'/fonts/Washington.ttf',
'/logo.svg'
];
self.addEventListener('install', (event) => {
// Create a new cache and add all files to it
async function addFilesToCache() {
const cache = await caches.open(CACHE);
const imageCache = await caches.open(IMAGE_CACHE);
// Cache core assets
await cache.addAll(ASSETS);
// Precache critical assets with error handling
await Promise.allSettled(
CRITICAL_ASSETS.map(async (asset) => {
try {
const response = await fetch(asset);
if (response.ok) {
if (asset.includes('jpg') || asset.includes('jpeg') || asset.includes('png') || asset.includes('webp')) {
await imageCache.put(asset, response);
} else {
await cache.put(asset, response);
}
}
} catch (error) {
console.warn(`Failed to cache ${asset}:`, error);
}
})
);
}
event.waitUntil(addFilesToCache());
// Skip waiting to activate immediately
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
// Remove previous cached data from disk
async function deleteOldCaches() {
const currentCaches = [CACHE, RUNTIME_CACHE, IMAGE_CACHE];
for (const key of await caches.keys()) {
if (key !== CACHE) await caches.delete(key);
if (!currentCaches.includes(key)) {
await caches.delete(key);
}
}
}
event.waitUntil(deleteOldCaches());
// Claim clients immediately
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
@@ -51,6 +88,8 @@ self.addEventListener('fetch', (event) => {
async function respond() {
const url = new URL(event.request.url);
const cache = await caches.open(CACHE);
const runtimeCache = await caches.open(RUNTIME_CACHE);
const imageCache = await caches.open(IMAGE_CACHE);
// `build`/`files` can always be served from the cache
if (ASSETS.includes(url.pathname)) {
@@ -61,6 +100,50 @@ self.addEventListener('fetch', (event) => {
}
}
// Handle images with cache-first strategy
if (url.pathname.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i)) {
const cachedResponse = await imageCache.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
try {
const response = await fetch(event.request);
if (response.ok) {
imageCache.put(event.request, response.clone());
}
return response;
} catch {
// Return a fallback image or the cached version
return cachedResponse || new Response('Image not available', { status: 404 });
}
}
// Handle fonts with cache-first strategy
if (url.pathname.match(/\.(woff|woff2|ttf|otf)$/i)) {
const cachedResponse = await cache.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
}
// Handle API and dynamic content with network-first strategy
if (url.pathname.startsWith('/api/') || url.searchParams.has('_data')) {
try {
const response = await fetch(event.request);
if (response.ok) {
runtimeCache.put(event.request, response.clone());
}
return response;
} catch (err) {
const cachedResponse = await runtimeCache.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
throw err;
}
}
// for everything else, try the network first, but
// fall back to the cache if we're offline
try {
@@ -73,15 +156,19 @@ self.addEventListener('fetch', (event) => {
}
if (response.status === 200) {
cache.put(event.request, response.clone());
runtimeCache.put(event.request, response.clone());
}
return response;
} catch (err) {
const response = await cache.match(event.request);
// Try all caches for fallback
const cachedResponse =
await cache.match(event.request) ||
await runtimeCache.match(event.request) ||
await imageCache.match(event.request);
if (response) {
return response;
if (cachedResponse) {
return cachedResponse;
}
// if there's no cache, then just error out