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

Binary file not shown.

Binary file not shown.

173
logs/logboek.md Normal file
View File

@@ -0,0 +1,173 @@
# Logboek - Serengo Project
## Oktober 2025
### 7 Oktober 2025 (Maandag) - 1 uur
**Werk uitgevoerd:**
- Logo padding gefixed
- Favicon bestanden opgeruimd (verwijderd oude favicon bestanden)
- Manifest.json geoptimaliseerd
**Commits:** 1 commit (5f0cae6)
---
### 3 Oktober 2025 (Donderdag) - 8 uren
**Werk uitgevoerd:**
- Google OAuth implementatie toegevoegd
- Database schema uitgebreid voor OAuth
- Login formulier met shadcn/ui componenten
- Background afbeelding toegevoegd voor login pagina
- Location tracking en kaart functionaliteit geïmplementeerd
- Sonner toast notifications voor foutmeldingen
- Skeleton loading states toegevoegd
- MapLibre GL JS maps geïntegreerd
- Diverse styling fixes en verbeteringen
**Commits:** 10 commits (fbe0a75, 0caa5dc, 6fddf42, d8aa99e, 101b6cf, 5cb0b9e, d82f590, 00da815, b82141e, 0abf4f9)
**Details:**
- Complexe OAuth flow met Google
- Geolocation API integratie
- Real-time location watching
- UI component library uitbreiding
- Map integration met zoom functionaliteit
---
### 2 Oktober 2025 (Woensdag) - 5 uren
**Werk uitgevoerd:**
- MapLibre maps feature toegevoegd
- Header component verbeterd
- Basis kaart functionaliteit geïmplementeerd
- Heel wat testen met self-hosted maps adhv OpenMapTiles
**Commits:** 1 commit (b44a69b)
---
### 29 September 2025 (Zondag) - 3 uren
**Werk uitgevoerd:**
- Modal component geïmplementeerd
- Header component toegevoegd aan layout
- Profile panel component ontwikkeld
- UI verbeteringen voor login pagina
- Code cleanup en refactoring
**Commits:** 6 commits (aa8161f, 0b44f10, 13d9303, 204a443, cc24bc0, b6cf77e)
**Details:**
- Grote refactor van UI componenten
- Herbruikbare modal component
- Verbeterde gebruikersinterface
---
### 28 September 2025 (Zaterdag) - 5 uren
**Werk uitgevoerd:**
- Complete UI overhaul van homepage
- Storybook integratie verwijderd
- Custom componenten geïmplementeerd (Button, Input, ErrorMessage, UserInfo)
- ProfileIcon en ProfilePanel componenten
- Login UI drastisch verbeterd met custom styling
- Washington font toegevoegd
- CSS styling uitgebreid
**Commits:** 6 commits (19c1b7b, aa72a3b, 62b2108, 98d745b, bc8a76b, c4deeb8)
**Details:**
- Grote codebase cleanup (1000+ lijnen verwijderd)
- Custom component library opgebouwd
- Professionele login interface
- Typography verbeteringen
---
### 27 September 2025 (Vrijdag) - 6 uren
**Werk uitgevoerd:**
- Vercel deployment configuratie
- Auth systeem naar main applicatie verplaatst
- Docker configuratie fixes
- Container deployment setup
- CSRF auth fixes
- Database migraties
**Commits:** 12 commits (925e716, bb118ca, 806fb92, 79f57c1, d8e2569, dd20a12, 219a78e, 0bcf6c8, 752cb04, 62c7c5e, 1b413e1, 88a7e74, bbe68c6, fc39408)
**Details:**
- Production deployment voorbereiding
- Docker containerization
- Auth system integratie
- Security verbeteringen
---
### 26 September 2025 (Donderdag) - 5 uren
**Werk uitgevoerd:**
- Lucia auth systeem geïmplementeerd
- Database schema voor gebruikers
- Login/logout functionaliteit
- pnpm-lock.yaml cleanup
- AGENTS.md documentatie toegevoegd
- Database setup en migraties
- Logo en PWA manifest
- Chromatic integration
- Code formatting
- Tailwind CSS verwijderd
**Commits:** 10 commits (7e4570c, 3cdb35b, e4fdfc4, f0d30bc, 9b4e956, fe0f939, ad72884, 8745ef1, 9ce6f96, d8f7921, 93eded5, fc5b927, faa2f94)
**Details:**
- Volledige authenticatie systeem
- Database structuur opgezet
- Project configuratie
- PWA functionaliteit
- Initial project setup
---
## Totaal Overzicht
**Totale geschatte uren:** 33 uren
**Werkdagen:** 6 dagen
**Gemiddelde uren per dag:** 5.5 uur
### Project Milestones:
1. **26 Sept**: Project initialisatie en auth systeem
2. **27 Sept**: Deployment en productie setup
3. **28 Sept**: UI/UX complete overhaul
4. **29 Sept**: Component architectuur verbetering
5. **2-3 Okt**: Maps en location features
6. **7 Okt**: Optimalisaties
### Hoofdfunctionaliteiten geïmplementeerd:
- ✅ Gebruikersauthenticatie (Lucia + Google OAuth)
- ✅ Responsive UI met custom componenten
- ✅ Real-time locatie tracking
- ✅ Interactive maps (MapLibre GL JS)
- ✅ PWA functionaliteit
- ✅ Docker deployment
- ✅ Database (PostgreSQL + Drizzle ORM)
- ✅ Toast notifications
- ✅ Loading states en error handling

