Skip to content

Commit

Permalink
renamed the plugin rss to feed, added support for JSON Feed
Browse files Browse the repository at this point in the history
  • Loading branch information
oscarotero committed Apr 27, 2023
1 parent 3ea74a2 commit 5c7a8aa
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 173 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Any BREAKING CHANGE between minor versions will be documented here in upper case

## [1.17.0] - Unreleased
### Added
- RSS Plugin [#413]
- Feed Plugin [#413]
- Support for negative tags in `search` plugin. For example:
`search.pages("tag1 !tag2")`.
- Support for remote files in `sass` plugin.
Expand Down
2 changes: 1 addition & 1 deletion core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const pluginNames = [
"date",
"esbuild",
"eta",
"feed",
"filter_pages",
"imagick",
"inline",
Expand All @@ -35,7 +36,6 @@ export const pluginNames = [
"relative_urls",
"remark",
"resolve_urls",
"rss",
"sass",
"sheets",
"sitemap",
Expand Down
207 changes: 207 additions & 0 deletions plugins/feed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import {
DeepPartial,
getExtension,
getLumeVersion,
merge,
} from "../core/utils.ts";
import { getDataValue } from "./utils.ts";
import { $XML, stringify } from "../deps/xml.ts";
import { Page } from "../core/filesystem.ts";
import { Search } from "../plugins/search.ts";

import type { Data, Site } from "../core.ts";

export interface Options {
output: string | string[];
query: string;
sort: string;
limit: number;
info: {
title: string;
subtitle?: string;
date: Date;
description: string;
lang: string;
generator: string | boolean;
};
items: {
title: string;
description: string;
date: string;
content: string;
lang: string;
};
}

export const defaults: Options = {
output: "/feed.rss",
query: "",
sort: "date=desc",
limit: 10,
info: {
title: "My RSS Feed",
date: new Date(),
description: "",
lang: "en",
generator: true,
},
items: {
title: "title",
description: "description",
date: "date",
content: "children",
lang: "lang",
},
};

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

export interface FeedItem {
title: string;
url: string;
description: string;
date: Date;
content: string;
lang: string;
}

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

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

return (site: Site) => {
const search = new Search(site, true);

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

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

const feed: FeedData = {
title: info.title,
description: info.description,
date: info.date,
lang: info.lang,
url: site.url("", true),
generator: info.generator === true
? defaultGenerator
: info.generator || undefined,
items: pages.map((data): FeedItem => {
return {
title: options.items.title &&
getDataValue(data, `=${options.items.title}`),
url: site.url(data.url as string, true),
description: options.items.description &&
getDataValue(data, `=${options.items.description}`),
date: options.items.date &&
getDataValue(data, `=${options.items.date}`),
content: options.items.content &&
getDataValue(data, `=${options.items.content}`)?.toString(),
lang: options.items.lang &&
getDataValue(data, `=${options.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(filename, generateRss(feed, file)));
break;

case "json":
site.pages.push(Page.create(filename, generateJson(feed, file)));
break;

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

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: {
title: data.title,
link: data.url,
"atom:link": {
"@href": file,
"@rel": "self",
"@type": "application/rss+xml",
},
description: data.description,
lastBuildDate: data.date.toISOString(),
language: data.lang,
generator: data.generator,
item: data.items.map((item) => ({
title: item.title,
link: item.url,
guid: {
"@isPermaLink": false,
"#text": item.url,
},
description: item.description,
"content:encoded": item.content,
pubDate: item.date.toISOString(),
})),
},
},
};

return stringify(feed);
}

function generateJson(data: FeedData, file: string): string {
const feed = {
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) => ({
id: item.url,
url: item.url,
title: item.title,
content_html: item.content,
date_published: item.date.toUTCString(),
})),
};

return JSON.stringify(feed);
}
66 changes: 15 additions & 51 deletions plugins/metas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getLumeVersion, merge } from "../core/utils.ts";
import { getDataValue } from "./utils.ts";

import type { Page, Site } from "../core.ts";
import type { HTMLDocument } from "../deps/dom.ts";
Expand All @@ -9,14 +10,6 @@ export interface Options {

/** The key name for the transformations definitions */
name: string;

/**
* Use page data as meta data if the correspond metas value does not exists
* @deprecated Use "=key" syntax instead
*/
defaultPageData?: {
[K in keyof MetaData]?: string;
};
}

export interface MetaData {
Expand Down Expand Up @@ -83,55 +76,26 @@ export default function (userOptions?: Partial<Options>) {
return;
}

const getMetaValue = <T extends keyof MetaData>(key: T) => {
const value = metas[key];

// Get the value from the page data
if (typeof value === "string" && value.startsWith("=")) {
const key = value.slice(1);

if (!key.includes(".")) {
return page.data[key];
}

const keys = key.split(".");
let val = page.data;
for (const key of keys) {
val = val[key];
}
return val;
} else if (value === undefined || value === null) {
// Get the value from the default page data
if (options.defaultPageData && key in options.defaultPageData) {
const pageKey = options.defaultPageData[key];
if (pageKey) {
return page.data[pageKey] as MetaData[T];
}
}
} else {
return value;
}
};

const { document } = page;
const metaIcon = getMetaValue("icon");
const metaImage = getMetaValue("image");
const { document, data } = page;
const metaIcon = getDataValue(data, metas["icon"]);
const metaImage = getDataValue(data, metas["image"]);

const url = site.url(page.data.url as string, true);
const icon = metaIcon ? new URL(site.url(metaIcon), url).href : undefined;
const image = metaImage
? new URL(site.url(metaImage), url).href
: undefined;

const type = getMetaValue("type");
const site_name = getMetaValue("site");
const lang = getMetaValue("lang");
const title = getMetaValue("title");
const description = getMetaValue("description");
const twitter = getMetaValue("twitter");
const keywords = getMetaValue("keywords");
const robots = getMetaValue("robots");
const color = getMetaValue("color");
const generator = getMetaValue("generator");
const type = getDataValue(data, metas["type"]);
const site_name = getDataValue(data, metas["site"]);
const lang = getDataValue(data, metas["lang"]);
const title = getDataValue(data, metas["title"]);
const description = getDataValue(data, metas["description"]);
const twitter = getDataValue(data, metas["twitter"]);
const keywords = getDataValue(data, metas["keywords"]);
const robots = getDataValue(data, metas["robots"]);
const color = getDataValue(data, metas["color"]);
const generator = getDataValue(data, metas["generator"]);

// Open graph
addMeta(document, "property", "og:type", type || "website");
Expand Down
Loading

0 comments on commit 5c7a8aa

Please sign in to comment.