my website at ewancroft.uk
6
fork

Configure Feed

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

Add Atom feed support for blog

Introduces an Atom feed endpoint at `/blog/atom` with a new AtomIcon and navigation link. The Atom feed is generated from blog posts and includes author and post metadata, providing an alternative to the existing RSS feed.

Ewan Croft dcd679df 77844a2a

+133 -1
+1
src/lib/components/icons/index.ts
··· 3 3 export { default as MastodonIcon } from './social/MastodonIcon.svelte'; 4 4 export { default as RedditIcon } from './social/RedditIcon.svelte'; 5 5 export { default as RssIcon } from './social/RssIcon.svelte'; 6 + export { default as AtomIcon } from "./social/AtomIcon.svelte"; 6 7 export { default as ShareIcons } from './social/ShareIcons.svelte'; 7 8 export { default as BookOpenIcon } from './utility/BookOpenIcon.svelte'; 8 9 export { default as CopyLinkIcon } from './utility/CopyLinkIcon.svelte';
+22
src/lib/components/icons/social/AtomIcon.svelte
··· 1 + <script> 2 + export let size = "24"; 3 + export let stroke = "currentColor"; 4 + export let fill = "none"; 5 + </script> 6 + 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width={size} 10 + height={size} 11 + viewBox="0 0 24 24" 12 + {fill} 13 + {stroke} 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path opacity="0.1" fill-rule="evenodd" clip-rule="evenodd" d="M15.9998 16C17.2861 14.7137 18.3497 13.3465 19.1568 12.0001C18.3497 10.6536 17.2861 9.28634 15.9998 8C14.7134 6.71368 13.3462 5.65007 11.9998 4.84302C10.6533 5.65006 9.28611 6.71367 7.99981 7.99998C6.71346 9.28632 5.64983 10.6536 4.84277 12.0001C5.64982 13.3465 6.71343 14.7137 7.99974 16C9.28606 17.2864 10.6533 18.35 11.9998 19.157C13.3463 18.35 14.7135 17.2863 15.9998 16ZM11.9998 9.74994C10.7571 9.74994 9.74976 10.7573 9.74976 11.9999C9.74976 13.2426 10.7571 14.2499 11.9998 14.2499C13.2424 14.2499 14.2498 13.2426 14.2498 11.9999C14.2498 10.7573 13.2424 9.74994 11.9998 9.74994Z" fill={stroke}/> 19 + <path d="M20 20.0001C17.7909 22.2092 12.4183 20.4183 8 16.0001C3.58171 11.5818 1.79084 6.20916 3.99999 4.00001C6.20913 1.79087 11.5817 3.58173 16 8.00003C20.4183 12.4183 22.2092 17.7909 20 20.0001Z" stroke={stroke} stroke-width="2"/> 20 + <path d="M3.99994 20C1.79079 17.7909 3.58166 12.4183 7.99995 8C12.4182 3.58171 17.7908 1.79084 20 3.99999C22.2091 6.20913 20.4183 11.5817 16 16C11.5817 20.4183 6.20908 22.2092 3.99994 20Z" stroke={stroke} stroke-width="2"/> 21 + <path d="M14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12Z" stroke={stroke} stroke-width="2"/> 22 + </svg>
+10 -1
src/lib/components/layout/header/Navigation.svelte
··· 2 2 import { getStores } from "$app/stores"; 3 3 const { page } = getStores(); 4 4 5 - import { HomeIcon, RssIcon, BookOpenIcon } from "$components/icons"; 5 + import { HomeIcon, RssIcon, BookOpenIcon, AtomIcon } from "$components/icons"; 6 6 7 7 export const isHomePage: boolean = false; 8 8 export let isBlogIndex: boolean = false; ··· 31 31 download="{cleanOrigin}_Blog.rss" 32 32 > 33 33 <RssIcon /> 34 + </a> 35 + <!-- Atom Feed Link --> 36 + <a 37 + href="{$page.url.origin}/blog/atom" 38 + class="font-medium text-[large] hover:text-[var(--link-hover-color)]" 39 + aria-label="Atom Feed" 40 + download="{cleanOrigin}_Blog.atom" 41 + > 42 + <AtomIcon /> 34 43 </a> 35 44 {/if} 36 45 {#if $page.url.pathname.startsWith("/blog/") && $page.url.pathname !== "/blog/"}
+100
src/routes/blog/atom/+server.ts
··· 1 + import type { RequestHandler } from "../rss/$types"; 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"; 6 + 7 + export const GET: RequestHandler = async ({ url, fetch }) => { 8 + 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 + ); 42 + const baseUrl = dev ? url.origin : "https://ewancroft.uk"; 43 + const atomXml = `<?xml version="1.0" encoding="utf-8"?> 44 + <feed xmlns="http://www.w3.org/2005/Atom"> 45 + <title>Blog - Ewan's Corner</title> 46 + <subtitle>A personal blog where I share my thoughts on coding, technology, and life.</subtitle> 47 + <link href="${baseUrl}/blog" /> 48 + <link href="${baseUrl}/blog/atom" rel="self" /> 49 + <updated>${new Date().toISOString()}</updated> 50 + <id>${baseUrl}/blog</id> 51 + <author> 52 + <name>${profileData.displayName || profileData.handle}</name> 53 + <uri>${baseUrl}/blog</uri> 54 + </author> 55 + ${sortedPosts 56 + .map( 57 + (post) => ` 58 + <entry> 59 + <title>${escapeXml(post.title)}</title> 60 + <link href="${baseUrl}/blog/${post.rkey}" /> 61 + <id>${baseUrl}/blog/${post.rkey}</id> 62 + <updated>${post.createdAt.toISOString()}</updated> 63 + <summary type="html"><![CDATA[${post.excerpt || ""}]]></summary> 64 + <content type="html"><![CDATA[${post.content || ""}]]></content> 65 + <author> 66 + <name>${profileData.displayName || profileData.handle}</name> 67 + </author> 68 + </entry>` 69 + ) 70 + .join("")} 71 + </feed>`; 72 + return new Response(atomXml, { 73 + headers: { 74 + "Content-Type": "application/atom+xml", 75 + "Cache-Control": "max-age=0, s-maxage=3600", 76 + }, 77 + }); 78 + } catch (error) { 79 + console.error("Error generating Atom feed:", error); 80 + 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>`, 82 + { 83 + headers: { 84 + "Content-Type": "application/atom+xml", 85 + "Cache-Control": "no-cache", 86 + }, 87 + } 88 + ); 89 + } 90 + }; 91 + 92 + function escapeXml(unsafe: string): string { 93 + if (!unsafe) return ""; 94 + return unsafe 95 + .replace(/&/g, "&amp;") 96 + .replace(/</g, "&lt;") 97 + .replace(/>/g, "&gt;") 98 + .replace(/"/g, "&quot;") 99 + .replace(/'/g, "&apos;"); 100 + }