my website at ewancroft.uk
1import type { Handle } from '@sveltejs/kit';
2import { PUBLIC_CORS_ALLOWED_ORIGINS } from '$env/static/public';
3import { HTTP_CACHE_HEADERS } from '$lib/config/cache.config';
4
5/**
6 * Global request handler with CORS support
7 *
8 * CORS headers are dynamically configured via the PUBLIC_CORS_ALLOWED_ORIGINS environment variable.
9 * Set it to a comma-separated list of allowed origins, or "*" to allow all origins.
10 */
11export const handle: Handle = async ({ event, resolve }) => {
12 // Handle OPTIONS preflight requests for CORS
13 if (event.request.method === 'OPTIONS' && event.url.pathname.startsWith('/api/')) {
14 const origin = event.request.headers.get('origin');
15 const allowedOrigins =
16 PUBLIC_CORS_ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || [];
17
18 const headers: Record<string, string> = {
19 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
20 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
21 'Access-Control-Max-Age': '86400'
22 };
23
24 if (allowedOrigins.includes('*')) {
25 headers['Access-Control-Allow-Origin'] = '*';
26 } else if (origin && allowedOrigins.includes(origin)) {
27 headers['Access-Control-Allow-Origin'] = origin;
28 headers['Vary'] = 'Origin';
29 }
30
31 return new Response(null, { status: 204, headers });
32 }
33
34 const response = await resolve(event, {
35 filterSerializedResponseHeaders: (name) => {
36 return name === 'content-type' || name === 'cache-control' || name.startsWith('x-');
37 }
38 });
39
40 // Add HTTP caching headers for better performance and reduced timeouts
41 // Layout data (root route) is cached aggressively since profile/site info changes infrequently
42 if (!event.url.pathname.startsWith('/api/') && event.url.pathname !== '/webhook') {
43 // Root layout loads profile and site info - cache aggressively
44 if (event.url.pathname === '/' || event.url.pathname === '') {
45 response.headers.set('Cache-Control', HTTP_CACHE_HEADERS.LAYOUT);
46 }
47 // Blog listing pages
48 else if (event.url.pathname.startsWith('/blog') || event.url.pathname.startsWith('/archive')) {
49 response.headers.set('Cache-Control', HTTP_CACHE_HEADERS.BLOG_LISTING);
50 }
51 // Individual blog post pages
52 else if (event.url.pathname.match(/^\/[a-z0-9-]+$/)) {
53 response.headers.set('Cache-Control', HTTP_CACHE_HEADERS.BLOG_POST);
54 }
55 // Other pages get moderate caching
56 else {
57 response.headers.set('Cache-Control', HTTP_CACHE_HEADERS.LAYOUT);
58 }
59 }
60
61 // Add CORS headers for API routes
62 if (event.url.pathname.startsWith('/api/')) {
63 const origin = event.request.headers.get('origin');
64 const allowedOrigins =
65 PUBLIC_CORS_ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || [];
66
67 // If * is specified, allow any origin
68 if (allowedOrigins.includes('*')) {
69 response.headers.set('Access-Control-Allow-Origin', '*');
70 } else if (origin && allowedOrigins.includes(origin)) {
71 // Only set the specific origin if it's in the allowed list
72 response.headers.set('Access-Control-Allow-Origin', origin);
73 response.headers.set('Vary', 'Origin');
74 }
75
76 response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
77 response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
78 response.headers.set('Access-Control-Max-Age', '86400'); // 24 hours
79 }
80
81 return response;
82};