my website at ewancroft.uk
6
fork

Configure Feed

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

Refactor and enhance Atom feed generation

Refactored blog Atom feed to use a new blogService for loading posts, improved XML output with additional metadata (icon, logo, enclosure), and standardized content types and error handling. Updated now Atom feed to improve metadata, fix author tag, add published date, and enhance error responses. Both feeds now use consistent content-type headers and improved sorting for updated timestamps.

Ewan Croft b0d31e7c dcd679df

+74 -57
+34 -44
src/routes/blog/atom/+server.ts
··· 1 1 import type { RequestHandler } from "../rss/$types"; 2 2 import { dev } from "$app/environment"; 3 - import { parse } from "$lib/parser"; 4 - import type { MarkdownPost } from "$components/shared"; 5 - import { getProfile } from "$components/profile/profile"; 3 + import { loadAllPosts } from "$services/blogService"; 6 4 7 5 export const GET: RequestHandler = async ({ url, fetch }) => { 8 6 try { 9 - const profileData = await getProfile(fetch); 10 - const did = profileData.did; 11 - const pdsUrl = profileData.pds; 12 - if (!pdsUrl) throw new Error("Could not find PDS URL"); 13 - const postsResponse = await fetch( 14 - `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=com.whtwnd.blog.entry` 15 - ); 16 - if (!postsResponse.ok) 17 - throw new Error(`Posts fetch failed: ${postsResponse.status}`); 18 - const postsData = await postsResponse.json(); 19 - const mdposts: Map<string, MarkdownPost> = new Map(); 20 - for (const data of postsData.records) { 21 - const matches = data.uri.split("/"); 22 - const rkey = matches[matches.length - 1]; 23 - const record = data.value; 24 - if ( 25 - matches && 26 - matches.length === 5 && 27 - record && 28 - (record.visibility === "public" || !record.visibility) 29 - ) { 30 - mdposts.set(rkey, { 31 - title: record.title, 32 - createdAt: new Date(record.createdAt), 33 - mdcontent: record.content, 34 - rkey, 35 - }); 36 - } 37 - } 38 - const posts = await parse(mdposts); 39 - const sortedPosts = Array.from(posts.values()).sort( 40 - (a, b) => b.createdAt.getTime() - a.createdAt.getTime() 41 - ); 7 + const { profile, sortedPosts } = await loadAllPosts(fetch); 8 + 42 9 const baseUrl = dev ? url.origin : "https://ewancroft.uk"; 10 + 43 11 const atomXml = `<?xml version="1.0" encoding="utf-8"?> 44 12 <feed xmlns="http://www.w3.org/2005/Atom"> 45 13 <title>Blog - Ewan's Corner</title> 46 14 <subtitle>A personal blog where I share my thoughts on coding, technology, and life.</subtitle> 47 15 <link href="${baseUrl}/blog" /> 48 - <link href="${baseUrl}/blog/atom" rel="self" /> 49 - <updated>${new Date().toISOString()}</updated> 16 + <link href="${baseUrl}/blog/atom" rel="self" type="application/atom+xml" /> 17 + <updated>${sortedPosts.length > 0 ? sortedPosts[0].createdAt.toISOString() : new Date().toISOString()}</updated> 50 18 <id>${baseUrl}/blog</id> 51 19 <author> 52 - <name>${profileData.displayName || profileData.handle}</name> 20 + <name>${profile.displayName || profile.handle}</name> 53 21 <uri>${baseUrl}/blog</uri> 54 22 </author> 23 + <icon>${baseUrl}/api/og/blog.png</icon> 24 + <logo>${baseUrl}/api/og/blog.png</logo> 55 25 ${sortedPosts 56 26 .map( 57 27 (post) => ` 58 28 <entry> 59 29 <title>${escapeXml(post.title)}</title> 60 - <link href="${baseUrl}/blog/${post.rkey}" /> 30 + <link href="${baseUrl}/blog/${post.rkey}" rel="alternate" type="text/html" /> 61 31 <id>${baseUrl}/blog/${post.rkey}</id> 62 32 <updated>${post.createdAt.toISOString()}</updated> 33 + <published>${post.createdAt.toISOString()}</published> 63 34 <summary type="html"><![CDATA[${post.excerpt || ""}]]></summary> 64 35 <content type="html"><![CDATA[${post.content || ""}]]></content> 65 36 <author> 66 - <name>${profileData.displayName || profileData.handle}</name> 37 + <name>${profile.displayName || profile.handle}</name> 38 + <uri>https://bsky.app/profile/${profile.handle}</uri> 67 39 </author> 40 + <link href="${baseUrl}/api/og/blog/${post.rkey}.png" rel="enclosure" type="image/png" /> 68 41 </entry>` 69 42 ) 70 43 .join("")} 71 44 </feed>`; 45 + 72 46 return new Response(atomXml, { 73 47 headers: { 74 - "Content-Type": "application/atom+xml", 48 + "Content-Type": "application/atom+xml; charset=utf-8", 75 49 "Cache-Control": "max-age=0, s-maxage=3600", 76 50 }, 77 51 }); 78 52 } catch (error) { 79 53 console.error("Error generating Atom feed:", error); 54 + 55 + const baseUrl = dev ? url.origin : "https://ewancroft.uk"; 56 + 80 57 return new Response( 81 - `<?xml version="1.0" encoding="utf-8"?>\n<feed xmlns="http://www.w3.org/2005/Atom">\n <title>Blog - Ewan's Corner</title>\n <subtitle>A personal blog where I share my thoughts on coding, technology, and life.</subtitle>\n <link href="${url.origin}/blog" />\n <link href="${url.origin}/blog/atom" rel="self" />\n <updated>${new Date().toISOString()}</updated>\n <!-- Error occurred while generating feed entries -->\n</feed>`, 58 + `<?xml version="1.0" encoding="utf-8"?> 59 + <feed xmlns="http://www.w3.org/2005/Atom"> 60 + <title>Blog - Ewan's Corner</title> 61 + <subtitle>A personal blog where I share my thoughts on coding, technology, and life.</subtitle> 62 + <link href="${baseUrl}/blog" /> 63 + <link href="${baseUrl}/blog/atom" rel="self" type="application/atom+xml" /> 64 + <updated>${new Date().toISOString()}</updated> 65 + <id>${baseUrl}/blog</id> 66 + <author> 67 + <name>Ewan's Corner</name> 68 + <uri>${baseUrl}/blog</uri> 69 + </author> 70 + <!-- Error occurred while generating feed entries --> 71 + </feed>`, 82 72 { 83 73 headers: { 84 - "Content-Type": "application/atom+xml", 74 + "Content-Type": "application/atom+xml; charset=utf-8", 85 75 "Cache-Control": "no-cache", 86 76 }, 87 77 }
+40 -13
src/routes/now/atom/+server.ts
··· 6 6 import type { StatusUpdate } from "$components/shared"; 7 7 8 8 export const GET: RequestHandler = async ({ url, fetch }: { url: URL, fetch: typeof globalThis.fetch }) => { 9 - let baseUrl: string; 10 - baseUrl = dev ? url.origin : "https://ewancroft.uk"; 9 + const baseUrl = dev ? url.origin : "https://ewancroft.uk"; 10 + 11 11 try { 12 12 const profileData = await getProfile(fetch); 13 13 const did = profileData.did; 14 14 const pdsUrl = profileData.pds; 15 + 15 16 if (!pdsUrl) throw new Error("Could not find PDS URL"); 17 + 16 18 const statusResponse = await fetch( 17 19 `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=uk.ewancroft.now` 18 20 ); 21 + 19 22 if (!statusResponse.ok) 20 23 throw new Error(`Status fetch failed: ${statusResponse.status}`); 24 + 21 25 const statusData = await statusResponse.json(); 22 26 const statusUpdates: StatusUpdate[] = []; 27 + 23 28 for (const data of statusData.records) { 24 29 const matches = data.uri.split("/"); 25 30 const tid = matches[matches.length - 1]; 26 31 const record = data.value; 32 + 27 33 if (matches && matches.length === 5 && record) { 28 34 statusUpdates.push({ 29 35 text: record.text, ··· 32 38 }); 33 39 } 34 40 } 41 + 35 42 const sortedUpdates = statusUpdates.sort( 36 43 (a, b) => b.createdAt.getTime() - a.createdAt.getTime() 37 44 ); 45 + 38 46 const atomXml = `<?xml version="1.0" encoding="utf-8"?> 39 47 <feed xmlns="http://www.w3.org/2005/Atom"> 40 48 <title>Now - ${profileData.displayName || profileData.handle}'s Status Updates</title> 41 49 <subtitle>Short status updates showing what ${profileData.displayName || profileData.handle} is currently doing.</subtitle> 42 50 <link href="${baseUrl}/now" /> 43 - <link href="${baseUrl}/now/atom" rel="self" /> 44 - <updated>${new Date().toISOString()}</updated> 51 + <link href="${baseUrl}/now/atom" rel="self" type="application/atom+xml" /> 52 + <updated>${sortedUpdates.length > 0 ? sortedUpdates[0].createdAt.toISOString() : new Date().toISOString()}</updated> 45 53 <id>${baseUrl}/now</id> 46 54 <author> 47 - <name>${profileData.displayName || profileData.handle}</name> 55 + <n>${profileData.displayName || profileData.handle}</n> 48 56 <uri>${baseUrl}/now</uri> 49 57 </author> 58 + <icon>${baseUrl}/api/og/now.png</icon> 59 + <logo>${baseUrl}/api/og/now.png</logo> 50 60 ${sortedUpdates 51 61 .map( 52 62 (status) => ` 53 63 <entry> 54 64 <title>Status update from ${formatDate(status.createdAt)}</title> 55 - <link href="${baseUrl}/now#${status.tid}" /> 56 - <id>${did}/uk.ewancroft.now/${status.tid}</id> 65 + <link href="${baseUrl}/now#${status.tid}" rel="alternate" type="text/html" /> 66 + <id>${baseUrl}/now/${status.tid}</id> 57 67 <updated>${status.createdAt.toISOString()}</updated> 58 - <summary type="html"><![CDATA[${escapeXml(status.text)}]]></summary> 59 - <content type="html"><![CDATA[${escapeXml(status.text)}]]></content> 68 + <published>${status.createdAt.toISOString()}</published> 69 + <summary type="text">${escapeXml(status.text)}</summary> 70 + <content type="text">${escapeXml(status.text)}</content> 60 71 <author> 61 - <name>${profileData.displayName || profileData.handle}</name> 72 + <n>${profileData.displayName || profileData.handle}</n> 73 + <uri>https://bsky.app/profile/${profileData.handle}</uri> 62 74 </author> 63 75 </entry>` 64 76 ) 65 77 .join("")} 66 78 </feed>`; 79 + 67 80 return new Response(atomXml, { 68 81 headers: { 69 - "Content-Type": "application/atom+xml", 82 + "Content-Type": "application/atom+xml; charset=utf-8", 70 83 "Cache-Control": "max-age=0, s-maxage=600", 71 84 }, 72 85 }); 73 86 } catch (error) { 74 87 console.error("Error generating Atom feed:", error); 88 + 75 89 return new Response( 76 - `<?xml version="1.0" encoding="utf-8"?>\n<feed xmlns="http://www.w3.org/2005/Atom">\n <title>Now - Status Updates</title>\n <subtitle>Short status updates showing what I'm currently doing.</subtitle>\n <link href="${url.origin}/now" />\n <link href="${url.origin}/now/atom" rel="self" />\n <updated>${new Date().toISOString()}</updated>\n <!-- Error occurred while generating feed entries -->\n</feed>`, 90 + `<?xml version="1.0" encoding="utf-8"?> 91 + <feed xmlns="http://www.w3.org/2005/Atom"> 92 + <title>Now - Status Updates</title> 93 + <subtitle>Short status updates showing what I'm currently doing.</subtitle> 94 + <link href="${baseUrl}/now" /> 95 + <link href="${baseUrl}/now/atom" rel="self" type="application/atom+xml" /> 96 + <updated>${new Date().toISOString()}</updated> 97 + <id>${baseUrl}/now</id> 98 + <author> 99 + <n>Ewan's Corner</n> 100 + <uri>${baseUrl}/now</uri> 101 + </author> 102 + <!-- Error occurred while generating feed entries --> 103 + </feed>`, 77 104 { 78 105 headers: { 79 - "Content-Type": "application/atom+xml", 106 + "Content-Type": "application/atom+xml; charset=utf-8", 80 107 "Cache-Control": "no-cache", 81 108 }, 82 109 }