Skip to content

Commit

Permalink
lume: add xecast RSS feed
Browse files Browse the repository at this point in the history
Signed-off-by: Xe Iaso <[email protected]>
  • Loading branch information
Xe committed Aug 15, 2024
1 parent 2e03f38 commit b40fed8
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 0 deletions.
18 changes: 18 additions & 0 deletions lume/_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import readInfo from "lume/plugins/reading_info.ts";

import annotateYear from "./plugins/annotate_year.ts";
import feed from "./plugins/feed.ts";
import podcast_feed from "./plugins/podcast_feed.ts";

//import pagefind from "lume/plugins/pagefind.ts";
//import _ from "npm:@pagefind/linux-x64";
Expand Down Expand Up @@ -96,6 +97,23 @@ site.use(feed({
},*/
},
}));

site.use(podcast_feed({
output: ["/xecast.rss"],
query: "is_xecast=true",
info: {
title: "Xecast",
description: "Thoughts and musings from Xe Iaso, now in podcast form",
published: new Date(),
lang: "en",
},
items: {
title: "=title",
description: "=desc",
podcast: "=podcast",
},
}))

site.use(mdx({
components: {
"BlockQuote": BlockQuote,
Expand Down
286 changes: 286 additions & 0 deletions lume/plugins/podcast_feed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import { getExtension } from "lume/core/utils/path.ts";
import { merge } from "lume/core/utils/object.ts";
import { getCurrentVersion } from "lume/core/utils/lume_version.ts";
import { getDataValue } from "lume/core/utils/data_values.ts";
import { $XML, stringify } from "lume/deps/xml.ts";
import { Page } from "lume/core/file.ts";

import type Site from "lume/core/site.ts";
import type { Data } from "lume/core/file.ts";
import { info } from "lume/deps/log.ts";

export interface Options {
/** The output filenames */
output?: string | string[];

/** The query to search the pages */
query?: string;

/** The sort order */
sort?: string;

/** The maximum number of items */
limit?: number;

/** The feed info */
info?: FeedInfoOptions;

/** The feed items configuration */
items?: FeedItemOptions;
}

export interface FeedInfoOptions {
/** The feed title */
title?: string;

/** The feed subtitle */
subtitle?: string;

/**
* The feed published date
* @default `new Date()`
*/
published?: Date;

/** The feed description */
description?: string;

/** The feed language */
lang?: string;

/** The feed generator. Set `true` to generate automatically */
generator?: string | boolean;

/** The feed author */
author?: string;
}

export interface FeedItemOptions {
/** The item title */
title?: string | ((data: Data) => string | undefined);

/** The item description */
description?: string | ((data: Data) => string | undefined);

/** The item published date */
published?: string | ((data: Data) => Date | undefined);

/** The item updated date */
updated?: string | ((data: Data) => Date | undefined);

/** The item content */
content?: string | ((data: Data) => string | undefined);

/** The item language */
lang?: string | ((data: Data) => string | undefined);

podcast?: string | ((data: Data) => FeedPodcastItem | undefined);
}

export const defaults: Options = {
/** The output filenames */
output: "/feed.rss",

/** The query to search the pages */
query: "",

/** The sort order */
sort: "date=desc",

/** The maximum number of items */
limit: 10,

/** The feed info */
info: {
title: "My RSS Feed",
published: new Date(),
description: "",
lang: "en",
generator: true,
author: "Xe Iaso",
},
items: {
title: "=title",
description: "=description",
published: "=date",
content: "=children",
lang: "=lang",
},
};

export interface FeedData {
title: string;
url: string;
description: string;
published: Date;
lang: string;
generator?: string;
items: FeedItem[];
copyright?: string;
author?: string;
}

export interface FeedItem {
title: string;
url: string;
description: string;
published: Date;
updated?: Date;
content: string;
lang: string;
podcast?: FeedPodcastItem;
}

export interface FeedPodcastItem {
link: string;
length: number;
}

const defaultGenerator = `Lume ${getCurrentVersion()}`;

export default function (userOptions?: Options) {
const options = merge(defaults, userOptions);

return (site: Site) => {
site.addEventListener("beforeSave", () => {
const output = Array.isArray(options.output)
? options.output
: [options.output];

const pages = site.search.pages(
options.query,
options.sort,
options.limit,
) as Data[];

const { info, items } = options;
const rootData = site.source.data.get("/") || {};

const feed: FeedData = {
title: getDataValue(rootData, info.title),
description: getDataValue(rootData, info.description),
published: getDataValue(rootData, info.published),
lang: getDataValue(rootData, info.lang),
url: site.url("", true),
generator: info.generator === true
? defaultGenerator
: info.generator || undefined,
copyright: ${new Date().getFullYear()} ${getDataValue(rootData, info.author)}`,
author: getDataValue(rootData, info.author),
items: pages.map((data): FeedItem => {
const content = getDataValue(data, items.content)?.toString();
const pageUrl = site.url(data.url, true);
const fixedContent = fixUrls(new URL(pageUrl), content || "");

return {
title: getDataValue(data, items.title),
url: site.url(data.url, true),
description: getDataValue(data, items.description),
published: getDataValue(data, items.published),
updated: getDataValue(data, items.updated),
content: fixedContent,
lang: getDataValue(data, items.lang),
podcast: getDataValue(data, items.podcast),
};
}),
};

for (const filename of output) {
const format = getExtension(filename).slice(1);
const file = site.url(filename, true);

switch (format) {
case "rss":
case "xml":
site.pages.push(
Page.create({ url: filename, content: generateRss(feed, file) }),
);
break;

default:
throw new Error(`Invalid Feed format "${format}"`);
}
}
});
};
}

function fixUrls(base: URL, html: string): string {
return html.replaceAll(
/\s(href|src)="([^"]+)"/g,
(_match, attr, value) => ` ${attr}="${new URL(value, base).href}"`,
);
}

function generateRss(data: FeedData, file: string): string {
const feed = {
[$XML]: { cdata: [["rss", "channel", "item", "content:encoded"]] },
xml: {
"@version": "1.0",
"@encoding": "UTF-8",
},
rss: {
"@xmlns:content": "http://purl.org/rss/1.0/modules/content/",
"@xmlns:wfw": "http://wellformedweb.org/CommentAPI/",
"@xmlns:dc": "http://purl.org/dc/elements/1.1/",
"@xmlns:atom": "http://www.w3.org/2005/Atom",
"@xmlns:sy": "http://purl.org/rss/1.0/modules/syndication/",
"@xmlns:slash": "http://purl.org/rss/1.0/modules/slash/",
"@xmlns:itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd",
"@version": "2.0",
channel: clean({
title: data.title,
link: data.url,
"atom:link": {
"@href": file,
"@rel": "self",
"@type": "application/rss+xml",
},
description: data.description,
lastBuildDate: data.published.toUTCString(),
language: data.lang,
generator: data.generator,
copyright: data.copyright,
"itunes:author": data.author,
"itunes:name": data.title,
"itunes:category": {
"@text": "Technology"
},
"itunes:explicit": "false",
"itunes:image": {
"@href": "https://cdn.xeiaso.net/file/christine-static/xecast/itunes-image.jpg",
},
item: data.items.map((item) =>
clean({
title: item.title,
link: item.url,
"itunes:title": item.title,
"itunes:summary": item.description,
guid: {
"@isPermaLink": false,
"#text": item.url,
},
description: item.description,
"content:encoded": item.content,
pubDate: item.published.toUTCString(),
"atom:updated": item.updated?.toISOString(),
enclosure: {
"@url": item.podcast?.link,
"@length": item.podcast?.length,
"@type": "audio/mpeg",
}
})
),
}),
},
};

return stringify(feed);
}

/** Remove undefined values of an object */
function clean(obj: Record<string, unknown>) {
return Object.fromEntries(
Object.entries(obj).filter(([, value]) => value !== undefined),
);
}
4 changes: 4 additions & 0 deletions lume/src/xecast/001.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
title: "Xecast Episode 1: Origins and Techaro"
date: 2024-07-28
image: "xecast/episodes/001"
desc: "Xe Iaso talks about their background in tech, the Techaro series, and AI."
podcast:
link: "https://cdn.xeiaso.net/file/christine-static/xecast/episodes/001.mp3"
length: "46339200"
---

<Picture path="xecast/episodes/001" />
Expand Down
4 changes: 4 additions & 0 deletions lume/src/xecast/002.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
title: "Xecast Episode 2: Conferences, homelabs, and AI"
date: 2024-08-11
image: "xecast/episodes/002"
desc: "Xe Iaso shares their DevOpsDays MSP experience, home studio upgrades, and AI musings."
podcast:
link: "https://cdn.xeiaso.net/file/christine-static/xecast/episodes/002.mp3"
length: "49996935"
---

<Picture path="xecast/episodes/002" />
Expand Down

0 comments on commit b40fed8

Please sign in to comment.