my website at ewancroft.uk
6
fork

Configure Feed

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

refactor: replace local impls with @ewanc26 package re-exports

+46 -243
+4 -4
package.json
··· 31 31 }, 32 32 "dependencies": { 33 33 "@atproto/api": "^0.18.21", 34 - "@ewanc26/atproto": "^0.2.2", 34 + "@ewanc26/atproto": "^0.2.3", 35 35 "@ewanc26/noise-avatar": "^0.1.0", 36 - "@ewanc26/supporters": "^0.1.5", 36 + "@ewanc26/supporters": "^0.1.6", 37 37 "@ewanc26/tid": "^1.1.1", 38 - "@ewanc26/ui": "^0.1.7", 39 - "@ewanc26/utils": "^0.1.2", 38 + "@ewanc26/ui": "^0.1.8", 39 + "@ewanc26/utils": "^0.1.3", 40 40 "@lucide/svelte": "^0.554.0", 41 41 "hls.js": "^1.6.15" 42 42 }
+21 -21
pnpm-lock.yaml
··· 12 12 specifier: ^0.18.21 13 13 version: 0.18.21 14 14 '@ewanc26/atproto': 15 - specifier: ^0.2.2 16 - version: 0.2.2(@atproto/api@0.18.21) 15 + specifier: ^0.2.3 16 + version: 0.2.3(@atproto/api@0.18.21) 17 17 '@ewanc26/noise-avatar': 18 18 specifier: ^0.1.0 19 19 version: 0.1.0 20 20 '@ewanc26/supporters': 21 - specifier: ^0.1.5 22 - version: 0.1.5(@atproto/api@0.18.21)(svelte@5.53.8) 21 + specifier: ^0.1.6 22 + version: 0.1.6(@atproto/api@0.18.21)(svelte@5.53.8) 23 23 '@ewanc26/tid': 24 24 specifier: ^1.1.1 25 25 version: 1.1.1 26 26 '@ewanc26/ui': 27 - specifier: ^0.1.7 28 - version: 0.1.7(@atproto/api@0.18.21)(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.8)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.8)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.8)(tailwindcss@4.2.1) 27 + specifier: ^0.1.8 28 + version: 0.1.8(@atproto/api@0.18.21)(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.8)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.8)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.8)(tailwindcss@4.2.1) 29 29 '@ewanc26/utils': 30 - specifier: ^0.1.2 31 - version: 0.1.2 30 + specifier: ^0.1.3 31 + version: 0.1.3 32 32 '@lucide/svelte': 33 33 specifier: ^0.554.0 34 34 version: 0.554.0(svelte@5.53.8) ··· 417 417 cpu: [x64] 418 418 os: [win32] 419 419 420 - '@ewanc26/atproto@0.2.2': 421 - resolution: {integrity: sha512-rR0p30j/7vdZMcP/TrVewSF+HQBJYmhufHa76CwBf+TBKIenG/nZwqot0nDKDyHCyeb9p97dveDcYryz3908pw==} 420 + '@ewanc26/atproto@0.2.3': 421 + resolution: {integrity: sha512-jCZifBFiClo+tZZJGQoQeECQQawYrHqAZBZrieSgolUm4OUI7FfKc1s6/uYhFWWmjhoFoYoNrtJVCRl/5BIZiQ==} 422 422 peerDependencies: 423 423 '@atproto/api': '>=0.13.0' 424 424 425 425 '@ewanc26/noise-avatar@0.1.0': 426 426 resolution: {integrity: sha512-T7a1zYaie2GkL6Fe3OKWDxJr68pu/nWrObwI8LPjRsaJiV432Tt9l5pGppb869CfNYEtUTLoGhgXL+RrPnNBKw==} 427 427 428 - '@ewanc26/supporters@0.1.5': 429 - resolution: {integrity: sha512-Kl2TUY/b/Y99rbOqcm9HJqBGlHRnnBiF9BIxDJYW4SwxfmnwWEeIwM7mAjRVb2s23CQwvHAQUz/JdX8R6W7Hww==} 428 + '@ewanc26/supporters@0.1.6': 429 + resolution: {integrity: sha512-L+lF6QZqWiNy+jy3AdJ5kfBHN8xShDJUsRiQYzLLELQk2ZCCqnEsFxwTY4B/A3ECBbPpC0kYpj12XX6dGZpBCQ==} 430 430 peerDependencies: 431 431 '@atproto/api': '>=0.13.0' 432 432 svelte: ^5.0.0 ··· 434 434 '@ewanc26/tid@1.1.1': 435 435 resolution: {integrity: sha512-u/Ks251B+5Dy1lx1PC814mWpMg2TNly4b+bHMdLCz4TwiArD/se3iApL+L6pl16eKQQlqWS2yrVKhhSwThC3WA==} 436 436 437 - '@ewanc26/ui@0.1.7': 438 - resolution: {integrity: sha512-3YTH6f9vv124inztpJSZ77FXtLUnag8Da4H0EfeREEH5nOECt+/dfkmOtzjBw4+heAtiGjTELv3sLlRXqtzVQA==} 437 + '@ewanc26/ui@0.1.8': 438 + resolution: {integrity: sha512-0fBhp2NEyShULtXwCSyc5ykzozZrRDM2qyJfyw+hX0oYDZXfVBsysyYwXwlGB7Ky4ll/Odq6EwUNRvy2VFgtQg==} 439 439 peerDependencies: 440 440 '@sveltejs/kit': '>=2.8.3' 441 441 svelte: '>=5.0.0' 442 442 tailwindcss: '>=4.0.0' 443 443 444 - '@ewanc26/utils@0.1.2': 445 - resolution: {integrity: sha512-IPCWmxof4o+tPDTFx0Zif3PXrCnnuSL7myHHqD3712HWOYSQs8CBFCq1OX1A+pkRpXaf4GH8q2BZa+lVm8vWZg==} 444 + '@ewanc26/utils@0.1.3': 445 + resolution: {integrity: sha512-3KG4jwNr2BsgxcGB8qrhfpp8Z0dxIA5tBc133HGS2edJNcDz3yLz81VKMfcFO+uSMT1LbtuMdlMImUNawsE1Dw==} 446 446 447 447 '@isaacs/fs-minipass@4.0.1': 448 448 resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} ··· 1505 1505 '@esbuild/win32-x64@0.27.3': 1506 1506 optional: true 1507 1507 1508 - '@ewanc26/atproto@0.2.2(@atproto/api@0.18.21)': 1508 + '@ewanc26/atproto@0.2.3(@atproto/api@0.18.21)': 1509 1509 dependencies: 1510 1510 '@atproto/api': 0.18.21 1511 1511 1512 1512 '@ewanc26/noise-avatar@0.1.0': {} 1513 1513 1514 - '@ewanc26/supporters@0.1.5(@atproto/api@0.18.21)(svelte@5.53.8)': 1514 + '@ewanc26/supporters@0.1.6(@atproto/api@0.18.21)(svelte@5.53.8)': 1515 1515 dependencies: 1516 1516 '@atproto/api': 0.18.21 1517 1517 '@ewanc26/tid': 1.1.1 ··· 1519 1519 1520 1520 '@ewanc26/tid@1.1.1': {} 1521 1521 1522 - '@ewanc26/ui@0.1.7(@atproto/api@0.18.21)(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.8)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.8)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.8)(tailwindcss@4.2.1)': 1522 + '@ewanc26/ui@0.1.8(@atproto/api@0.18.21)(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.8)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.8)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.8)(tailwindcss@4.2.1)': 1523 1523 dependencies: 1524 1524 '@lucide/svelte': 0.554.0(svelte@5.53.8) 1525 1525 '@sveltejs/kit': 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.8)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.8)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)) 1526 1526 svelte: 5.53.8 1527 1527 tailwindcss: 4.2.1 1528 1528 optionalDependencies: 1529 - '@ewanc26/atproto': 0.2.2(@atproto/api@0.18.21) 1529 + '@ewanc26/atproto': 0.2.3(@atproto/api@0.18.21) 1530 1530 transitivePeerDependencies: 1531 1531 - '@atproto/api' 1532 1532 1533 - '@ewanc26/utils@0.1.2': {} 1533 + '@ewanc26/utils@0.1.3': {} 1534 1534 1535 1535 '@isaacs/fs-minipass@4.0.1': 1536 1536 dependencies:
+6 -85
src/lib/config/slugs.ts
··· 1 + import { normalizeSlug, isTidFormat } from '@ewanc26/utils'; 1 2 import { slugMappings, type SlugMapping, type PublicationPlatform } from '$lib/data/slug-mappings'; 2 3 3 - /** 4 - * Normalize a slug to be URI-compatible 5 - * 6 - * Transformations: 7 - * - Convert to lowercase 8 - * - Replace spaces with hyphens 9 - * - Remove all characters except alphanumeric, hyphens, and underscores 10 - * - Collapse multiple hyphens into single hyphen 11 - * - Remove leading/trailing hyphens 12 - * 13 - * @param slug - The slug to normalize 14 - * @returns URI-compatible slug 15 - * 16 - * @example 17 - * normalizeSlug('My Blog Post!') // 'my-blog-post' 18 - * normalizeSlug('Hello World') // 'hello-world' 19 - * normalizeSlug('Test---Slug___') // 'test-slug' 20 - */ 21 - export function normalizeSlug(slug: string): string { 22 - return slug 23 - .toLowerCase() 24 - .trim() 25 - .replace(/\s+/g, '-') // Replace spaces with hyphens 26 - .replace(/[^a-z0-9\-_]/g, '') // Remove non-alphanumeric except hyphens and underscores 27 - .replace(/-+/g, '-') // Collapse multiple hyphens 28 - .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens 29 - } 4 + export { normalizeSlug, isTidFormat }; 30 5 31 - /** 32 - * Get publication info from slug 33 - * Automatically normalizes the slug before lookup 34 - * 35 - * @param slug - The slug to look up (will be normalized) 36 - * @returns Object with rkey and platform, or null if not found 37 - */ 38 6 export function getPublicationFromSlug( 39 7 slug: string 40 8 ): { rkey: string; platform: PublicationPlatform } | null { 41 9 const normalizedSlug = normalizeSlug(slug); 42 10 const mapping = slugMappings.find((m) => normalizeSlug(m.slug) === normalizedSlug); 43 11 if (!mapping) return null; 44 - return { 45 - rkey: mapping.publicationRkey, 46 - platform: mapping.platform 47 - }; 12 + return { rkey: mapping.publicationRkey, platform: mapping.platform }; 48 13 } 49 14 50 - /** 51 - * Get publication rkey from slug (backwards compatibility) 52 - * Automatically normalizes the slug before lookup 53 - * 54 - * @param slug - The slug to look up (will be normalized) 55 - * @returns The publication rkey or null if not found 56 - */ 57 15 export function getPublicationRkeyFromSlug(slug: string): string | null { 58 - const result = getPublicationFromSlug(slug); 59 - return result?.rkey || null; 16 + return getPublicationFromSlug(slug)?.rkey || null; 60 17 } 61 18 62 - /** 63 - * Get slug from publication rkey 64 - * 65 - * @param rkey - The publication rkey 66 - * @returns The slug or null if not found 67 - */ 68 19 export function getSlugFromPublicationRkey(rkey: string): string | null { 69 - const mapping = slugMappings.find((m) => m.publicationRkey === rkey); 70 - return mapping?.slug || null; 20 + return slugMappings.find((m) => m.publicationRkey === rkey)?.slug || null; 71 21 } 72 22 73 - /** 74 - * Get all configured slugs (normalized) 75 - * 76 - * @returns Array of normalized slugs 77 - */ 78 23 export function getAllSlugs(): string[] { 79 24 return slugMappings.map((m) => normalizeSlug(m.slug)); 80 25 } 81 26 82 - /** 83 - * Get all slug mappings with normalized slugs 84 - * 85 - * @returns Array of slug mappings with normalized slugs 86 - */ 87 27 export function getAllSlugMappings(): SlugMapping[] { 88 - return slugMappings.map((m) => ({ 89 - ...m, 90 - slug: normalizeSlug(m.slug) 91 - })); 92 - } 93 - 94 - /** 95 - * Check if a string is a valid TID (AT Protocol record key) 96 - * 97 - * @param str - String to check 98 - * @returns True if the string matches TID format (12-16 alphanumeric characters) 99 - */ 100 - export function isTidFormat(str: string): boolean { 101 - const tidPattern = /^[a-zA-Z0-9]{12,16}$/; 102 - return tidPattern.test(str); 28 + return slugMappings.map((m) => ({ ...m, slug: normalizeSlug(m.slug) })); 103 29 } 104 30 105 - /** 106 - * Get all publication rkeys from slug mappings 107 - * 108 - * @returns Array of publication rkeys 109 - */ 110 31 export function getAllPublicationRkeys(): string[] { 111 32 return slugMappings.map((m) => m.publicationRkey); 112 33 }
+3 -6
src/lib/data/navItems.ts
··· 1 - export interface NavItem { 2 - href: string; 3 - label: string; 4 - // The property holds the Lucide component name (e.g., 'Home') 5 - iconPath: string; 6 - } 1 + import type { NavItem } from '@ewanc26/ui'; 2 + 3 + export type { NavItem }; 7 4 8 5 export const navItems: NavItem[] = [ 9 6 { href: '/', label: 'Home', iconPath: 'Home' },
+1 -41
src/lib/helper/metaTags.ts
··· 1 - import type { SiteMetadata } from './siteMeta'; 2 - 3 - /** 4 - * Generates an array of meta tag objects for Svelte head. 5 - * Falls back to site defaults if a property is missing. 6 - */ 7 - export function generateMetaTags(meta: SiteMetadata, defaults: SiteMetadata) { 8 - const finalMeta: SiteMetadata = { 9 - title: meta.title || defaults.title, 10 - description: meta.description || defaults.description, 11 - keywords: meta.keywords || defaults.keywords, 12 - url: meta.url || defaults.url, 13 - image: meta.image || defaults.image, 14 - imageWidth: meta.imageWidth || defaults.imageWidth, 15 - imageHeight: meta.imageHeight || defaults.imageHeight 16 - }; 17 - 18 - return [ 19 - { name: 'description', content: finalMeta.description }, 20 - { name: 'keywords', content: finalMeta.keywords }, 21 - 22 - { property: 'og:type', content: 'website' }, 23 - { property: 'og:url', content: finalMeta.url }, 24 - { property: 'og:title', content: finalMeta.title }, 25 - { property: 'og:description', content: finalMeta.description }, 26 - { property: 'og:site_name', content: defaults.title }, // always site title for OG 27 - { property: 'og:image', content: finalMeta.image }, 28 - ...(finalMeta.imageWidth 29 - ? [{ property: 'og:image:width', content: finalMeta.imageWidth.toString() }] 30 - : []), 31 - ...(finalMeta.imageHeight 32 - ? [{ property: 'og:image:height', content: finalMeta.imageHeight.toString() }] 33 - : []), 34 - 35 - { name: 'twitter:card', content: 'summary_large_image' }, 36 - { name: 'twitter:url', content: finalMeta.url }, 37 - { name: 'twitter:title', content: finalMeta.title }, 38 - { name: 'twitter:description', content: finalMeta.description }, 39 - { name: 'twitter:image', content: finalMeta.image } 40 - ]; 41 - } 1 + export { generateMetaTags, createSiteMeta } from '@ewanc26/ui';
+4 -21
src/lib/helper/siteMeta.ts
··· 5 5 PUBLIC_SITE_KEYWORDS, 6 6 PUBLIC_SITE_URL 7 7 } from '$env/static/public'; 8 + import type { SiteMetadata } from '@ewanc26/ui'; 8 9 9 - export interface SiteMetadata { 10 - title: string; 11 - description: string; 12 - keywords: string; 13 - url: string; 14 - image: string; 15 - imageWidth?: number; 16 - imageHeight?: number; 17 - } 10 + export type { SiteMetadata }; 11 + export { createSiteMeta } from '@ewanc26/ui'; 18 12 19 13 /** 20 14 * Default metadata that applies site-wide. 21 - * Can be overridden dynamically for each page or component. 15 + * Can be overridden per-page via createSiteMeta. 22 16 */ 23 17 export const defaultSiteMeta: SiteMetadata = { 24 18 title: PUBLIC_SITE_TITLE, ··· 29 23 imageWidth: 1200, 30 24 imageHeight: 630 31 25 }; 32 - 33 - /** 34 - * Utility function to generate flexible metadata objects. 35 - * Merges defaults with any overrides provided. 36 - */ 37 - export function createSiteMeta(overrides: Partial<SiteMetadata> = {}): SiteMetadata { 38 - return { 39 - ...defaultSiteMeta, 40 - ...overrides 41 - }; 42 - }
+5 -54
src/lib/services/atproto/supporters.ts
··· 1 - /** 2 - * Ko-fi supporters service 3 - * 4 - * Reads uk.ewancroft.kofi.supporter records from the PDS as a timeline. 5 - * Each record is returned with its rkey and decoded timestamp. 6 - * No auth required — records are publicly readable. 7 - * 8 - * The PDS URL is resolved automatically from PUBLIC_ATPROTO_DID via resolveIdentity. 9 - */ 10 - 11 1 import { PUBLIC_ATPROTO_DID } from '$env/static/public'; 12 - import { getPDSAgent } from '@ewanc26/atproto'; 13 - import { decodeTid } from '@ewanc26/tid'; 14 - import type { KofiEventType } from '@ewanc26/supporters'; 15 - 16 - export type { KofiEventType }; 2 + import { fetchEvents } from '@ewanc26/supporters'; 17 3 18 - const COLLECTION = 'uk.ewancroft.kofi.supporter'; 4 + export type { KofiSupportEvent } from '@ewanc26/supporters'; 5 + export type { KofiEventType } from '@ewanc26/supporters'; 19 6 20 - export interface KofiSupportEvent { 21 - rkey: string; 22 - name: string; 23 - type: KofiEventType; 24 - tier?: string; 25 - date: Date; 26 - } 27 - 28 - export async function fetchSupporters(): Promise<KofiSupportEvent[]> { 29 - const agent = await getPDSAgent(PUBLIC_ATPROTO_DID); 30 - const events: KofiSupportEvent[] = []; 31 - let cursor: string | undefined; 32 - 33 - do { 34 - const res = await agent.com.atproto.repo.listRecords({ 35 - repo: PUBLIC_ATPROTO_DID, 36 - collection: COLLECTION, 37 - limit: 100, 38 - cursor 39 - }); 40 - 41 - for (const record of res.data.records) { 42 - const value = record.value as { name: string; type: KofiEventType; tier?: string }; 43 - const rkey = record.uri.split('/').pop() ?? ''; 44 - let date: Date; 45 - try { 46 - date = decodeTid(rkey).date; 47 - } catch { 48 - date = new Date(0); 49 - } 50 - events.push({ rkey, name: value.name, type: value.type, tier: value.tier, date }); 51 - } 52 - 53 - cursor = res.data.cursor; 54 - } while (cursor); 55 - 56 - // Most recent first 57 - return events.sort((a, b) => b.date.getTime() - a.date.getTime()); 7 + export function fetchSupporters() { 8 + return fetchEvents(PUBLIC_ATPROTO_DID); 58 9 }
+2 -11
src/lib/services/atproto/types.ts
··· 2 2 // The app's service wrappers use these directly. 3 3 export type { 4 4 ProfileData, 5 + StatusData, 5 6 SiteInfoData, 6 7 LinkData, 7 8 LinkCard, 8 9 BlueskyPost, 9 10 BlogPost, 11 + BlogPostsData, 10 12 PostAuthor, 11 13 ExternalLink, 12 14 Facet, ··· 31 33 StandardSiteBasicTheme, 32 34 StandardSiteThemeColor 33 35 } from '@ewanc26/atproto'; 34 - 35 - // StatusData is app-local (not in the package) — keep it here. 36 - export interface StatusData { 37 - text: string; 38 - createdAt: string; 39 - } 40 - 41 - // BlogPostsData is also app-local. 42 - export interface BlogPostsData { 43 - posts: import('@ewanc26/atproto').BlogPost[]; 44 - }