The code and data behind xeiaso.net
5
fork

Configure Feed

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

lume: add xecast RSS feed

Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso b40fed8e 2e03f388

+312
+18
lume/_config.ts
··· 12 12 13 13 import annotateYear from "./plugins/annotate_year.ts"; 14 14 import feed from "./plugins/feed.ts"; 15 + import podcast_feed from "./plugins/podcast_feed.ts"; 15 16 16 17 //import pagefind from "lume/plugins/pagefind.ts"; 17 18 //import _ from "npm:@pagefind/linux-x64"; ··· 96 97 },*/ 97 98 }, 98 99 })); 100 + 101 + site.use(podcast_feed({ 102 + output: ["/xecast.rss"], 103 + query: "is_xecast=true", 104 + info: { 105 + title: "Xecast", 106 + description: "Thoughts and musings from Xe Iaso, now in podcast form", 107 + published: new Date(), 108 + lang: "en", 109 + }, 110 + items: { 111 + title: "=title", 112 + description: "=desc", 113 + podcast: "=podcast", 114 + }, 115 + })) 116 + 99 117 site.use(mdx({ 100 118 components: { 101 119 "BlockQuote": BlockQuote,
+286
lume/plugins/podcast_feed.ts
··· 1 + import { getExtension } from "lume/core/utils/path.ts"; 2 + import { merge } from "lume/core/utils/object.ts"; 3 + import { getCurrentVersion } from "lume/core/utils/lume_version.ts"; 4 + import { getDataValue } from "lume/core/utils/data_values.ts"; 5 + import { $XML, stringify } from "lume/deps/xml.ts"; 6 + import { Page } from "lume/core/file.ts"; 7 + 8 + import type Site from "lume/core/site.ts"; 9 + import type { Data } from "lume/core/file.ts"; 10 + import { info } from "lume/deps/log.ts"; 11 + 12 + export interface Options { 13 + /** The output filenames */ 14 + output?: string | string[]; 15 + 16 + /** The query to search the pages */ 17 + query?: string; 18 + 19 + /** The sort order */ 20 + sort?: string; 21 + 22 + /** The maximum number of items */ 23 + limit?: number; 24 + 25 + /** The feed info */ 26 + info?: FeedInfoOptions; 27 + 28 + /** The feed items configuration */ 29 + items?: FeedItemOptions; 30 + } 31 + 32 + export interface FeedInfoOptions { 33 + /** The feed title */ 34 + title?: string; 35 + 36 + /** The feed subtitle */ 37 + subtitle?: string; 38 + 39 + /** 40 + * The feed published date 41 + * @default `new Date()` 42 + */ 43 + published?: Date; 44 + 45 + /** The feed description */ 46 + description?: string; 47 + 48 + /** The feed language */ 49 + lang?: string; 50 + 51 + /** The feed generator. Set `true` to generate automatically */ 52 + generator?: string | boolean; 53 + 54 + /** The feed author */ 55 + author?: string; 56 + } 57 + 58 + export interface FeedItemOptions { 59 + /** The item title */ 60 + title?: string | ((data: Data) => string | undefined); 61 + 62 + /** The item description */ 63 + description?: string | ((data: Data) => string | undefined); 64 + 65 + /** The item published date */ 66 + published?: string | ((data: Data) => Date | undefined); 67 + 68 + /** The item updated date */ 69 + updated?: string | ((data: Data) => Date | undefined); 70 + 71 + /** The item content */ 72 + content?: string | ((data: Data) => string | undefined); 73 + 74 + /** The item language */ 75 + lang?: string | ((data: Data) => string | undefined); 76 + 77 + podcast?: string | ((data: Data) => FeedPodcastItem | undefined); 78 + } 79 + 80 + export const defaults: Options = { 81 + /** The output filenames */ 82 + output: "/feed.rss", 83 + 84 + /** The query to search the pages */ 85 + query: "", 86 + 87 + /** The sort order */ 88 + sort: "date=desc", 89 + 90 + /** The maximum number of items */ 91 + limit: 10, 92 + 93 + /** The feed info */ 94 + info: { 95 + title: "My RSS Feed", 96 + published: new Date(), 97 + description: "", 98 + lang: "en", 99 + generator: true, 100 + author: "Xe Iaso", 101 + }, 102 + items: { 103 + title: "=title", 104 + description: "=description", 105 + published: "=date", 106 + content: "=children", 107 + lang: "=lang", 108 + }, 109 + }; 110 + 111 + export interface FeedData { 112 + title: string; 113 + url: string; 114 + description: string; 115 + published: Date; 116 + lang: string; 117 + generator?: string; 118 + items: FeedItem[]; 119 + copyright?: string; 120 + author?: string; 121 + } 122 + 123 + export interface FeedItem { 124 + title: string; 125 + url: string; 126 + description: string; 127 + published: Date; 128 + updated?: Date; 129 + content: string; 130 + lang: string; 131 + podcast?: FeedPodcastItem; 132 + } 133 + 134 + export interface FeedPodcastItem { 135 + link: string; 136 + length: number; 137 + } 138 + 139 + const defaultGenerator = `Lume ${getCurrentVersion()}`; 140 + 141 + export default function (userOptions?: Options) { 142 + const options = merge(defaults, userOptions); 143 + 144 + return (site: Site) => { 145 + site.addEventListener("beforeSave", () => { 146 + const output = Array.isArray(options.output) 147 + ? options.output 148 + : [options.output]; 149 + 150 + const pages = site.search.pages( 151 + options.query, 152 + options.sort, 153 + options.limit, 154 + ) as Data[]; 155 + 156 + const { info, items } = options; 157 + const rootData = site.source.data.get("/") || {}; 158 + 159 + const feed: FeedData = { 160 + title: getDataValue(rootData, info.title), 161 + description: getDataValue(rootData, info.description), 162 + published: getDataValue(rootData, info.published), 163 + lang: getDataValue(rootData, info.lang), 164 + url: site.url("", true), 165 + generator: info.generator === true 166 + ? defaultGenerator 167 + : info.generator || undefined, 168 + copyright: `© ${new Date().getFullYear()} ${getDataValue(rootData, info.author)}`, 169 + author: getDataValue(rootData, info.author), 170 + items: pages.map((data): FeedItem => { 171 + const content = getDataValue(data, items.content)?.toString(); 172 + const pageUrl = site.url(data.url, true); 173 + const fixedContent = fixUrls(new URL(pageUrl), content || ""); 174 + 175 + return { 176 + title: getDataValue(data, items.title), 177 + url: site.url(data.url, true), 178 + description: getDataValue(data, items.description), 179 + published: getDataValue(data, items.published), 180 + updated: getDataValue(data, items.updated), 181 + content: fixedContent, 182 + lang: getDataValue(data, items.lang), 183 + podcast: getDataValue(data, items.podcast), 184 + }; 185 + }), 186 + }; 187 + 188 + for (const filename of output) { 189 + const format = getExtension(filename).slice(1); 190 + const file = site.url(filename, true); 191 + 192 + switch (format) { 193 + case "rss": 194 + case "xml": 195 + site.pages.push( 196 + Page.create({ url: filename, content: generateRss(feed, file) }), 197 + ); 198 + break; 199 + 200 + default: 201 + throw new Error(`Invalid Feed format "${format}"`); 202 + } 203 + } 204 + }); 205 + }; 206 + } 207 + 208 + function fixUrls(base: URL, html: string): string { 209 + return html.replaceAll( 210 + /\s(href|src)="([^"]+)"/g, 211 + (_match, attr, value) => ` ${attr}="${new URL(value, base).href}"`, 212 + ); 213 + } 214 + 215 + function generateRss(data: FeedData, file: string): string { 216 + const feed = { 217 + [$XML]: { cdata: [["rss", "channel", "item", "content:encoded"]] }, 218 + xml: { 219 + "@version": "1.0", 220 + "@encoding": "UTF-8", 221 + }, 222 + rss: { 223 + "@xmlns:content": "http://purl.org/rss/1.0/modules/content/", 224 + "@xmlns:wfw": "http://wellformedweb.org/CommentAPI/", 225 + "@xmlns:dc": "http://purl.org/dc/elements/1.1/", 226 + "@xmlns:atom": "http://www.w3.org/2005/Atom", 227 + "@xmlns:sy": "http://purl.org/rss/1.0/modules/syndication/", 228 + "@xmlns:slash": "http://purl.org/rss/1.0/modules/slash/", 229 + "@xmlns:itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd", 230 + "@version": "2.0", 231 + channel: clean({ 232 + title: data.title, 233 + link: data.url, 234 + "atom:link": { 235 + "@href": file, 236 + "@rel": "self", 237 + "@type": "application/rss+xml", 238 + }, 239 + description: data.description, 240 + lastBuildDate: data.published.toUTCString(), 241 + language: data.lang, 242 + generator: data.generator, 243 + copyright: data.copyright, 244 + "itunes:author": data.author, 245 + "itunes:name": data.title, 246 + "itunes:category": { 247 + "@text": "Technology" 248 + }, 249 + "itunes:explicit": "false", 250 + "itunes:image": { 251 + "@href": "https://cdn.xeiaso.net/file/christine-static/xecast/itunes-image.jpg", 252 + }, 253 + item: data.items.map((item) => 254 + clean({ 255 + title: item.title, 256 + link: item.url, 257 + "itunes:title": item.title, 258 + "itunes:summary": item.description, 259 + guid: { 260 + "@isPermaLink": false, 261 + "#text": item.url, 262 + }, 263 + description: item.description, 264 + "content:encoded": item.content, 265 + pubDate: item.published.toUTCString(), 266 + "atom:updated": item.updated?.toISOString(), 267 + enclosure: { 268 + "@url": item.podcast?.link, 269 + "@length": item.podcast?.length, 270 + "@type": "audio/mpeg", 271 + } 272 + }) 273 + ), 274 + }), 275 + }, 276 + }; 277 + 278 + return stringify(feed); 279 + } 280 + 281 + /** Remove undefined values of an object */ 282 + function clean(obj: Record<string, unknown>) { 283 + return Object.fromEntries( 284 + Object.entries(obj).filter(([, value]) => value !== undefined), 285 + ); 286 + }
+4
lume/src/xecast/001.mdx
··· 2 2 title: "Xecast Episode 1: Origins and Techaro" 3 3 date: 2024-07-28 4 4 image: "xecast/episodes/001" 5 + desc: "Xe Iaso talks about their background in tech, the Techaro series, and AI." 6 + podcast: 7 + link: "https://cdn.xeiaso.net/file/christine-static/xecast/episodes/001.mp3" 8 + length: "46339200" 5 9 --- 6 10 7 11 <Picture path="xecast/episodes/001" />
+4
lume/src/xecast/002.mdx
··· 2 2 title: "Xecast Episode 2: Conferences, homelabs, and AI" 3 3 date: 2024-08-11 4 4 image: "xecast/episodes/002" 5 + desc: "Xe Iaso shares their DevOpsDays MSP experience, home studio upgrades, and AI musings." 6 + podcast: 7 + link: "https://cdn.xeiaso.net/file/christine-static/xecast/episodes/002.mp3" 8 + length: "49996935" 5 9 --- 6 10 7 11 <Picture path="xecast/episodes/002" />