your personal website on atproto - mirror blento.app

Merge pull request #202 from flo-bit/event-og-images

Event og images

authored by

Florian and committed by
GitHub
74018a5f dcb98beb

+437 -101
+1 -1
docs/Autofill.md
··· 11 11 - blue.flashes.actor.portfolio 12 12 - social.grain.gallery 13 13 - xyz.statusphere.status 14 - - site.standard.publication 14 + - site.standard.publication 15 15 - fm.teal.alpha.feed.play 16 16 - dev.npmx.feed.like 17 17 - add bluesky profile card
+40
src/lib/actor.ts
··· 1 + import type { ActorIdentifier, Did } from '@atcute/lexicons'; 2 + import type { CacheService } from './cache'; 3 + import { env as publicEnv } from '$env/dynamic/public'; 4 + import { resolveHandle } from './atproto'; 5 + import { isHandle } from '@atcute/lexicons/syntax'; 6 + 7 + export async function getActor({ 8 + request, 9 + paramActor, 10 + platform, 11 + blockBoth = true 12 + }: { 13 + request: Request; 14 + paramActor?: ActorIdentifier; 15 + platform: Readonly<App.Platform> | undefined; 16 + blockBoth?: boolean; 17 + }): Promise<Did | undefined> { 18 + const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 19 + let actor = paramActor; 20 + 21 + if (!actor) { 22 + const kv = platform?.env?.CUSTOM_DOMAINS; 23 + 24 + if (kv && customDomain) { 25 + try { 26 + const did = await kv.get(customDomain); 27 + 28 + if (did) actor = did as ActorIdentifier; 29 + } catch (error) { 30 + console.error('failed to get custom domain kv', error); 31 + } 32 + } else { 33 + actor = publicEnv.PUBLIC_HANDLE as ActorIdentifier; 34 + } 35 + } else if (customDomain && paramActor && blockBoth) { 36 + actor = undefined; 37 + } 38 + 39 + return isHandle(actor) ? await resolveHandle({ handle: actor }) : actor; 40 + }
+4 -2
src/lib/atproto/methods.ts
··· 395 395 */ 396 396 export function getCDNImageBlobUrl({ 397 397 did, 398 - blob 398 + blob, 399 + type = 'webp' 399 400 }: { 400 401 did?: string; 401 402 blob: { ··· 404 405 $link: string; 405 406 }; 406 407 }; 408 + type?: 'webp' | 'jpeg'; 407 409 }) { 408 410 if (!blob || !did) return; 409 411 did ??= user.did; 410 412 411 - return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@webp`; 413 + return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@${type}`; 412 414 } 413 415 414 416 /**
+2 -1
src/lib/cards/social/EventCard/index.ts
··· 24 24 alt?: string; 25 25 role?: string; 26 26 content?: { 27 - ref?: { 27 + $type: 'blob'; 28 + ref: { 28 29 $link: string; 29 30 }; 30 31 mimeType?: string;
+2 -23
src/routes/[[actor=actor]]/(pages)/+layout.server.ts
··· 2 2 import { env } from '$env/dynamic/private'; 3 3 import { error } from '@sveltejs/kit'; 4 4 import { createCache } from '$lib/cache'; 5 - import type { ActorIdentifier } from '@atcute/lexicons'; 6 - import { env as publicEnv } from '$env/dynamic/public'; 5 + import { getActor } from '$lib/actor.js'; 7 6 8 7 export async function load({ params, platform, request }) { 9 8 if (env.PUBLIC_IS_SELFHOSTED) error(404); 10 9 11 10 const cache = createCache(platform); 12 11 13 - const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 14 - 15 - let actor: ActorIdentifier | undefined = params.actor; 16 - 17 - if (!actor) { 18 - const kv = platform?.env?.CUSTOM_DOMAINS; 19 - 20 - if (kv && customDomain) { 21 - try { 22 - const did = await kv.get(customDomain); 23 - 24 - if (did) actor = did as ActorIdentifier; 25 - } catch (error) { 26 - console.error('failed to get custom domain kv', error); 27 - } 28 - } else { 29 - actor = publicEnv.PUBLIC_HANDLE as ActorIdentifier; 30 - } 31 - } else if (customDomain && params.actor) { 32 - actor = undefined; 33 - } 12 + const actor = await getActor({ request, paramActor: params.actor, platform }); 34 13 35 14 if (!actor) { 36 15 throw error(404, 'Page not found');
+2 -24
src/routes/[[actor=actor]]/.well-known/site.standard.publication/+server.ts
··· 1 1 import { loadData } from '$lib/website/load'; 2 2 import { createCache } from '$lib/cache'; 3 3 import { env } from '$env/dynamic/private'; 4 - import { env as publicEnv } from '$env/dynamic/public'; 5 - import type { ActorIdentifier } from '@atcute/lexicons'; 6 - 7 4 import { error } from '@sveltejs/kit'; 8 5 import { text } from '@sveltejs/kit'; 6 + import { getActor } from '$lib/actor.js'; 9 7 10 8 export async function GET({ params, platform, request }) { 11 9 const cache = createCache(platform); 12 10 13 - const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 14 - 15 - let actor: ActorIdentifier | undefined = params.actor; 16 - 17 - if (!actor) { 18 - const kv = platform?.env?.CUSTOM_DOMAINS; 19 - 20 - if (kv && customDomain) { 21 - try { 22 - const did = await kv.get(customDomain); 23 - 24 - if (did) actor = did as ActorIdentifier; 25 - } catch (error) { 26 - console.error('failed to get custom domain kv', error); 27 - } 28 - } else { 29 - actor = publicEnv.PUBLIC_HANDLE as ActorIdentifier; 30 - } 31 - } else if (customDomain && params.actor) { 32 - actor = undefined; 33 - } 11 + const actor = await getActor({ request, paramActor: params.actor, platform }); 34 12 35 13 if (!actor) { 36 14 throw error(404, 'Page not found');
+2 -21
src/routes/[[actor=actor]]/api/refresh/+server.ts
··· 1 1 import { createCache } from '$lib/cache'; 2 2 import { loadData } from '$lib/website/load.js'; 3 3 import { env } from '$env/dynamic/private'; 4 - import { env as publicEnv } from '$env/dynamic/public'; 5 - import type { ActorIdentifier } from '@atcute/lexicons'; 6 4 import { error, json } from '@sveltejs/kit'; 5 + import { getActor } from '$lib/actor'; 7 6 8 7 export async function GET({ params, platform, request }) { 9 8 const cache = createCache(platform); 10 9 if (!cache) return json('no cache'); 11 10 12 - const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 13 - 14 - let actor: ActorIdentifier | undefined = params.actor; 15 - 16 - if (!actor) { 17 - const kv = platform?.env?.CUSTOM_DOMAINS; 18 - 19 - if (kv && customDomain) { 20 - try { 21 - const did = await kv.get(customDomain); 22 - 23 - if (did) actor = did as ActorIdentifier; 24 - } catch (error) { 25 - console.error('failed to get custom domain kv', error); 26 - } 27 - } else { 28 - actor = publicEnv.PUBLIC_HANDLE as ActorIdentifier; 29 - } 30 - } 11 + const actor = await getActor({ request, paramActor: params.actor, platform, blockBoth: false }); 31 12 32 13 if (!actor) { 33 14 throw error(404, 'Page not found');
+54
src/routes/[[actor=actor]]/e/+page.server.ts
··· 1 + import { error } from '@sveltejs/kit'; 2 + import type { EventData } from '$lib/cards/social/EventCard'; 3 + import { getBlentoOrBskyProfile } from '$lib/atproto/methods.js'; 4 + import { createCache, type CachedProfile } from '$lib/cache'; 5 + import type { Did } from '@atcute/lexicons'; 6 + import { getActor } from '$lib/actor.js'; 7 + 8 + export async function load({ params, platform, request }) { 9 + const cache = createCache(platform); 10 + 11 + const did = await getActor({ request, paramActor: params.actor, platform }); 12 + 13 + if (!did) { 14 + throw error(404, 'Events not found'); 15 + } 16 + 17 + try { 18 + const [eventsResponse, hostProfile] = await Promise.all([ 19 + fetch( 20 + `https://smokesignal.events/xrpc/community.lexicon.calendar.searchEvents?repository=${encodeURIComponent(did)}&query=upcoming` 21 + ), 22 + cache 23 + ? cache.getProfile(did as Did).catch(() => null) 24 + : getBlentoOrBskyProfile({ did: did as Did }) 25 + .then( 26 + (p): CachedProfile => ({ 27 + did: p.did as string, 28 + handle: p.handle as string, 29 + displayName: p.displayName as string | undefined, 30 + avatar: p.avatar as string | undefined, 31 + hasBlento: p.hasBlento, 32 + url: p.url 33 + }) 34 + ) 35 + .catch(() => null) 36 + ]); 37 + 38 + if (!eventsResponse.ok) { 39 + throw error(404, 'Events not found'); 40 + } 41 + 42 + const data: { results: EventData[] } = await eventsResponse.json(); 43 + const events = data.results; 44 + 45 + return { 46 + events, 47 + did, 48 + hostProfile: hostProfile ?? null 49 + }; 50 + } catch (e) { 51 + if (e && typeof e === 'object' && 'status' in e) throw e; 52 + throw error(404, 'Events not found'); 53 + } 54 + }
+183
src/routes/[[actor=actor]]/e/+page.svelte
··· 1 + <script lang="ts"> 2 + import type { EventData } from '$lib/cards/social/EventCard'; 3 + import { getCDNImageBlobUrl } from '$lib/atproto'; 4 + import { Avatar as FoxAvatar, Badge } from '@foxui/core'; 5 + import Avatar from 'svelte-boring-avatars'; 6 + 7 + let { data } = $props(); 8 + 9 + let events: EventData[] = $derived(data.events); 10 + let did: string = $derived(data.did); 11 + let hostProfile = $derived(data.hostProfile); 12 + 13 + let hostName = $derived(hostProfile?.displayName || hostProfile?.handle || did); 14 + let hostUrl = $derived( 15 + hostProfile?.url ?? `https://bsky.app/profile/${hostProfile?.handle || did}` 16 + ); 17 + 18 + function formatDate(dateStr: string): string { 19 + const date = new Date(dateStr); 20 + const options: Intl.DateTimeFormatOptions = { 21 + weekday: 'short', 22 + month: 'short', 23 + day: 'numeric' 24 + }; 25 + if (date.getFullYear() !== new Date().getFullYear()) { 26 + options.year = 'numeric'; 27 + } 28 + return date.toLocaleDateString('en-US', options); 29 + } 30 + 31 + function formatTime(dateStr: string): string { 32 + return new Date(dateStr).toLocaleTimeString('en-US', { 33 + hour: 'numeric', 34 + minute: '2-digit' 35 + }); 36 + } 37 + 38 + function getModeLabel(mode: string): string { 39 + if (mode.includes('virtual')) return 'Virtual'; 40 + if (mode.includes('hybrid')) return 'Hybrid'; 41 + if (mode.includes('inperson')) return 'In-Person'; 42 + return 'Event'; 43 + } 44 + 45 + function getModeColor(mode: string): 'cyan' | 'purple' | 'amber' | 'secondary' { 46 + if (mode.includes('virtual')) return 'cyan'; 47 + if (mode.includes('hybrid')) return 'purple'; 48 + if (mode.includes('inperson')) return 'amber'; 49 + return 'secondary'; 50 + } 51 + 52 + function getLocationString(locations: EventData['locations']): string | undefined { 53 + if (!locations || locations.length === 0) return undefined; 54 + 55 + const loc = locations.find((v) => v.$type === 'community.lexicon.location.address'); 56 + if (!loc) return undefined; 57 + 58 + const flat = loc as Record<string, unknown>; 59 + const nested = loc.address; 60 + 61 + const locality = (flat.locality as string) || nested?.locality; 62 + const region = (flat.region as string) || nested?.region; 63 + 64 + const parts = [locality, region].filter(Boolean); 65 + return parts.length > 0 ? parts.join(', ') : undefined; 66 + } 67 + 68 + function getThumbnail(event: EventData): { url: string; alt: string } | null { 69 + if (!event.media || event.media.length === 0) return null; 70 + const media = event.media.find((m) => m.role === 'thumbnail'); 71 + if (!media?.content) return null; 72 + const url = getCDNImageBlobUrl({ did, blob: media.content, type: 'jpeg' }); 73 + if (!url) return null; 74 + return { url, alt: media.alt || event.name }; 75 + } 76 + 77 + function getRkey(event: EventData): string { 78 + return event.url.split('/').pop() || ''; 79 + } 80 + 81 + let actorPrefix = $derived(data.hostProfile?.handle ? `/${data.hostProfile.handle}` : `/${did}`); 82 + </script> 83 + 84 + <svelte:head> 85 + <title>{hostName} - Events</title> 86 + <meta name="description" content="Events hosted by {hostName}" /> 87 + <meta property="og:title" content="{hostName} - Events" /> 88 + <meta property="og:description" content="Events hosted by {hostName}" /> 89 + <meta name="twitter:card" content="summary" /> 90 + <meta name="twitter:title" content="{hostName} - Events" /> 91 + <meta name="twitter:description" content="Events hosted by {hostName}" /> 92 + </svelte:head> 93 + 94 + <div class="bg-base-50 dark:bg-base-950 min-h-screen px-6 py-12 sm:py-12"> 95 + <div class="mx-auto max-w-4xl"> 96 + <!-- Header --> 97 + <div class="mb-8"> 98 + <h1 class="text-base-900 dark:text-base-50 mb-2 text-2xl font-bold sm:text-3xl"> 99 + Upcoming events 100 + </h1> 101 + <div class="flex items-center gap-2 mt-4"> 102 + <span class="text-base-500 dark:text-base-400 text-sm">Hosted by</span> 103 + <a 104 + href={hostUrl} 105 + target={hostProfile?.hasBlento ? undefined : '_blank'} 106 + rel={hostProfile?.hasBlento ? undefined : 'noopener noreferrer'} 107 + class="flex items-center gap-1.5 hover:underline" 108 + > 109 + <FoxAvatar src={hostProfile?.avatar} alt={hostName} class="size-5 shrink-0" /> 110 + <span class="text-base-900 dark:text-base-100 text-sm font-medium">{hostName}</span> 111 + </a> 112 + </div> 113 + </div> 114 + 115 + {#if events.length === 0} 116 + <p class="text-base-500 dark:text-base-400 py-12 text-center">No events found.</p> 117 + {:else} 118 + <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> 119 + {#each events as event (event.url)} 120 + {@const thumbnail = getThumbnail(event)} 121 + {@const location = getLocationString(event.locations)} 122 + {@const rkey = getRkey(event)} 123 + <a 124 + href="{actorPrefix}/e/{rkey}" 125 + class="border-base-200 dark:border-base-800 hover:border-base-300 dark:hover:border-base-700 group block overflow-hidden rounded-xl border transition-colors" 126 + > 127 + <!-- Thumbnail --> 128 + {#if thumbnail} 129 + <img 130 + src={thumbnail.url} 131 + alt={thumbnail.alt} 132 + class="aspect-square w-full object-cover" 133 + /> 134 + {:else} 135 + <div 136 + class="bg-base-100 dark:bg-base-900 aspect-square w-full [&>svg]:h-full [&>svg]:w-full" 137 + > 138 + <Avatar 139 + size={400} 140 + name={rkey} 141 + variant="marble" 142 + colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']} 143 + square 144 + /> 145 + </div> 146 + {/if} 147 + 148 + <!-- Content --> 149 + <div class="p-4"> 150 + <h2 151 + class="text-base-900 dark:text-base-50 group-hover:text-base-700 dark:group-hover:text-base-200 mb-1 leading-snug font-semibold" 152 + > 153 + {event.name} 154 + </h2> 155 + 156 + <p class="text-base-500 dark:text-base-400 mb-2 text-sm"> 157 + {formatDate(event.startsAt)} &middot; {formatTime(event.startsAt)} 158 + </p> 159 + 160 + <div class="flex flex-wrap items-center gap-2"> 161 + {#if event.mode} 162 + <Badge size="sm" variant={getModeColor(event.mode)} 163 + >{getModeLabel(event.mode)}</Badge 164 + > 165 + {/if} 166 + 167 + {#if location} 168 + <span class="text-base-500 dark:text-base-400 truncate text-xs">{location}</span> 169 + {/if} 170 + </div> 171 + 172 + {#if event.countGoing && event.countGoing > 0} 173 + <p class="text-base-500 dark:text-base-400 mt-2 text-xs"> 174 + {event.countGoing} going 175 + </p> 176 + {/if} 177 + </div> 178 + </a> 179 + {/each} 180 + </div> 181 + {/if} 182 + </div> 183 + </div>
+2 -23
src/routes/[[actor=actor]]/e/[rkey]/+page.server.ts
··· 5 5 import { createCache, type CachedProfile } from '$lib/cache'; 6 6 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 7 7 import { env as publicEnv } from '$env/dynamic/public'; 8 + import { getActor } from '$lib/actor'; 8 9 9 10 export async function load({ params, platform, request }) { 10 11 const { rkey } = params; 11 12 12 13 const cache = createCache(platform); 13 14 14 - const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 15 - 16 - let actor: ActorIdentifier | undefined = params.actor; 17 - 18 - if (!actor) { 19 - const kv = platform?.env?.CUSTOM_DOMAINS; 20 - 21 - if (kv && customDomain) { 22 - try { 23 - const did = await kv.get(customDomain); 24 - 25 - if (did) actor = did as ActorIdentifier; 26 - } catch (error) { 27 - console.error('failed to get custom domain kv', error); 28 - } 29 - } else { 30 - actor = publicEnv.PUBLIC_HANDLE as ActorIdentifier; 31 - } 32 - } else if (customDomain && params.actor) { 33 - actor = undefined; 34 - } 35 - 36 - const did = isHandle(actor) ? await resolveHandle({ handle: actor }) : actor; 15 + const did = await getActor({ request, paramActor: params.actor, platform }); 37 16 38 17 if (!did || !rkey) { 39 18 throw error(404, 'Event not found');
+16 -6
src/routes/[[actor=actor]]/e/[rkey]/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import type { EventData } from '$lib/cards/social/EventCard'; 3 + import { getCDNImageBlobUrl } from '$lib/atproto'; 3 4 import { Avatar as FoxAvatar, Badge } from '@foxui/core'; 4 5 import Avatar from 'svelte-boring-avatars'; 5 6 import EventRsvp from './EventRsvp.svelte'; 7 + import { page } from '$app/state'; 6 8 7 9 let { data } = $props(); 8 10 ··· 80 82 let headerImage = $derived.by(() => { 81 83 if (!eventData.media || eventData.media.length === 0) return null; 82 84 const media = eventData.media.find((m) => m.role === 'thumbnail'); 83 - if (!media?.content?.ref?.$link) return null; 84 - return { 85 - url: `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${media.content.ref.$link}@jpeg`, 86 - alt: media.alt || eventData.name 87 - }; 85 + if (!media?.content) return null; 86 + const url = getCDNImageBlobUrl({ did, blob: media.content, type: 'jpeg' }); 87 + if (!url) return null; 88 + return { url, alt: media.alt || eventData.name }; 88 89 }); 89 90 90 91 let eventUrl = $derived(eventData.url || `https://smokesignal.events/${did}/${rkey}`); 91 92 let eventUri = $derived(`at://${did}/community.lexicon.calendar.event/${rkey}`); 93 + 94 + let ogImageUrl = $derived(`${page.url.origin}${page.url.pathname}/og.png`); 92 95 </script> 93 96 94 97 <svelte:head> 95 98 <title>{eventData.name}</title> 96 99 <meta name="description" content={eventData.description || `Event: ${eventData.name}`} /> 100 + <meta property="og:title" content={eventData.name} /> 101 + <meta property="og:description" content={eventData.description || `Event: ${eventData.name}`} /> 102 + <meta property="og:image" content={ogImageUrl} /> 103 + <meta name="twitter:card" content="summary_large_image" /> 104 + <meta name="twitter:title" content={eventData.name} /> 105 + <meta name="twitter:description" content={eventData.description || `Event: ${eventData.name}`} /> 106 + <meta name="twitter:image" content={ogImageUrl} /> 97 107 </svelte:head> 98 108 99 - <div class="bg-base-50 dark:bg-base-950 min-h-screen px-8 py-8 sm:py-12"> 109 + <div class="bg-base-50 dark:bg-base-950 min-h-screen px-6 py-12 sm:py-12"> 100 110 <div class="mx-auto max-w-4xl"> 101 111 <!-- Two-column layout: image left, details right --> 102 112 <div
+66
src/routes/[[actor=actor]]/e/[rkey]/og.png/+server.ts
··· 1 + import { getCDNImageBlobUrl, resolveHandle } from '$lib/atproto/methods.js'; 2 + import { env as publicEnv } from '$env/dynamic/public'; 3 + 4 + import type { ActorIdentifier } from '@atcute/lexicons'; 5 + import { isHandle } from '@atcute/lexicons/syntax'; 6 + import type { EventData } from '$lib/cards/social/EventCard'; 7 + import { ImageResponse } from '@ethercorps/sveltekit-og'; 8 + import { error } from '@sveltejs/kit'; 9 + import EventOgImage from './EventOgImage.svelte'; 10 + import { getActor } from '$lib/actor'; 11 + 12 + function formatDate(dateStr: string): string { 13 + const date = new Date(dateStr); 14 + const weekday = date.toLocaleDateString('en-US', { weekday: 'long' }); 15 + const month = date.toLocaleDateString('en-US', { month: 'long' }); 16 + const day = date.getDate(); 17 + return `${weekday}, ${month} ${day}`; 18 + } 19 + 20 + export async function GET({ params, platform, request }) { 21 + const { rkey } = params; 22 + 23 + const did = await getActor({ request, paramActor: params.actor, platform }); 24 + 25 + if (!did || !rkey) { 26 + throw error(404, 'Event not found'); 27 + } 28 + 29 + let eventData: EventData; 30 + 31 + try { 32 + const eventResponse = await fetch( 33 + `https://smokesignal.events/xrpc/community.lexicon.calendar.GetEvent?repository=${encodeURIComponent(did)}&record_key=${encodeURIComponent(rkey)}` 34 + ); 35 + 36 + if (!eventResponse.ok) { 37 + throw error(404, 'Event not found'); 38 + } 39 + 40 + eventData = await eventResponse.json(); 41 + } catch (e) { 42 + if (e && typeof e === 'object' && 'status' in e) throw e; 43 + throw error(404, 'Event not found'); 44 + } 45 + 46 + const dateStr = formatDate(eventData.startsAt); 47 + 48 + let thumbnailUrl: string | null = null; 49 + if (eventData.media && eventData.media.length > 0) { 50 + const media = eventData.media.find((m) => m.role === 'thumbnail'); 51 + if (media?.content) { 52 + thumbnailUrl = getCDNImageBlobUrl({ did, blob: media.content, type: 'jpeg' }) ?? null; 53 + } 54 + } 55 + 56 + return new ImageResponse( 57 + EventOgImage, 58 + { width: 1200, height: 630, debug: false }, 59 + { 60 + name: eventData.name, 61 + dateStr, 62 + thumbnailUrl, 63 + rkey 64 + } 65 + ); 66 + }
+63
src/routes/[[actor=actor]]/e/[rkey]/og.png/EventOgImage.svelte
··· 1 + <script lang="ts"> 2 + import Avatar from 'svelte-boring-avatars'; 3 + 4 + let { 5 + name, 6 + dateStr, 7 + thumbnailUrl, 8 + rkey 9 + }: { 10 + name: string; 11 + dateStr: string; 12 + thumbnailUrl: string | null; 13 + rkey: string; 14 + } = $props(); 15 + </script> 16 + 17 + <div class="flex h-full w-full bg-neutral-900 p-0"> 18 + <div class="flex h-full shrink-0 items-center px-8"> 19 + <div class="flex overflow-hidden rounded-3xl"> 20 + {#if thumbnailUrl} 21 + <img src={thumbnailUrl} alt={name} width="420" height="420" style="object-fit: cover;" /> 22 + {:else} 23 + <Avatar 24 + size={420} 25 + name={rkey} 26 + variant="marble" 27 + colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']} 28 + square 29 + /> 30 + {/if} 31 + </div> 32 + </div> 33 + 34 + <div class="flex min-w-0 flex-1 flex-col justify-center p-12"> 35 + <h1 36 + class="text-7xl leading-tight font-bold text-neutral-50" 37 + style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word;" 38 + > 39 + {name} 40 + </h1> 41 + 42 + <div class="mt-8 flex items-center"> 43 + <svg 44 + width="28" 45 + height="28" 46 + viewBox="0 0 24 24" 47 + fill="none" 48 + xmlns="http://www.w3.org/2000/svg" 49 + > 50 + <path 51 + d="M8 2v3M16 2v3M3.5 9.09h17M21 8.5v8c0 3-1.5 5-5 5H8c-3.5 0-5-2-5-5v-8c0-3 1.5-5 5-5h8c3.5 0 5 2 5 5Z" 52 + stroke="#a3a3a3" 53 + stroke-width="1.5" 54 + stroke-miterlimit="10" 55 + stroke-linecap="round" 56 + stroke-linejoin="round" 57 + /> 58 + </svg> 59 + <span class="ml-3 text-2xl text-neutral-300">{dateStr}</span> 60 + </div> 61 + 62 + </div> 63 + </div>