View File

@@ -80,6 +80,7 @@
src: url('/fonts/Washington.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
* {
@@ -131,5 +132,30 @@
a {
color: inherit;
text-decoration: none;
outline: none;
transition: all 0.2s ease;
}
/* Ensure links have visible focus indicators and non-color differentiation */
a:hover {
text-decoration: underline;
}
a:focus {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
text-decoration: underline;
}
/* Special handling for map attribution links */
.maplibregl-ctrl-attrib a {
color: #0078a8 !important;
text-decoration: underline !important;
}
.maplibregl-ctrl-attrib a:hover,
.maplibregl-ctrl-attrib a:focus {
text-decoration: underline !important;
background-color: rgba(0, 120, 168, 0.1) !important;
}
}

View File

@@ -2,8 +2,16 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#333333" />
<meta name="theme-color" content="#f8f8f8" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#333333" media="(prefers-color-scheme: dark)" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Serengo" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/logo.svg" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View File

@@ -39,5 +39,31 @@ export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = user;
event.locals.session = session;
return resolve(event);
const response = await resolve(event);
// Add security headers
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline' fonts.googleapis.com; " +
"font-src 'self' fonts.gstatic.com; " +
"img-src 'self' data: blob: *.openstreetmap.org *.tile.openstreetmap.org; " +
"connect-src 'self' *.openstreetmap.org; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self';"
);
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Add HSTS for HTTPS in production
if (event.url.protocol === 'https:') {
response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
return response;
};

View File

