my website at ewancroft.uk
6
fork

Configure Feed

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

feat: add rkey-based routing for publications

Support accessing Standard.site publications via rkey (e.g. /3m3x4bgbsh22k)
in addition to slugs (e.g. /blog). Both work interchangeably across all
routes including RSS feeds and document paths.

+174 -91
+20
src/lib/config/slugs.ts
··· 90 90 slug: normalizeSlug(m.slug) 91 91 })); 92 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); 103 + } 104 + 105 + /** 106 + * Get all publication rkeys from slug mappings 107 + * 108 + * @returns Array of publication rkeys 109 + */ 110 + export function getAllPublicationRkeys(): string[] { 111 + return slugMappings.map((m) => m.publicationRkey); 112 + }
+12 -3
src/params/slug.ts
··· 2 2 import { getAllSlugs } from '$lib/config/slugs'; 3 3 4 4 /** 5 - * Param matcher for valid slugs 6 - * Only allows slugs that are defined in the slug-mappings configuration 5 + * Param matcher for valid slugs or publication rkeys 6 + * Allows: 7 + * - Slugs defined in the slug-mappings configuration 8 + * - Publication rkeys (TID format: 12-16 alphanumeric characters) 7 9 */ 8 10 export const match: ParamMatcher = (param) => { 11 + // Check if it's a configured slug 9 12 const validSlugs = getAllSlugs(); 10 - return validSlugs.includes(param); 13 + if (validSlugs.includes(param)) { 14 + return true; 15 + } 16 + 17 + // Check if it's a valid TID format (AT Protocol record key) 18 + const tidPattern = /^[a-zA-Z0-9]{12,16}$/; 19 + return tidPattern.test(param); 11 20 };
+43 -24
src/routes/[slug=slug]/+server.ts
··· 1 1 import type { RequestHandler } from '@sveltejs/kit'; 2 2 import { fetchPublications } from '$lib/services/atproto'; 3 - import { getPublicationFromSlug } from '$lib/config/slugs'; 3 + import { getPublicationFromSlug, isTidFormat, getSlugFromPublicationRkey } from '$lib/config/slugs'; 4 4 5 5 /** 6 - * Dynamic slug root redirect handler 6 + * Dynamic slug/rkey root redirect handler 7 7 * 8 - * Redirects /{slug} to the appropriate Standard.site publication URL 9 - * Uses the slug mapping config to find the publication rkey 8 + * Handles both: 9 + * - /{slug} redirects to the appropriate Standard.site publication URL 10 + * - /{publication-rkey} redirects to the publication (for site.standard.publication rkeys) 11 + * 12 + * Uses the slug mapping config to find the publication rkey for slugs. 10 13 * Individual documents are handled by the [rkey] route. 11 14 */ 12 15 export const GET: RequestHandler = async ({ params, url }) => { 13 - const slug = params.slug; 16 + const slugOrRkey = params.slug; 14 17 15 - // If there's a path after /{slug}, let it fall through to other routes 16 - const slugPath = url.pathname.replace(new RegExp(`^/${slug}/?`), ''); 18 + // If there's a path after /{slugOrRkey}, let it fall through to other routes 19 + const slugPath = url.pathname.replace(new RegExp(`^/${slugOrRkey}/?`), ''); 17 20 18 21 if (slugPath && !['rss', 'atom'].includes(slugPath)) { 19 22 // This will be caught by the [rkey] route ··· 25 28 }); 26 29 } 27 30 28 - // For /{slug} root, redirect to the publication 31 + // For /{slugOrRkey} root, redirect to the publication 29 32 if (!slugPath || slugPath === '') { 30 - // Validate slug and get the publication info 31 - if (!slug) { 32 - return new Response('Invalid slug', { 33 + // Validate input 34 + if (!slugOrRkey) { 35 + return new Response('Invalid slug or rkey', { 33 36 status: 400, 34 37 headers: { 35 38 'Content-Type': 'text/plain; charset=utf-8' ··· 37 40 }); 38 41 } 39 42 40 - const publicationInfo = getPublicationFromSlug(slug); 43 + let publicationRkey: string; 44 + let isDirectRkey = false; 45 + 46 + // Check if input is a TID (rkey) or a slug 47 + if (isTidFormat(slugOrRkey)) { 48 + // Input is an rkey - use it directly 49 + publicationRkey = slugOrRkey; 50 + isDirectRkey = true; 51 + } else { 52 + // Input is a slug - look up the rkey 53 + const publicationInfo = getPublicationFromSlug(slugOrRkey); 41 54 42 - if (!publicationInfo) { 43 - return new Response( 44 - `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/data/slug-mappings.ts`, 45 - { 46 - status: 404, 47 - headers: { 48 - 'Content-Type': 'text/plain; charset=utf-8' 55 + if (!publicationInfo) { 56 + return new Response( 57 + `Slug not configured: ${slugOrRkey}\n\nPlease add this slug to src/lib/data/slug-mappings.ts`, 58 + { 59 + status: 404, 60 + headers: { 61 + 'Content-Type': 'text/plain; charset=utf-8' 62 + } 49 63 } 50 - } 51 - ); 52 - } 64 + ); 65 + } 53 66 54 - const { rkey: publicationRkey } = publicationInfo; 67 + publicationRkey = publicationInfo.rkey; 68 + } 55 69 let redirectUrl: string | null = null; 56 70 57 71 try { ··· 81 95 } 82 96 83 97 // No publication found 98 + const identifier = isDirectRkey ? `rkey: ${slugOrRkey}` : `slug: ${slugOrRkey}`; 84 99 return new Response( 85 - `Publication not found for slug: ${slug}\n\nPlease check your configuration in src/lib/data/slug-mappings.ts`, 100 + `Publication not found for ${identifier}\n\n${ 101 + isDirectRkey 102 + ? 'This publication rkey does not exist or is not accessible.' 103 + : 'Please check your configuration in src/lib/data/slug-mappings.ts' 104 + }`, 86 105 { 87 106 status: 404, 88 107 headers: {
+42 -27
src/routes/[slug=slug]/[rkey]/+server.ts
··· 2 2 import { PUBLIC_ATPROTO_DID, PUBLIC_BLOG_FALLBACK_URL } from '$env/static/public'; 3 3 import { withFallback } from '$lib/services/atproto'; 4 4 import { fetchPublications } from '$lib/services/atproto'; 5 - import { getPublicationFromSlug } from '$lib/config/slugs'; 5 + import { getPublicationFromSlug, isTidFormat } from '$lib/config/slugs'; 6 6 import type { PublicationPlatform } from '$lib/data/slug-mappings'; 7 7 8 8 /** 9 - * Smart document redirect handler for slugged publications 9 + * Smart document redirect handler for slugged or rkey-based publications 10 + * 11 + * Handles both: 12 + * - /{slug}/{document-rkey} - publication identified by slug 13 + * - /{publication-rkey}/{document-rkey} - publication identified by rkey 10 14 * 11 15 * Automatically detects Standard.site documents and redirects to the canonical URL. 12 16 * Uses the publication's URL + document path to construct the final URL. ··· 81 85 } 82 86 83 87 export const GET: RequestHandler = async ({ params, url }) => { 84 - const slug = params.slug; 85 - const rkey = params.rkey; 88 + const slugOrRkey = params.slug; 89 + const documentRkey = params.rkey; 86 90 87 - // Validate slug 88 - if (!slug) { 89 - return new Response('Invalid slug', { 91 + // Validate input 92 + if (!slugOrRkey) { 93 + return new Response('Invalid slug or publication rkey', { 90 94 status: 400, 91 95 headers: { 92 96 'Content-Type': 'text/plain; charset=utf-8' ··· 94 98 }); 95 99 } 96 100 97 - // Get the publication info from the slug 98 - const publicationInfo = getPublicationFromSlug(slug); 101 + let publicationRkey: string; 102 + let isDirectRkey = false; 103 + 104 + // Check if input is a TID (rkey) or a slug 105 + if (isTidFormat(slugOrRkey)) { 106 + // Input is a publication rkey - use it directly 107 + publicationRkey = slugOrRkey; 108 + isDirectRkey = true; 109 + } else { 110 + // Input is a slug - look up the publication rkey 111 + const publicationInfo = getPublicationFromSlug(slugOrRkey); 99 112 100 - if (!publicationInfo) { 101 - return new Response( 102 - `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/data/slug-mappings.ts`, 103 - { 104 - status: 404, 105 - headers: { 106 - 'Content-Type': 'text/plain; charset=utf-8' 113 + if (!publicationInfo) { 114 + return new Response( 115 + `Slug not configured: ${slugOrRkey}\n\nPlease add this slug to src/lib/data/slug-mappings.ts`, 116 + { 117 + status: 404, 118 + headers: { 119 + 'Content-Type': 'text/plain; charset=utf-8' 120 + } 107 121 } 108 - } 109 - ); 110 - } 122 + ); 123 + } 111 124 112 - const { rkey: publicationRkey } = publicationInfo; 125 + publicationRkey = publicationInfo.rkey; 126 + } 113 127 114 - // Validate TID format (AT Protocol record key) 128 + // Validate document rkey TID format (AT Protocol record key) 115 129 const tidPattern = /^[a-zA-Z0-9]{12,16}$/; 116 130 117 - if (!rkey || !tidPattern.test(rkey)) { 118 - return new Response('Invalid TID format. Expected 12-16 alphanumeric characters.', { 131 + if (!documentRkey || !tidPattern.test(documentRkey)) { 132 + return new Response('Invalid document TID format. Expected 12-16 alphanumeric characters.', { 119 133 status: 400, 120 134 headers: { 121 135 'Content-Type': 'text/plain; charset=utf-8' ··· 124 138 } 125 139 126 140 // Detect document and get canonical URL 127 - const detection = await detectDocumentUrl(rkey, publicationRkey); 141 + const detection = await detectDocumentUrl(documentRkey, publicationRkey); 128 142 129 143 let targetUrl: string | null = null; 130 144 let statusCode = 301; ··· 134 148 targetUrl = detection.url; 135 149 } else if (PUBLIC_BLOG_FALLBACK_URL) { 136 150 // Use fallback URL from environment variable 137 - targetUrl = `${PUBLIC_BLOG_FALLBACK_URL}/${rkey}`; 151 + targetUrl = `${PUBLIC_BLOG_FALLBACK_URL}/${documentRkey}`; 138 152 } else { 139 153 // No fallback configured, return 404 154 + const identifier = isDirectRkey ? `publication rkey "${slugOrRkey}"` : `slug "${slugOrRkey}"`; 140 155 return new Response( 141 - `Document not found: ${rkey} 156 + `Document not found: ${documentRkey} 142 157 143 - This document could not be found in the Standard.site publication for slug "${slug}". 158 + This document could not be found in the Standard.site publication for ${identifier}. 144 159 145 160 Note: Only checking Standard.site publication with rkey: ${publicationRkey} 146 161
+23 -17
src/routes/[slug=slug]/atom/+server.ts
··· 1 1 import type { RequestHandler } from '@sveltejs/kit'; 2 - import { getPublicationRkeyFromSlug } from '$lib/config/slugs'; 2 + import { getPublicationRkeyFromSlug, isTidFormat } from '$lib/config/slugs'; 3 3 4 4 /** 5 5 * Deprecated Atom feed 6 + * 7 + * Accessible via: 8 + * - /{slug}/atom - publication identified by slug 9 + * - /{publication-rkey}/atom - publication identified by rkey 6 10 * 7 11 * Atom feeds are no longer supported. Use RSS instead. 8 12 * ··· 13 17 * - Maintaining both RSS and Atom adds unnecessary complexity 14 18 */ 15 19 export const GET: RequestHandler = ({ params }) => { 16 - const slug = params.slug; 20 + const slugOrRkey = params.slug; 17 21 18 - // Validate slug 19 - if (!slug) { 20 - return new Response('Invalid slug', { 22 + // Validate input 23 + if (!slugOrRkey) { 24 + return new Response('Invalid slug or publication rkey', { 21 25 status: 400, 22 26 headers: { 23 27 'Content-Type': 'text/plain; charset=utf-8' ··· 25 29 }); 26 30 } 27 31 28 - // Validate slug exists in config 29 - const publicationRkey = getPublicationRkeyFromSlug(slug); 32 + // Validate that either the slug exists in config or it's a valid rkey 33 + if (!isTidFormat(slugOrRkey)) { 34 + const publicationRkey = getPublicationRkeyFromSlug(slugOrRkey); 30 35 31 - if (!publicationRkey) { 32 - return new Response( 33 - `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/config/slugs.ts`, 34 - { 35 - status: 404, 36 - headers: { 37 - 'Content-Type': 'text/plain; charset=utf-8' 36 + if (!publicationRkey) { 37 + return new Response( 38 + `Slug not configured: ${slugOrRkey}\n\nPlease add this slug to src/lib/config/slugs.ts`, 39 + { 40 + status: 404, 41 + headers: { 42 + 'Content-Type': 'text/plain; charset=utf-8' 43 + } 38 44 } 39 - } 40 - ); 45 + ); 46 + } 41 47 } 42 48 43 49 return new Response( ··· 45 51 46 52 This Atom feed is no longer available. Please use the RSS feed instead: 47 53 48 - RSS Feed: /${slug}/rss 54 + RSS Feed: /${slugOrRkey}/rss 49 55 50 56 For Leaflet posts with full content, the RSS feed will automatically redirect you to 51 57 Leaflet's native RSS feed which includes complete post content.
+34 -20
src/routes/[slug=slug]/rss/+server.ts
··· 6 6 PUBLIC_SITE_URL 7 7 } from '$env/static/public'; 8 8 import { fetchBlogPosts } from '$lib/services/atproto'; 9 - import { getPublicationRkeyFromSlug } from '$lib/config/slugs'; 9 + import { getPublicationRkeyFromSlug, isTidFormat } from '$lib/config/slugs'; 10 10 import { generateRSSFeed, createRSSResponse, type RSSItem } from '$lib/utils/rss'; 11 11 12 12 /** 13 - * RSS 2.0 feed for Standard.site publications (accessed via /{slug}/rss) 13 + * RSS 2.0 feed for Standard.site publications 14 + * 15 + * Accessible via: 16 + * - /{slug}/rss - publication identified by slug 17 + * - /{publication-rkey}/rss - publication identified by rkey 14 18 * 15 19 * Generates an RSS feed for all documents in the specified publication. 16 20 */ 17 21 export const GET: RequestHandler = async ({ params }) => { 18 - const slug = params.slug; 22 + const slugOrRkey = params.slug; 19 23 20 - // Validate slug 21 - if (!slug) { 22 - return new Response('Invalid slug', { 24 + // Validate input 25 + if (!slugOrRkey) { 26 + return new Response('Invalid slug or publication rkey', { 23 27 status: 400, 24 28 headers: { 25 29 'Content-Type': 'text/plain; charset=utf-8' ··· 27 31 }); 28 32 } 29 33 30 - // Get the publication rkey from the slug 31 - const publicationRkey = getPublicationRkeyFromSlug(slug); 34 + let publicationRkey: string; 32 35 33 - if (!publicationRkey) { 34 - return new Response( 35 - `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/config/slugs.ts`, 36 - { 37 - status: 404, 38 - headers: { 39 - 'Content-Type': 'text/plain; charset=utf-8' 36 + // Check if input is a TID (rkey) or a slug 37 + if (isTidFormat(slugOrRkey)) { 38 + // Input is a publication rkey - use it directly 39 + publicationRkey = slugOrRkey; 40 + } else { 41 + // Input is a slug - look up the publication rkey 42 + const rkey = getPublicationRkeyFromSlug(slugOrRkey); 43 + 44 + if (!rkey) { 45 + return new Response( 46 + `Slug not configured: ${slugOrRkey}\n\nPlease add this slug to src/lib/config/slugs.ts`, 47 + { 48 + status: 404, 49 + headers: { 50 + 'Content-Type': 'text/plain; charset=utf-8' 51 + } 40 52 } 41 - } 42 - ); 53 + ); 54 + } 55 + 56 + publicationRkey = rkey; 43 57 } 44 58 45 59 try { ··· 65 79 // Generate RSS feed 66 80 const feed = generateRSSFeed( 67 81 { 68 - title: `${PUBLIC_SITE_TITLE} - ${slug}`, 82 + title: `${PUBLIC_SITE_TITLE} - ${slugOrRkey}`, 69 83 link: PUBLIC_SITE_URL, 70 84 description: PUBLIC_SITE_DESCRIPTION, 71 85 language: 'en', 72 - selfLink: `${PUBLIC_SITE_URL}/${slug}/rss`, 86 + selfLink: `${PUBLIC_SITE_URL}/${slugOrRkey}/rss`, 73 87 generator: 'SvelteKit with AT Protocol' 74 88 }, 75 89 items ··· 79 93 } 80 94 81 95 // No posts at all 82 - return new Response(`No posts found for publication: ${slug}`, { 96 + return new Response(`No posts found for publication: ${slugOrRkey}`, { 83 97 status: 404, 84 98 headers: { 85 99 'Content-Type': 'text/plain; charset=utf-8'