my website at ewancroft.uk
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor: add slug param matcher and global error page for robust routing

- Added src/params/slug.ts param matcher to validate slugs against
configured mappings before route load, ensuring only valid slugs
resolve.
- Introduced src/routes/+error.svelte for global error handling with
dynamic messages and contextual styling for common HTTP statuses
(404, 403, 500, 503).
- Updated slug-based routes ([slug], [slug]/rss, [slug]/atom, [slug]/[rkey])
to use new [slug=slug] syntax for parameter validation.
- Added defensive checks for invalid or missing slugs, returning
appropriate 400 responses instead of silently failing.
- Improved resilience and user feedback for invalid or missing routes.

This refactor strengthens routing reliability, improves error visibility,
and introduces consistent, user-friendly error handling across the website.

+147 -2
+11
src/params/slug.ts
··· 1 + import type { ParamMatcher } from '@sveltejs/kit'; 2 + import { getAllSlugs } from '$lib/config/slugs'; 3 + 4 + /** 5 + * Param matcher for valid slugs 6 + * Only allows slugs that are defined in the slug-mappings configuration 7 + */ 8 + export const match: ParamMatcher = (param) => { 9 + const validSlugs = getAllSlugs(); 10 + return validSlugs.includes(param); 11 + };
+94
src/routes/+error.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/stores'; 3 + import { Card } from '$lib/components/ui'; 4 + 5 + // Get error details from page store 6 + const status = $derived($page.status); 7 + const errorMessage = $derived($page.error?.message || 'An unexpected error occurred'); 8 + 9 + // Error titles and descriptions based on status code 10 + const errorDetails = $derived.by(() => { 11 + switch (status) { 12 + case 404: 13 + return { 14 + title: 'Page Not Found', 15 + description: `The page <code class="rounded bg-canvas-200 px-2 py-1 font-mono text-sm dark:bg-canvas-800">${$page.url.pathname}</code> could not be found.` 16 + }; 17 + case 403: 18 + return { 19 + title: 'Access Forbidden', 20 + description: 'You do not have permission to access this resource.' 21 + }; 22 + case 500: 23 + return { 24 + title: 'Internal Server Error', 25 + description: 'Something went wrong on our end. Please try again later.' 26 + }; 27 + case 503: 28 + return { 29 + title: 'Service Unavailable', 30 + description: 'The service is temporarily unavailable. Please try again in a moment.' 31 + }; 32 + default: 33 + return { 34 + title: 'An Error Occurred', 35 + description: errorMessage 36 + }; 37 + } 38 + }); 39 + </script> 40 + 41 + <svelte:head> 42 + <title>{status} - {errorDetails.title}</title> 43 + </svelte:head> 44 + 45 + <div class="mx-auto max-w-2xl"> 46 + <Card variant="elevated" padding="lg"> 47 + {#snippet children()} 48 + <div class="text-center"> 49 + <!-- Large status code number --> 50 + <div class="mb-6"> 51 + <h1 class="text-8xl font-bold text-primary-500 dark:text-primary-400 md:text-9xl"> 52 + {status} 53 + </h1> 54 + </div> 55 + 56 + <!-- Error title --> 57 + <h2 class="mb-4 text-2xl font-bold text-ink-900 md:text-3xl dark:text-ink-50"> 58 + {errorDetails.title} 59 + </h2> 60 + 61 + <!-- Error description --> 62 + <p class="mb-6 text-ink-700 dark:text-ink-200"> 63 + {@html errorDetails.description} 64 + </p> 65 + 66 + <!-- Show additional error message if it's different from the description --> 67 + {#if errorMessage && errorMessage !== errorDetails.description && status !== 404} 68 + <p class="mb-6 rounded-lg bg-canvas-200 p-4 text-sm text-ink-600 dark:bg-canvas-800 dark:text-ink-300"> 69 + {errorMessage} 70 + </p> 71 + {/if} 72 + 73 + <!-- Action buttons --> 74 + <div class="flex flex-col items-center justify-center gap-3 sm:flex-row"> 75 + <a 76 + href="/" 77 + class="inline-flex w-full items-center justify-center rounded-lg bg-primary-500 px-6 py-3 font-medium text-white transition-colors hover:bg-primary-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700" 78 + > 79 + Return to Home 80 + </a> 81 + 82 + {#if status !== 404} 83 + <button 84 + onclick={() => window.location.reload()} 85 + class="inline-flex w-full items-center justify-center rounded-lg bg-canvas-300 px-6 py-3 font-medium text-ink-900 transition-colors hover:bg-canvas-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-canvas-600 sm:w-auto dark:bg-canvas-700 dark:text-ink-50 dark:hover:bg-canvas-600" 86 + > 87 + Try Again 88 + </button> 89 + {/if} 90 + </div> 91 + </div> 92 + {/snippet} 93 + </Card> 94 + </div>
+10 -1
src/routes/[slug]/+server.ts src/routes/[slug=slug]/+server.ts
··· 31 31 32 32 // For /{slug} root, redirect to the publication 33 33 if (!slugPath || slugPath === '') { 34 - // Get the publication rkey from the slug 34 + // Validate slug and get the publication rkey 35 + if (!slug) { 36 + return new Response('Invalid slug', { 37 + status: 400, 38 + headers: { 39 + 'Content-Type': 'text/plain; charset=utf-8' 40 + } 41 + }); 42 + } 43 + 35 44 const publicationRkey = getPublicationRkeyFromSlug(slug); 36 45 37 46 if (!publicationRkey) {
+10
src/routes/[slug]/[rkey]/+server.ts src/routes/[slug=slug]/[rkey]/+server.ts
··· 128 128 const slug = params.slug; 129 129 const rkey = params.rkey; 130 130 131 + // Validate slug 132 + if (!slug) { 133 + return new Response('Invalid slug', { 134 + status: 400, 135 + headers: { 136 + 'Content-Type': 'text/plain; charset=utf-8' 137 + } 138 + }); 139 + } 140 + 131 141 // Get the publication rkey from the slug 132 142 const publicationRkey = getPublicationRkeyFromSlug(slug); 133 143
+10
src/routes/[slug]/atom/+server.ts src/routes/[slug=slug]/atom/+server.ts
··· 15 15 export const GET: RequestHandler = ({ params }) => { 16 16 const slug = params.slug; 17 17 18 + // Validate slug 19 + if (!slug) { 20 + return new Response('Invalid slug', { 21 + status: 400, 22 + headers: { 23 + 'Content-Type': 'text/plain; charset=utf-8' 24 + } 25 + }); 26 + } 27 + 18 28 // Validate slug exists in config 19 29 const publicationRkey = getPublicationRkeyFromSlug(slug); 20 30
+12 -1
src/routes/[slug]/rss/+server.ts src/routes/[slug=slug]/rss/+server.ts
··· 20 20 export const GET: RequestHandler = async ({ params }) => { 21 21 const slug = params.slug; 22 22 23 + // Validate slug 24 + if (!slug) { 25 + return new Response('Invalid slug', { 26 + status: 400, 27 + headers: { 28 + 'Content-Type': 'text/plain; charset=utf-8' 29 + } 30 + }); 31 + } 32 + 23 33 // Get the publication rkey from the slug 24 34 const publicationRkey = getPublicationRkeyFromSlug(slug); 25 35 ··· 49 59 50 60 // If WhiteWind is enabled and we have WhiteWind posts, generate RSS for them 51 61 if (PUBLIC_ENABLE_WHITEWIND === 'true' && whiteWindPosts.length > 0) { 52 - return generateWhiteWindRSS(whiteWindPosts, slug); 62 + // slug is guaranteed to be defined here 63 + return generateWhiteWindRSS(whiteWindPosts, slug as string); 53 64 } 54 65 55 66 // If WhiteWind is disabled or only Leaflet posts exist, redirect to Leaflet RSS feed