fix: add friendly 404 pages with Bluesky integration (#301)

- add root error page with bufo shrug for 404s/500s
- add specialized artist 404 page that checks Bluesky
- if handle exists on Bluesky but not plyr, show link to their profile
- if handle doesn't exist anywhere, show helpful message
- improve artist page empty state when no tracks exist
- link to artist's Bluesky from empty state

fixes #295

authored by zzstoatzz.io and committed by GitHub 89b7e51d bf21bacc

Changed files
+313 -3
frontend
src
+104
frontend/src/routes/+error.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/stores'; 3 + import { APP_NAME } from '$lib/branding'; 4 + 5 + const status = $page.status; 6 + const message = $page.error?.message || 'something went wrong'; 7 + </script> 8 + 9 + <svelte:head> 10 + <title>{status} - {APP_NAME}</title> 11 + </svelte:head> 12 + 13 + <div class="error-container"> 14 + {#if status === 404} 15 + <div class="bufo-container"> 16 + <img 17 + src="https://all-the.bufo.zone/bufo-shrug.png" 18 + alt="bufo shrug" 19 + class="bufo-img" 20 + /> 21 + </div> 22 + <h1>404</h1> 23 + <p class="error-message">we couldn't find what you're looking for</p> 24 + {:else if status === 500} 25 + <h1>500</h1> 26 + <p class="error-message">something went wrong on our end</p> 27 + <p class="error-detail">we've been notified and will look into it</p> 28 + {:else} 29 + <h1>{status}</h1> 30 + <p class="error-message">{message}</p> 31 + {/if} 32 + 33 + <a href="/" class="home-link">go home</a> 34 + </div> 35 + 36 + <style> 37 + .error-container { 38 + display: flex; 39 + flex-direction: column; 40 + align-items: center; 41 + justify-content: center; 42 + min-height: 100vh; 43 + padding: 2rem; 44 + text-align: center; 45 + } 46 + 47 + .bufo-container { 48 + margin-bottom: 2rem; 49 + } 50 + 51 + .bufo-img { 52 + width: 200px; 53 + height: auto; 54 + opacity: 0.9; 55 + } 56 + 57 + h1 { 58 + font-size: 4rem; 59 + color: #e8e8e8; 60 + margin: 0 0 1rem 0; 61 + font-weight: 700; 62 + } 63 + 64 + .error-message { 65 + font-size: 1.25rem; 66 + color: #b0b0b0; 67 + margin: 0 0 0.5rem 0; 68 + } 69 + 70 + .error-detail { 71 + font-size: 1rem; 72 + color: #808080; 73 + margin: 0 0 2rem 0; 74 + } 75 + 76 + .home-link { 77 + color: var(--accent); 78 + text-decoration: none; 79 + font-size: 1.1rem; 80 + padding: 0.75rem 1.5rem; 81 + border: 1px solid var(--accent); 82 + border-radius: 6px; 83 + transition: all 0.2s; 84 + } 85 + 86 + .home-link:hover { 87 + background: var(--accent); 88 + color: #000; 89 + } 90 + 91 + @media (max-width: 768px) { 92 + .bufo-img { 93 + width: 150px; 94 + } 95 + 96 + h1 { 97 + font-size: 3rem; 98 + } 99 + 100 + .error-message { 101 + font-size: 1.1rem; 102 + } 103 + } 104 + </style>
+165
frontend/src/routes/u/[handle]/+error.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { page } from '$app/stores'; 4 + import { APP_NAME } from '$lib/branding'; 5 + 6 + const status = $page.status; 7 + const handle = $page.params.handle; 8 + 9 + let checkingBluesky = $state(false); 10 + let blueskyProfileExists = $state(false); 11 + let blueskyUrl = $state(''); 12 + 13 + onMount(async () => { 14 + // if this is a 404, check if the handle exists on Bluesky 15 + if (status === 404 && handle) { 16 + checkingBluesky = true; 17 + try { 18 + // try to resolve the handle via ATProto 19 + const response = await fetch( 20 + `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${handle}` 21 + ); 22 + 23 + if (response.ok) { 24 + const data = await response.json(); 25 + if (data.did) { 26 + blueskyProfileExists = true; 27 + blueskyUrl = `https://bsky.app/profile/${handle}`; 28 + } 29 + } 30 + } catch (e) { 31 + console.error('failed to check Bluesky:', e); 32 + } finally { 33 + checkingBluesky = false; 34 + } 35 + } 36 + }); 37 + </script> 38 + 39 + <svelte:head> 40 + <title>404 - artist not found - {APP_NAME}</title> 41 + </svelte:head> 42 + 43 + <div class="error-container"> 44 + <div class="bufo-container"> 45 + <img 46 + src="https://all-the.bufo.zone/bufo-shrug.png" 47 + alt="bufo shrug" 48 + class="bufo-img" 49 + /> 50 + </div> 51 + 52 + <h1>404</h1> 53 + 54 + {#if checkingBluesky} 55 + <p class="error-message">checking if @{handle} exists...</p> 56 + {:else if blueskyProfileExists} 57 + <p class="error-message">@{handle} hasn't joined {APP_NAME} yet</p> 58 + <p class="error-detail">but they're on Bluesky!</p> 59 + <div class="actions"> 60 + <a href={blueskyUrl} target="_blank" rel="noopener" class="bsky-link"> 61 + view their Bluesky profile 62 + </a> 63 + <a href="/" class="home-link">go home</a> 64 + </div> 65 + {:else} 66 + <p class="error-message">we couldn't find anyone by @{handle}</p> 67 + <p class="error-detail">the handle might not exist or could be misspelled</p> 68 + <a href="/" class="home-link">go home</a> 69 + {/if} 70 + </div> 71 + 72 + <style> 73 + .error-container { 74 + display: flex; 75 + flex-direction: column; 76 + align-items: center; 77 + justify-content: center; 78 + min-height: 100vh; 79 + padding: 2rem; 80 + text-align: center; 81 + } 82 + 83 + .bufo-container { 84 + margin-bottom: 2rem; 85 + } 86 + 87 + .bufo-img { 88 + width: 200px; 89 + height: auto; 90 + opacity: 0.9; 91 + } 92 + 93 + h1 { 94 + font-size: 4rem; 95 + color: #e8e8e8; 96 + margin: 0 0 1rem 0; 97 + font-weight: 700; 98 + } 99 + 100 + .error-message { 101 + font-size: 1.25rem; 102 + color: #b0b0b0; 103 + margin: 0 0 0.5rem 0; 104 + } 105 + 106 + .error-detail { 107 + font-size: 1rem; 108 + color: #808080; 109 + margin: 0 0 2rem 0; 110 + } 111 + 112 + .actions { 113 + display: flex; 114 + gap: 1rem; 115 + flex-wrap: wrap; 116 + justify-content: center; 117 + } 118 + 119 + .home-link, 120 + .bsky-link { 121 + color: var(--accent); 122 + text-decoration: none; 123 + font-size: 1.1rem; 124 + padding: 0.75rem 1.5rem; 125 + border: 1px solid var(--accent); 126 + border-radius: 6px; 127 + transition: all 0.2s; 128 + display: inline-block; 129 + } 130 + 131 + .bsky-link { 132 + background: var(--accent); 133 + color: #000; 134 + } 135 + 136 + .bsky-link:hover { 137 + background: #6a9fff; 138 + border-color: #6a9fff; 139 + } 140 + 141 + .home-link:hover { 142 + background: var(--accent); 143 + color: #000; 144 + } 145 + 146 + @media (max-width: 768px) { 147 + .bufo-img { 148 + width: 150px; 149 + } 150 + 151 + h1 { 152 + font-size: 3rem; 153 + } 154 + 155 + .error-message { 156 + font-size: 1.1rem; 157 + } 158 + 159 + .actions { 160 + flex-direction: column; 161 + width: 100%; 162 + max-width: 300px; 163 + } 164 + } 165 + </style>
+44 -3
frontend/src/routes/u/[handle]/+page.svelte
··· 269 269 {/if} 270 270 </h2> 271 271 {#if tracks.length === 0} 272 - <p class="empty">no tracks yet</p> 272 + <div class="empty-state"> 273 + <p class="empty-message">no tracks yet</p> 274 + <p class="empty-detail"> 275 + {artist.display_name} hasn't uploaded any music to {APP_NAME}. 276 + </p> 277 + <a 278 + href="https://bsky.app/profile/{artist.handle}" 279 + target="_blank" 280 + rel="noopener" 281 + class="bsky-link" 282 + > 283 + view their Bluesky profile 284 + </a> 285 + </div> 273 286 {:else} 274 287 <div class="track-list"> 275 288 {#each tracks as track, i} ··· 657 670 gap: 0.75rem; 658 671 } 659 672 660 - .empty { 673 + .empty-state { 661 674 text-align: center; 662 675 padding: 3rem; 676 + background: #141414; 677 + border: 1px solid #282828; 678 + border-radius: 8px; 679 + } 680 + 681 + .empty-message { 682 + color: #b0b0b0; 683 + font-size: 1.25rem; 684 + margin: 0 0 0.5rem 0; 685 + } 686 + 687 + .empty-detail { 663 688 color: #808080; 664 - font-style: italic; 689 + margin: 0 0 1.5rem 0; 690 + } 691 + 692 + .bsky-link { 693 + color: var(--accent); 694 + text-decoration: none; 695 + font-size: 1rem; 696 + padding: 0.75rem 1.5rem; 697 + border: 1px solid var(--accent); 698 + border-radius: 6px; 699 + transition: all 0.2s; 700 + display: inline-block; 701 + } 702 + 703 + .bsky-link:hover { 704 + background: var(--accent); 705 + color: #000; 665 706 } 666 707 667 708 /* respect reduced motion preference */