aboutsummaryrefslogtreecommitdiff
path: root/lume/plugins
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2024-02-24 16:07:38 -0500
committerXe Iaso <me@xeiaso.net>2024-02-24 16:07:38 -0500
commit774fccbe94f9a252c72d5394e5cbd027aaaebf64 (patch)
treef665950a197fe4cad115b9f07557acfa887e486a /lume/plugins
parent93f6421e1d4e9ce9a4bcf1a55815fed3e2cee962 (diff)
downloadxesite-774fccbe94f9a252c72d5394e5cbd027aaaebf64.tar.xz
xesite-774fccbe94f9a252c72d5394e5cbd027aaaebf64.zip
wire up the protofeed call
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'lume/plugins')
-rw-r--r--lume/plugins/feed.ts279
1 files changed, 279 insertions, 0 deletions
diff --git a/lume/plugins/feed.ts b/lume/plugins/feed.ts
new file mode 100644
index 0000000..8214b41
--- /dev/null
+++ b/lume/plugins/feed.ts
@@ -0,0 +1,279 @@
+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";
+
+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;
+}
+
+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);
+}
+
+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,
+ },
+ 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[];
+}
+
+export interface FeedItem {
+ title: string;
+ url: string;
+ description: string;
+ published: Date;
+ updated?: Date;
+ content: string;
+ lang: string;
+}
+
+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,
+ 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),
+ };
+ }),
+ };
+
+ for (const filename of output) {
+ const format = getExtension(filename).slice(1);
+ const file = site.url(filename, true);
+
+ switch (format) {
+ case "rss":
+ case "feed":
+ case "xml":
+ site.pages.push(
+ Page.create({ url: filename, content: generateRss(feed, file) }),
+ );
+ break;
+
+ case "json":
+ site.pages.push(
+ Page.create({ url: filename, content: generateJson(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/",
+ "@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,
+ item: data.items.map((item) =>
+ clean({
+ title: item.title,
+ link: item.url,
+ guid: {
+ "@isPermaLink": false,
+ "#text": item.url,
+ },
+ description: item.description,
+ "content:encoded": item.content,
+ pubDate: item.published.toUTCString(),
+ "atom:updated": item.updated?.toISOString(),
+ })
+ ),
+ }),
+ },
+ };
+
+ return stringify(feed);
+}
+
+function generateJson(data: FeedData, file: string): string {
+ const feed = clean({
+ version: "https://jsonfeed.org/version/1",
+ title: data.title,
+ home_page_url: data.url,
+ feed_url: file,
+ description: data.description,
+ items: data.items.map((item) =>
+ clean({
+ id: item.url,
+ url: item.url,
+ title: item.title,
+ content_html: item.content,
+ date_published: item.published.toISOString(),
+ date_modified: item.updated?.toISOString(),
+ })
+ ),
+ });
+
+ return JSON.stringify(feed);
+}
+
+/** Remove undefined values of an object */
+function clean(obj: Record<string, unknown>) {
+ return Object.fromEntries(
+ Object.entries(obj).filter(([, value]) => value !== undefined),
+ );
+} \ No newline at end of file