A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

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

Render ErrorScene on the writer route for SP12

+140 -90
+117 -90
src/pages/[author]/index.astro
··· 6 6 import { fetchActorProfile } from '../../lib/reader/profile'; 7 7 import { buildGetBlobUrl } from '../../lib/media/blob'; 8 8 import CreatePublicationCta from '../../components/CreatePublicationCta.tsx'; 9 + import ErrorScene from '../../components/ErrorScene.astro'; 10 + import { errorScene } from '../../lib/reader/errors'; 11 + import type { ErrorSceneCopy } from '../../lib/reader/errors'; 9 12 10 13 export const prerender = false; 11 14 12 15 const { author } = Astro.params; 16 + 17 + let error: ErrorSceneCopy | null = null; 18 + const handle = author?.startsWith( '@' ) ? author.slice( 1 ) : ''; 19 + 20 + let did = ''; 21 + let pdsUrl = ''; 22 + let profile: Awaited< ReturnType< typeof fetchActorProfile > > | null = null; 23 + let publications: Awaited< ReturnType< typeof listReaderPublications > > = []; 24 + let authorName = ''; 25 + let initial = ''; 26 + 13 27 if ( ! author || ! author.startsWith( '@' ) ) { 14 - return new Response( 'Not found', { status: 404 } ); 28 + error = errorScene( 'not-found' ); 15 29 } 16 - const handle = author.slice( 1 ); 17 30 18 - const resolved = await resolveAuthor( handle ); 19 - if ( ! resolved ) { 20 - return new Response( `Could not resolve @${ handle }`, { status: 404 } ); 31 + const resolved = error ? null : await resolveAuthor( handle ); 32 + if ( ! error && ! resolved ) { 33 + error = errorScene( 'writer-not-found', { handle } ); 21 34 } 22 - const { did, pdsUrl } = resolved; 23 35 24 - const [ profile, publications ] = await Promise.all( [ 25 - fetchActorProfile( pdsUrl, did ), 26 - listReaderPublications( pdsUrl, did ), 27 - ] ); 36 + if ( ! error && resolved ) { 37 + did = resolved.did; 38 + pdsUrl = resolved.pdsUrl; 39 + 40 + [ profile, publications ] = await Promise.all( [ 41 + fetchActorProfile( pdsUrl, did ), 42 + listReaderPublications( pdsUrl, did ), 43 + ] ); 28 44 29 - const authorName = profile.displayName ?? `@${ handle }`; 30 - const initial = authorName.replace( /^@/, '' ).charAt( 0 ).toUpperCase(); 45 + authorName = profile.displayName ?? `@${ handle }`; 46 + initial = authorName.replace( /^@/, '' ).charAt( 0 ).toUpperCase(); 47 + } 48 + 49 + if ( error ) { 50 + Astro.response.status = error.status; 51 + } 31 52 --- 32 53 33 - <Base title={`${ authorName } — SkyPress`} description={profile.description ?? undefined}> 34 - <header class="masthead"> 35 - <a href="/"><Logo /></a> 36 - </header> 54 + { 55 + error ? ( 56 + <ErrorScene eyebrow={error.eyebrow} heading={error.heading} subline={error.subline} /> 57 + ) : ( 58 + <Base title={`${ authorName } — SkyPress`} description={profile!.description ?? undefined}> 59 + <header class="masthead"> 60 + <a href="/"><Logo /></a> 61 + </header> 37 62 38 - <main class="author"> 39 - <div class="author__hero"> 40 - {profile.banner && ( 41 - <div class="author__cover" style={`background-image:url("${ profile.banner }")`} aria-hidden="true"></div> 42 - )} 43 - <div class={`author__identity${ profile.banner ? ' author__identity--overlap' : '' }`}> 44 - {profile.avatar ? ( 45 - <img class="author__avatar" src={profile.avatar} alt="" width="96" height="96" /> 63 + <main class="author"> 64 + <div class="author__hero"> 65 + {profile!.banner && ( 66 + <div class="author__cover" style={`background-image:url("${ profile!.banner }")`} aria-hidden="true"></div> 67 + )} 68 + <div class={`author__identity${ profile!.banner ? ' author__identity--overlap' : '' }`}> 69 + {profile!.avatar ? ( 70 + <img class="author__avatar" src={profile!.avatar} alt="" width="96" height="96" /> 71 + ) : ( 72 + <span class="author__avatar author__avatar--fallback" aria-hidden="true">{initial}</span> 73 + )} 74 + <h1 class="author__name">{authorName}</h1> 75 + <p class="author__handle"> 76 + <a 77 + class="author__handle-link" 78 + href={`https://bsky.app/profile/${ handle }`} 79 + target="_blank" 80 + rel="noopener noreferrer" 81 + aria-label={`View @${ handle } on Bluesky`} 82 + > 83 + @{handle} 84 + <svg 85 + class="author__handle-arrow" 86 + width="11" 87 + height="11" 88 + viewBox="0 0 24 24" 89 + fill="none" 90 + stroke="currentColor" 91 + stroke-width="2.5" 92 + stroke-linecap="round" 93 + stroke-linejoin="round" 94 + aria-hidden="true" 95 + > 96 + <path d="M7 17 17 7" /> 97 + <path d="M8 7h9v9" /> 98 + </svg> 99 + </a> 100 + </p> 101 + {profile!.description && <p class="author__bio">{profile!.description}</p>} 102 + </div> 103 + </div> 104 + 105 + <h2 class="author__heading">Publications</h2> 106 + {publications.length === 0 ? ( 107 + <div class="author__emptyblock"> 108 + <p class="author__empty">No SkyPress publications yet.</p> 109 + <CreatePublicationCta client:only="react" profileDid={did} /> 110 + </div> 46 111 ) : ( 47 - <span class="author__avatar author__avatar--fallback" aria-hidden="true">{initial}</span> 112 + <ul class="author__list"> 113 + {publications.map( ( pub ) => { 114 + const logoUrl = pub.icon 115 + ? buildGetBlobUrl( pdsUrl, did, pub.icon.ref.$link ) 116 + : null; 117 + return ( 118 + <li class="author__item"> 119 + <a class="author__pub" href={`/@${ handle }/${ pub.slug }`}> 120 + {logoUrl ? ( 121 + <img class="author__publogo" src={logoUrl} alt="" width="52" height="52" /> 122 + ) : ( 123 + <span class="author__publogo author__publogo--fallback" aria-hidden="true"> 124 + {pub.name.charAt( 0 ).toUpperCase()} 125 + </span> 126 + )} 127 + <span class="author__pubtext"> 128 + <span class="author__pubname">{pub.name}</span> 129 + {pub.description && <span class="author__pubdesc">{pub.description}</span>} 130 + </span> 131 + </a> 132 + </li> 133 + ); 134 + } )} 135 + </ul> 48 136 )} 49 - <h1 class="author__name">{authorName}</h1> 50 - <p class="author__handle"> 51 - <a 52 - class="author__handle-link" 53 - href={`https://bsky.app/profile/${ handle }`} 54 - target="_blank" 55 - rel="noopener noreferrer" 56 - aria-label={`View @${ handle } on Bluesky`} 57 - > 58 - @{handle} 59 - <svg 60 - class="author__handle-arrow" 61 - width="11" 62 - height="11" 63 - viewBox="0 0 24 24" 64 - fill="none" 65 - stroke="currentColor" 66 - stroke-width="2.5" 67 - stroke-linecap="round" 68 - stroke-linejoin="round" 69 - aria-hidden="true" 70 - > 71 - <path d="M7 17 17 7" /> 72 - <path d="M8 7h9v9" /> 73 - </svg> 74 - </a> 75 - </p> 76 - {profile.description && <p class="author__bio">{profile.description}</p>} 77 - </div> 78 - </div> 79 - 80 - <h2 class="author__heading">Publications</h2> 81 - {publications.length === 0 ? ( 82 - <div class="author__emptyblock"> 83 - <p class="author__empty">No SkyPress publications yet.</p> 84 - <CreatePublicationCta client:only="react" profileDid={did} /> 85 - </div> 86 - ) : ( 87 - <ul class="author__list"> 88 - {publications.map( ( pub ) => { 89 - const logoUrl = pub.icon 90 - ? buildGetBlobUrl( pdsUrl, did, pub.icon.ref.$link ) 91 - : null; 92 - return ( 93 - <li class="author__item"> 94 - <a class="author__pub" href={`/@${ handle }/${ pub.slug }`}> 95 - {logoUrl ? ( 96 - <img class="author__publogo" src={logoUrl} alt="" width="52" height="52" /> 97 - ) : ( 98 - <span class="author__publogo author__publogo--fallback" aria-hidden="true"> 99 - {pub.name.charAt( 0 ).toUpperCase()} 100 - </span> 101 - )} 102 - <span class="author__pubtext"> 103 - <span class="author__pubname">{pub.name}</span> 104 - {pub.description && <span class="author__pubdesc">{pub.description}</span>} 105 - </span> 106 - </a> 107 - </li> 108 - ); 109 - } )} 110 - </ul> 111 - )} 112 - </main> 113 - </Base> 137 + </main> 138 + </Base> 139 + ) 140 + } 114 141 115 142 <style> 116 143 .masthead {
+23
src/pages/[author]/index.meta.test.ts
··· 1 + import { readFileSync } from 'node:fs'; 2 + import { dirname, join } from 'node:path'; 3 + import { fileURLToPath } from 'node:url'; 4 + import { describe, expect, it } from 'vitest'; 5 + 6 + const here = dirname( fileURLToPath( import.meta.url ) ); 7 + const page = readFileSync( join( here, './index.astro' ), 'utf8' ); 8 + 9 + describe( 'writer page error wiring', () => { 10 + it( 'renders ErrorScene instead of plain-text 404s', () => { 11 + expect( page ).toMatch( /import ErrorScene from '[^']*components\/ErrorScene.astro'/ ); 12 + expect( page ).toMatch( /import\s*\{\s*errorScene/ ); 13 + expect( page ).toMatch( /<ErrorScene/ ); 14 + expect( page ).not.toMatch( /new Response\(\s*'Not found'/ ); 15 + expect( page ).not.toMatch( /new Response\(\s*`Could not resolve/ ); 16 + } ); 17 + 18 + it( 'maps the writer-not-found failure and sets the response status', () => { 19 + expect( page ).toMatch( /errorScene\(\s*'writer-not-found'/ ); 20 + expect( page ).toMatch( /errorScene\(\s*'not-found'/ ); 21 + expect( page ).toMatch( /Astro\.response\.status\s*=/ ); 22 + } ); 23 + } );