my website at ewancroft.uk
6
fork

Configure Feed

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

feat: use noise-avatar for all ATProto media fallbacks, fix CSRF config

- bump @ewanc26/noise-avatar to ^0.2.1
- replace initial-letter and icon placeholders with noise canvas in
ProfileCard, BlueskyPostCard, MusicStatusCard, SupportersCard
- add noise canvas cover image fallback to BlogPostCard and DocumentCard
- svelte.config.js: replace deprecated csrf.checkOrigin with trustedOrigins
- bump website to v11.2.1

+83 -69
+1 -1
README.md
··· 811 811 812 812 Built with ❤️ using SvelteKit, AT Protocol, and open-source tools 813 813 814 - **Version**: 11.2.0 814 + **Version**: 11.2.1
+2 -2
package-lock.json
··· 1 1 { 2 2 "name": "website", 3 - "version": "11.2.0", 3 + "version": "11.2.1", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "website", 9 - "version": "11.2.0", 9 + "version": "11.2.1", 10 10 "dependencies": { 11 11 "@atproto/api": "^0.18.21", 12 12 "@ewanc26/atproto": "^0.2.2",
+2 -2
package.json
··· 1 1 { 2 2 "name": "website", 3 3 "private": true, 4 - "version": "11.2.0", 4 + "version": "11.2.1", 5 5 "type": "module", 6 6 "scripts": { 7 7 "dev": "vite dev", ··· 32 32 "dependencies": { 33 33 "@atproto/api": "^0.18.21", 34 34 "@ewanc26/atproto": "^0.2.3", 35 - "@ewanc26/noise-avatar": "^0.1.0", 35 + "@ewanc26/noise-avatar": "^0.2.1", 36 36 "@ewanc26/supporters": "^0.1.6", 37 37 "@ewanc26/tid": "^1.1.1", 38 38 "@ewanc26/ui": "^0.1.8",
+12 -5
pnpm-lock.yaml
··· 15 15 specifier: ^0.2.3 16 16 version: 0.2.3(@atproto/api@0.18.21) 17 17 '@ewanc26/noise-avatar': 18 - specifier: ^0.1.0 19 - version: 0.1.0 18 + specifier: ^0.2.1 19 + version: 0.2.1 20 20 '@ewanc26/supporters': 21 21 specifier: ^0.1.6 22 22 version: 0.1.6(@atproto/api@0.18.21)(svelte@5.53.8) ··· 422 422 peerDependencies: 423 423 '@atproto/api': '>=0.13.0' 424 424 425 - '@ewanc26/noise-avatar@0.1.0': 426 - resolution: {integrity: sha512-T7a1zYaie2GkL6Fe3OKWDxJr68pu/nWrObwI8LPjRsaJiV432Tt9l5pGppb869CfNYEtUTLoGhgXL+RrPnNBKw==} 425 + '@ewanc26/noise-avatar@0.2.1': 426 + resolution: {integrity: sha512-IF7pPhzXS8a+5FIeuqJLKCf+ZYMYL3uZ5fcNbk5Qp2kPmbefcYrpeuA/WvW7VvDYu+AUcsm8MgxAOtSTFqk7SQ==} 427 + 428 + '@ewanc26/noise@0.1.1': 429 + resolution: {integrity: sha512-XeAc0vFrcDHQA7K8xoVVCTYhB2opeBnvGj4s+6SqDS/E3IAP6V32mGf+H2U0JcQHsH4hvpVehYEFt0i1Blnrfg==} 427 430 428 431 '@ewanc26/supporters@0.1.6': 429 432 resolution: {integrity: sha512-L+lF6QZqWiNy+jy3AdJ5kfBHN8xShDJUsRiQYzLLELQk2ZCCqnEsFxwTY4B/A3ECBbPpC0kYpj12XX6dGZpBCQ==} ··· 1509 1512 dependencies: 1510 1513 '@atproto/api': 0.18.21 1511 1514 1512 - '@ewanc26/noise-avatar@0.1.0': {} 1515 + '@ewanc26/noise-avatar@0.2.1': 1516 + dependencies: 1517 + '@ewanc26/noise': 0.1.1 1518 + 1519 + '@ewanc26/noise@0.1.1': {} 1513 1520 1514 1521 '@ewanc26/supporters@0.1.6(@atproto/api@0.18.21)(svelte@5.53.8)': 1515 1522 dependencies:
+2 -2
src/lib/components/layout/Footer.svelte
··· 104 104 type="button" 105 105 onclick={() => happyMacStore.incrementClick()} 106 106 class="cursor-default transition-colors select-none hover:text-ink-600 dark:hover:text-ink-300" 107 - aria-label="Version 11.2.0{showHint 107 + aria-label="Version 11.2.1{showHint 108 108 ? ` - ${$happyMacStore.clickCount} of 24 clicks` 109 109 : ''}" 110 110 title={showHint ? `${$happyMacStore.clickCount}/24` : ''} 111 111 > 112 - v11.2.0{#if showHint}<span class="ml-1 text-xs opacity-60" 112 + v11.2.1{#if showHint}<span class="ml-1 text-xs opacity-60" 113 113 >({$happyMacStore.clickCount}/24)</span 114 114 >{/if} 115 115 </button>
+27 -34
src/lib/components/layout/main/card/BlueskyPostCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { Card } from '$lib/components/ui'; 3 3 import { fetchLatestBlueskyPost, type BlueskyPost } from '$lib/services/atproto'; 4 + import { noiseAvatarAction } from '@ewanc26/noise-avatar'; 4 5 import { formatRelativeTime } from '$lib/utils/formatDate'; 5 6 import { formatCompactNumber } from '$lib/utils/formatNumber'; 6 7 import { Heart, Repeat2, MessageCircle, ExternalLink, X } from '@lucide/svelte'; ··· 208 209 <!-- Author Info --> 209 210 <div class="relative flex gap-3"> 210 211 {#if isReplyParent} 211 - <a 212 - href={getProfileUrl(postData.author.handle)} 213 - target="_blank" 214 - rel="noopener noreferrer" 215 - class="shrink-0 transition-opacity hover:opacity-80" 216 - > 217 - {#if postData.author.avatar} 218 - <img 219 - src={postData.author.avatar} 220 - alt={postData.author.displayName || postData.author.handle} 221 - class="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10" 222 - loading="lazy" 223 - /> 224 - {:else} 225 - <div 226 - class="flex h-8 w-8 items-center justify-center rounded-full bg-primary-200 sm:h-10 sm:w-10 dark:bg-primary-800" 227 - > 228 - <span 229 - class="text-sm font-semibold text-primary-700 sm:text-base dark:text-primary-300" 230 - > 231 - {(postData.author.displayName || postData.author.handle).charAt(0).toUpperCase()} 232 - </span> 233 - </div> 234 - {/if} 235 - </a> 212 + <a 213 + href={getProfileUrl(postData.author.handle)} 214 + target="_blank" 215 + rel="noopener noreferrer" 216 + class="shrink-0 transition-opacity hover:opacity-80" 217 + > 218 + {#if postData.author.avatar} 219 + <img 220 + src={postData.author.avatar} 221 + alt={postData.author.displayName || postData.author.handle} 222 + class="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10" 223 + loading="lazy" 224 + /> 225 + {:else} 226 + <canvas 227 + use:noiseAvatarAction={`${postData.author.did || postData.author.handle}|avatar`} 228 + class="h-8 w-8 rounded-full sm:h-10 sm:w-10" 229 + aria-label="{postData.author.displayName || postData.author.handle}'s avatar placeholder" 230 + ></canvas> 231 + {/if} 232 + </a> 236 233 {:else} 237 234 <a 238 235 href={getProfileUrl(postData.author.handle)} ··· 248 245 loading="lazy" 249 246 /> 250 247 {:else} 251 - <div 252 - class="flex h-10 w-10 items-center justify-center rounded-full bg-primary-200 sm:h-12 sm:w-12 dark:bg-primary-800" 253 - > 254 - <span 255 - class="text-base font-semibold text-primary-700 sm:text-lg dark:text-primary-300" 256 - > 257 - {(postData.author.displayName || postData.author.handle).charAt(0).toUpperCase()} 258 - </span> 259 - </div> 248 + <canvas 249 + use:noiseAvatarAction={`${postData.author.did || postData.author.handle}|avatar`} 250 + class="h-10 w-10 rounded-full sm:h-12 sm:w-12" 251 + aria-label="{postData.author.displayName || postData.author.handle}'s avatar placeholder" 252 + ></canvas> 260 253 {/if} 261 254 </a> 262 255 {/if}
+7 -6
src/lib/components/layout/main/card/MusicStatusCard.svelte
··· 4 4 import { formatRelativeTime } from '$lib/utils/formatDate'; 5 5 6 6 // Icons 7 - import { Music, Disc3, Users, Album, Clock, Radio } from '@lucide/svelte'; 7 + import { Music, Users, Album, Clock, Radio } from '@lucide/svelte'; 8 + import { noiseAvatarAction } from '@ewanc26/noise-avatar'; 8 9 9 10 interface Props { 10 11 musicStatus?: MusicStatusData | null; ··· 84 85 onerror={handleImageError} 85 86 /> 86 87 {:else} 87 - <div 88 - class="flex h-20 w-20 items-center justify-center rounded-lg bg-canvas-200 shadow-md dark:bg-canvas-700" 89 - > 90 - <Disc3 class="h-10 w-10 text-ink-500 dark:text-ink-400" aria-hidden="true" /> 91 - </div> 88 + <canvas 89 + use:noiseAvatarAction={`${safeMusicStatus.trackName}|${formatArtists(safeMusicStatus.artists)}`} 90 + class="h-20 w-20 rounded-lg shadow-md" 91 + aria-label="Album artwork placeholder for {safeMusicStatus.trackName}" 92 + ></canvas> 92 93 {/if} 93 94 </div> 94 95
+6 -7
src/lib/components/layout/main/card/ProfileCard.svelte
··· 3 3 import type { ProfileData } from '$lib/services/atproto'; 4 4 import LinkCard from './LinkCard.svelte'; 5 5 import { formatCompactNumber } from '$lib/utils/formatNumber'; 6 + import { noiseAvatarAction } from '@ewanc26/noise-avatar'; 6 7 7 8 interface Props { 8 9 profile?: ProfileData | null; ··· 85 86 loading="lazy" 86 87 /> 87 88 {:else} 88 - <div 89 - class="flex h-full w-full items-center justify-center bg-primary-200 text-3xl font-bold text-primary-800 dark:bg-primary-800 dark:text-primary-200" 90 - role="img" 91 - aria-label="{safeProfile.displayName || safeProfile.handle}'s avatar initials" 92 - > 93 - {(safeProfile.displayName || safeProfile.handle).charAt(0).toUpperCase()} 94 - </div> 89 + <canvas 90 + use:noiseAvatarAction={`${safeProfile.did || safeProfile.handle}|avatar`} 91 + class="h-full w-full" 92 + aria-label="{safeProfile.displayName || safeProfile.handle}'s avatar placeholder" 93 + ></canvas> 95 94 {/if} 96 95 </div> 97 96 </div>
+11 -4
src/lib/components/ui/BlogPostCard.svelte
··· 4 4 import { InternalCard } from '$lib/components/ui'; 5 5 import { getPostBadges, getBadgeClasses } from '$lib/helper/badges'; 6 6 import { formatLocalizedDate } from '$lib/utils/locale'; 7 + import { noiseAvatarAction } from '@ewanc26/noise-avatar'; 7 8 8 9 interface Props { 9 10 post: BlogPost; ··· 18 19 <InternalCard href={post.url}> 19 20 {#snippet children()} 20 21 <!-- Cover Image (Standard.site only) --> 21 - {#if post.coverImage} 22 - <div class="mb-3 overflow-hidden rounded-lg"> 22 + <div class="mb-3 overflow-hidden rounded-lg"> 23 + {#if post.coverImage} 23 24 <img 24 25 src={post.coverImage} 25 26 alt={post.title} 26 27 class="h-48 w-full object-cover transition-transform duration-300 hover:scale-105" 27 28 /> 28 - </div> 29 - {/if} 29 + {:else} 30 + <canvas 31 + use:noiseAvatarAction={`${post.title}|cover`} 32 + class="h-48 w-full" 33 + aria-hidden="true" 34 + ></canvas> 35 + {/if} 36 + </div> 30 37 31 38 <div class="relative min-w-0 flex-1 space-y-2"> 32 39 <!-- Badges: Platform and Publication -->
+11 -4
src/lib/components/ui/DocumentCard.svelte
··· 3 3 import type { StandardSiteDocument } from '$lib/services/atproto'; 4 4 import { InternalCard } from '$lib/components/ui'; 5 5 import { formatLocalizedDate } from '$lib/utils/locale'; 6 + import { noiseAvatarAction } from '@ewanc26/noise-avatar'; 6 7 7 8 interface Props { 8 9 document: StandardSiteDocument; ··· 15 16 <InternalCard href={document.url}> 16 17 {#snippet children()} 17 18 <!-- Cover Image --> 18 - {#if document.coverImage} 19 - <div class="mb-3 overflow-hidden rounded-lg"> 19 + <div class="mb-3 overflow-hidden rounded-lg"> 20 + {#if document.coverImage} 20 21 <img 21 22 src={document.coverImage} 22 23 alt={document.title} 23 24 class="h-48 w-full object-cover transition-transform duration-300 hover:scale-105" 24 25 /> 25 - </div> 26 - {/if} 26 + {:else} 27 + <canvas 28 + use:noiseAvatarAction={`${document.title}|cover`} 29 + class="h-48 w-full" 30 + aria-hidden="true" 31 + ></canvas> 32 + {/if} 33 + </div> 27 34 28 35 <div class="relative min-w-0 flex-1 space-y-2"> 29 36 <!-- Publication Badge -->
+2 -2
svelte.config.js
··· 30 30 $helper: 'src/lib/helper' 31 31 }, 32 32 33 - // Disable CSRF protection for the webhook route so Ko-fi's server-to-server 33 + // Disable CSRF origin checking for the webhook route so Ko-fi's server-to-server 34 34 // POST requests (which have no matching Origin header) are not rejected. 35 35 csrf: { 36 - checkOrigin: false 36 + trustedOrigins: ['*'] 37 37 }, 38 38 39 39 // Prerender configuration