my website at ewancroft.uk

feat(blog): implement smart post redirect detection for WhiteWind and Leaflet

authored by ewancroft.uk and committed by ewancroft.uk 8f9b96f1 83e8f89e

Changed files
+181 -12
src
routes
blog
[rkey]
+181 -12
src/routes/blog/[rkey]/+server.ts
··· 1 1 import type { RequestHandler } from '@sveltejs/kit'; 2 + import { 3 + PUBLIC_ATPROTO_DID, 4 + PUBLIC_LEAFLET_BASE_PATH, 5 + PUBLIC_LEAFLET_BLOG_PUBLICATION, 6 + PUBLIC_BLOG_FALLBACK_URL 7 + } from '$env/static/public'; 8 + import { withFallback } from '$lib/services/atproto'; 9 + import { fetchLeafletPublications } from '$lib/services/atproto'; 2 10 3 - export const GET: RequestHandler = ({ params, url }) => { 4 - const tid = params.rkey; 5 - const tidPattern = /^[a-zA-Z0-9]{12,16}$/; 11 + /** 12 + * Smart blog post redirect handler 13 + * 14 + * Automatically detects whether the post is from WhiteWind or Leaflet 15 + * and redirects to the appropriate URL. 16 + * 17 + * WhiteWind: https://whtwnd.com/{DID}/{rkey} 18 + * Leaflet: {LEAFLET_BASE_PATH}/{rkey} or https://leaflet.pub/{DID}/{rkey} 19 + * 20 + * If detection fails, falls back to PUBLIC_BLOG_FALLBACK_URL or returns 404. 21 + * 22 + * Supports multiple Leaflet publications: 23 + * - If PUBLIC_LEAFLET_BLOG_PUBLICATION is set, only checks that specific publication 24 + * - Otherwise, checks all publications for the document 25 + */ 26 + 27 + async function detectPostPlatform( 28 + rkey: string 29 + ): Promise<{ platform: 'whitewind' | 'leaflet' | 'unknown'; url?: string }> { 30 + try { 31 + // Check WhiteWind first using atproto services 32 + const whiteWindRecord = await withFallback( 33 + PUBLIC_ATPROTO_DID, 34 + async (agent) => { 35 + try { 36 + const response = await agent.com.atproto.repo.getRecord({ 37 + repo: PUBLIC_ATPROTO_DID, 38 + collection: 'com.whtwnd.blog.entry', 39 + rkey 40 + }); 41 + return response.data; 42 + } catch (err) { 43 + // Record not found 44 + return null; 45 + } 46 + }, 47 + true // Use PDS first for custom collections 48 + ); 49 + 50 + if (whiteWindRecord) { 51 + const value = whiteWindRecord.value as any; 52 + // Skip drafts and non-public posts 53 + if (!value?.isDraft && (!value?.visibility || value.visibility === 'public')) { 54 + return { 55 + platform: 'whitewind', 56 + url: `https://whtwnd.com/${PUBLIC_ATPROTO_DID}/${rkey}` 57 + }; 58 + } 59 + } 60 + 61 + // Check Leaflet using atproto services 62 + const leafletRecord = await withFallback( 63 + PUBLIC_ATPROTO_DID, 64 + async (agent) => { 65 + try { 66 + const response = await agent.com.atproto.repo.getRecord({ 67 + repo: PUBLIC_ATPROTO_DID, 68 + collection: 'pub.leaflet.document', 69 + rkey 70 + }); 71 + return response.data; 72 + } catch (err) { 73 + // Record not found 74 + return null; 75 + } 76 + }, 77 + true // Use PDS first for custom collections 78 + ); 79 + 80 + if (leafletRecord) { 81 + const value = leafletRecord.value as any; 82 + const publicationUri = value?.publication; 83 + 84 + // Fetch publications to get base path 85 + const { publications } = await fetchLeafletPublications(); 86 + const publication = publicationUri 87 + ? publications.find((p) => p.uri === publicationUri) 88 + : null; 89 + 90 + // If a specific blog publication is configured, check if this document belongs to it 91 + if (PUBLIC_LEAFLET_BLOG_PUBLICATION && publication) { 92 + if (publication.rkey !== PUBLIC_LEAFLET_BLOG_PUBLICATION) { 93 + // Document belongs to a different publication, not the blog 94 + return { platform: 'unknown' }; 95 + } 96 + } 97 + 98 + // Determine URL based on priority: env var → publication base_path → Leaflet /lish format 99 + let url: string; 100 + const publicationRkey = publication?.rkey || ''; 6 101 7 - if (!tid || !tidPattern.test(tid)) { 8 - return new Response('Invalid TID', { status: 400 }); 9 - } 102 + if (PUBLIC_LEAFLET_BASE_PATH) { 103 + url = `${PUBLIC_LEAFLET_BASE_PATH}/${rkey}`; 104 + } else if (publication?.basePath) { 105 + url = `${publication.basePath}/${rkey}`; 106 + } else if (publicationRkey) { 107 + url = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}/${rkey}`; 108 + } else { 109 + url = `https://leaflet.pub/${PUBLIC_ATPROTO_DID}/${rkey}`; 110 + } 10 111 11 - const queryString = url.search; 12 - const targetUrl = `https://blog.ewancroft.uk/${tid}${queryString}`; 112 + return { 113 + platform: 'leaflet', 114 + url 115 + }; 116 + } 13 117 14 - return new Response(null, { 15 - status: 301, 16 - headers: { Location: targetUrl } 17 - }); 118 + return { platform: 'unknown' }; 119 + } catch (error) { 120 + console.error('Error detecting post platform:', error); 121 + return { platform: 'unknown' }; 122 + } 123 + } 124 + 125 + export const GET: RequestHandler = async ({ params, url }) => { 126 + const rkey = params.rkey; 127 + 128 + // Validate TID format (AT Protocol record key) 129 + const tidPattern = /^[a-zA-Z0-9]{12,16}$/; 130 + 131 + if (!rkey || !tidPattern.test(rkey)) { 132 + return new Response('Invalid TID format. Expected 12-16 alphanumeric characters.', { 133 + status: 400, 134 + headers: { 135 + 'Content-Type': 'text/plain; charset=utf-8' 136 + } 137 + }); 138 + } 139 + 140 + // Detect platform and get appropriate URL 141 + const detection = await detectPostPlatform(rkey); 142 + 143 + let targetUrl: string | null = null; 144 + let statusCode = 301; 145 + 146 + if (detection.platform !== 'unknown' && detection.url) { 147 + // Found the post on WhiteWind or Leaflet 148 + targetUrl = detection.url; 149 + } else if (PUBLIC_BLOG_FALLBACK_URL) { 150 + // Use fallback URL from environment variable 151 + targetUrl = `${PUBLIC_BLOG_FALLBACK_URL}/${rkey}`; 152 + } else { 153 + // No fallback configured, return 404 154 + const blogPublicationNote = PUBLIC_LEAFLET_BLOG_PUBLICATION 155 + ? `\n\nNote: Only checking Leaflet publication: ${PUBLIC_LEAFLET_BLOG_PUBLICATION}` 156 + : ''; 157 + 158 + return new Response( 159 + `Blog post not found: ${rkey} 160 + 161 + This post could not be found on WhiteWind or Leaflet platforms.${blogPublicationNote} 162 + 163 + Please check: 164 + - WhiteWind: https://whtwnd.com 165 + - Leaflet: https://leaflet.pub`, 166 + { 167 + status: 404, 168 + headers: { 169 + 'Content-Type': 'text/plain; charset=utf-8' 170 + } 171 + } 172 + ); 173 + } 174 + 175 + // Preserve query string 176 + const queryString = url.search; 177 + targetUrl += queryString; 178 + 179 + // Use 301 for permanent redirect (better for SEO) 180 + return new Response(null, { 181 + status: statusCode, 182 + headers: { 183 + Location: targetUrl, 184 + 'Cache-Control': 'public, max-age=31536000, immutable' 185 + } 186 + }); 18 187 };