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.

Add errorScene copy module for SP12 error pages

+136
+64
src/lib/reader/errors.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + import { errorScene, type ErrorKind } from './errors'; 3 + 4 + const ALL_KINDS: ErrorKind[] = [ 5 + 'writer-not-found', 6 + 'publication-not-found', 7 + 'article-not-found', 8 + 'not-found', 9 + 'server-error', 10 + ]; 11 + 12 + describe( 'errorScene', () => { 13 + it( 'returns 404 for every not-found kind and 500 for server-error', () => { 14 + expect( errorScene( 'writer-not-found' ).status ).toBe( 404 ); 15 + expect( errorScene( 'publication-not-found' ).status ).toBe( 404 ); 16 + expect( errorScene( 'article-not-found' ).status ).toBe( 404 ); 17 + expect( errorScene( 'not-found' ).status ).toBe( 404 ); 18 + expect( errorScene( 'server-error' ).status ).toBe( 500 ); 19 + } ); 20 + 21 + it( 'interpolates the handle into the writer-not-found copy', () => { 22 + const scene = errorScene( 'writer-not-found', { handle: 'jeherve.com' } ); 23 + expect( scene.heading ).toBe( 'No writer at @jeherve.com' ); 24 + } ); 25 + 26 + it( 'interpolates handle + slug into the publication-not-found copy', () => { 27 + const scene = errorScene( 'publication-not-found', { 28 + handle: 'jeherve.com', 29 + slug: 'tribulations', 30 + } ); 31 + expect( scene.subline ).toContain( '@jeherve.com' ); 32 + expect( scene.subline ).toContain( 'tribulations' ); 33 + } ); 34 + 35 + it( 'interpolates the publication name into the article-not-found copy', () => { 36 + const scene = errorScene( 'article-not-found', { 37 + publicationName: 'Tribulations of a Software Engineer', 38 + } ); 39 + expect( scene.subline ).toContain( 'Tribulations of a Software Engineer' ); 40 + } ); 41 + 42 + it( 'falls back gracefully when the article publication name is missing', () => { 43 + const scene = errorScene( 'article-not-found' ); 44 + expect( scene.subline ).toContain( 'this publication' ); 45 + } ); 46 + 47 + it( 'keeps the horizon metaphor for the generic not-found heading', () => { 48 + expect( errorScene( 'not-found' ).heading ).toBe( 49 + 'This page set below the horizon' 50 + ); 51 + } ); 52 + 53 + it( 'never emits an em dash in any copy field (house rule)', () => { 54 + for ( const kind of ALL_KINDS ) { 55 + const scene = errorScene( kind, { 56 + handle: 'x', 57 + slug: 'y', 58 + publicationName: 'Z', 59 + } ); 60 + const text = `${ scene.eyebrow }${ scene.heading }${ scene.subline }`; 61 + expect( text ).not.toContain( '—' ); // em dash 62 + } 63 + } ); 64 + } );
+72
src/lib/reader/errors.ts
··· 1 + /** 2 + * Source of truth for reader error-page copy + HTTP status (SP12). 3 + * 4 + * Pure and dependency-free (no @wordpress, no network) so it can run on the read 5 + * path (AGENTS.md #3) and be unit-tested directly. Copy avoids em dashes by house 6 + * rule; the eyebrow uses a middle-dot kicker in SkyPress's editorial voice. 7 + */ 8 + export type ErrorKind = 9 + | 'writer-not-found' 10 + | 'publication-not-found' 11 + | 'article-not-found' 12 + | 'not-found' 13 + | 'server-error'; 14 + 15 + export interface ErrorContext { 16 + handle?: string; 17 + slug?: string; 18 + publicationName?: string; 19 + } 20 + 21 + export interface ErrorSceneCopy { 22 + status: number; 23 + eyebrow: string; 24 + heading: string; 25 + subline: string; 26 + } 27 + 28 + export function errorScene( 29 + kind: ErrorKind, 30 + context: ErrorContext = {} 31 + ): ErrorSceneCopy { 32 + switch ( kind ) { 33 + case 'writer-not-found': 34 + return { 35 + status: 404, 36 + eyebrow: '404 · no one by that name', 37 + heading: `No writer at @${ context.handle ?? '' }`, 38 + subline: 39 + 'Nobody on the network goes by that handle yet. It might have a typo.', 40 + }; 41 + case 'publication-not-found': 42 + return { 43 + status: 404, 44 + eyebrow: '404 · publication not found', 45 + heading: 'No publication by that name', 46 + subline: `@${ context.handle ?? '' } hasn't published anything under "${ context.slug ?? '' }".`, 47 + }; 48 + case 'article-not-found': 49 + return { 50 + status: 404, 51 + eyebrow: '404 · story not found', 52 + heading: 'This story set below the horizon', 53 + subline: `It's no longer part of ${ context.publicationName ?? 'this publication' }. It may have been unpublished or moved.`, 54 + }; 55 + case 'server-error': 56 + return { 57 + status: 500, 58 + eyebrow: '500 · overcast', 59 + heading: "The sky's a bit cloudy right now", 60 + subline: 61 + 'Something went wrong on our end. Give it a moment, then try again.', 62 + }; 63 + case 'not-found': 64 + default: 65 + return { 66 + status: 404, 67 + eyebrow: '404 · off the map', 68 + heading: 'This page set below the horizon', 69 + subline: "There's nothing to read at this address.", 70 + }; 71 + } 72 + }