@@ -14,6 +14,20 @@
<svelte:head>
<link rel="icon" href={favicon} />
<!-- Performance optimizations -->
<link
rel="preload"
href="/fonts/Washington.ttf"
as="font"
type="font/ttf"
crossorigin="anonymous"
/>
<link rel="dns-prefetch" href="//tile.openstreetmap.org" />
<link rel="preconnect" href="https://tile.openstreetmap.org" crossorigin="anonymous" />
<!-- Resource hints for login page background -->
{#if isLoginRoute}
<link rel="preload" href="/cafe-bg-compressed.jpg" as="image" />
{/if}
</svelte:head>
<Toaster />

View File

@@ -2,6 +2,26 @@
import { Map } from '$lib';
</script>
<svelte:head>
<title>Serengo - Meet the Unexpected</title>
<meta
name="description"
content="Discover unexpected places and experiences with Serengo's interactive map. Find hidden gems and explore your surroundings like never before."
/>
<meta property="og:title" content="Serengo - Meet the Unexpected" />
<meta
property="og:description"
content="Discover unexpected places and experiences with Serengo's interactive map. Find hidden gems and explore your surroundings like never before."
/>
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Serengo - Meet the Unexpected" />
<meta
name="twitter:description"
content="Discover unexpected places and experiences with Serengo's interactive map. Find hidden gems and explore your surroundings like never before."
/>
</svelte:head>
<div class="home-container">
<main class="main-content">
<div class="map-section">

View File

@@ -5,6 +5,26 @@
let { form }: { form: ActionData } = $props();
</script>
<svelte:head>
<title>Login - Serengo</title>
<meta
name="description"
content="Sign in to your Serengo account to access personalized maps and discover unexpected places around you."
/>
<meta property="og:title" content="Login - Serengo" />
<meta
property="og:description"
content="Sign in to your Serengo account to access personalized maps and discover unexpected places around you."
/>
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Login - Serengo" />
<meta
name="twitter:description"
content="Sign in to your Serengo account to access personalized maps and discover unexpected places around you."
/>
</svelte:head>
<div class="login-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-sm flex-col gap-6">
<LoginForm {form} />
@@ -12,7 +32,7 @@
<style>
.login-background {
background-image: url('/cafe-bg.jpeg');
background-image: url('/cafe-bg-compressed.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
@@ -34,5 +54,16 @@
position: relative;
z-index: 2;
}
/* Preload hint for faster loading */
@media (prefers-reduced-data: no-preference) {
.login-background::after {
content: '';
position: absolute;
background-image: url('/cafe-bg-compressed.jpg');
opacity: 0;
z-index: -1;
}
}
</style>
</div>

View File

@@ -0,0 +1,29 @@
import type { RequestHandler } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ url }) => {
const baseUrl = url.origin;
const lastmod = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>${baseUrl}/</loc>
<lastmod>${lastmod}</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>${baseUrl}/login</loc>
<lastmod>${lastmod}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>`;
return new Response(sitemap, {
headers: {
'Content-Type': 'application/xml',
'Cache-Control': 'max-age=86400' // Cache for 24 hours
}
});
};

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

View File

@@ -1,7 +1,7 @@
{
"short_name": "Serengo",
"name": "Serengo",
"description": "meet the unexpected.",
"name": "Serengo - meet the unexpected.",
"description": "Discover unexpected places and experiences with Serengo's interactive map. Find hidden gems and explore your surroundings like never before.",
"icons": [
{
"src": "/logo.svg",
@@ -10,8 +10,11 @@
"purpose": "any maskable"
}
],
"start_url": ".",
"start_url": "/",
"display": "standalone",
"theme_color": "white",
"background_color": "white"
"orientation": "portrait-primary",
"theme_color": "#333333",
"background_color": "#f8f8f8",
"scope": "/",
"categories": ["travel", "maps", "discovery"]
}

View File

@@ -1,3 +1,13 @@
# allow crawling everything by default
# Serengo - meet the unexpected.
User-agent: *
Disallow:
Allow: /
Disallow: /api/
Disallow: /logout
Disallow: /_app/
Disallow: /.svelte-kit/
# Sitemap location
Sitemap: https://serengo.ziasvannes.tech/sitemap.xml
# Crawl delay for polite crawling
Crawl-delay: 1

View File

@@ -4,5 +4,19 @@ import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
preview: { allowedHosts: ['ziasvannes.tech'] }
preview: { allowedHosts: ['ziasvannes.tech'] },
build: {
target: 'es2020',
cssCodeSplit: true,
rollupOptions: {
output: {
manualChunks: {
maplibre: ['maplibre-gl', 'svelte-maplibre']
}
}
}
},
optimizeDeps: {
include: ['maplibre-gl', 'svelte-maplibre']
}
});