Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 1 addition & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ feed:
order_by: -date
icon: icon.png
autodiscovery: true
template:
```
- **enable** - Enables or disables this plugin. Enabled by default.
- **type** - Feed type. `atom` or `rss2`. Specify `['atom', 'rss2']` to output both types. (Default: `atom`)
Expand All @@ -57,28 +56,11 @@ feed:
```
- **path** - Feed path. When both types are specified, path must follow the order of type value. (Default: atom.xml/rss2.xml)
- **limit** - Maximum number of posts in the feed (Use `0` or `false` to show all posts)
- **hub** - URL of the PubSubHubbub hubs (Leave it empty if you don't use it)
- **hub** - (optional) URL of the PubSubHubbub hubs (Leave it empty if you don't use it)
- **content** - (optional) set to 'true' to include the contents of the entire post in the feed.
- **content_limit** - (optional) Default length of post content used in summary. Only used, if **content** setting is false and no custom post description present.
- **content_limit_delim** - (optional) If **content_limit** is used to shorten post contents, only cut at the last occurrence of this delimiter before reaching the character limit. Not used by default.
- **order_by** - Feed order-by. (Default: -date)
- **icon** - (optional) Custom feed icon. Defaults to a gravatar of email specified in the main config.
- **autodiscovery** - Add feed [autodiscovery](https://www.rssboard.org/rss-autodiscovery). (Default: `true`)
* Many themes already offer this feature, so you may also need to adjust the theme's config if you wish to disable it.
- **template** - Custom template path(s). This file will be used to generate feed xml file, see the default templates: [atom.xml](atom.xml) and [rss2.xml](rss2.xml).
* It is possible to specify just one custom template, even when this plugin is configured to output both feed types,
``` yaml
# (Optional) Exclude custom template from being copied into public/ folder
# Alternatively, you could also prepend an underscore to its filename, e.g. _custom.xml
# https://hexo.io/docs/configuration#Include-Exclude-Files-or-Folders
exclude:
- 'custom.xml'
feed:
type:
- atom
- rss2
template:
- ./source/custom.xml
# atom will be generated using custom.xml
# rss2 will be generated using the default template instead
```
58 changes: 0 additions & 58 deletions atom.xml

This file was deleted.

3 changes: 1 addition & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ hexo.config.feed = Object.assign({
content_limit: 140,
content_limit_delim: '',
order_by: '-date',
autodiscovery: true,
template: ''
autodiscovery: true
}, hexo.config.feed);

const config = hexo.config.feed;
Expand Down
226 changes: 182 additions & 44 deletions lib/generator.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,201 @@
'use strict';

const nunjucks = require('nunjucks');
const env = new nunjucks.Environment();
const { join } = require('path');
const { readFileSync } = require('fs');
const { encodeURL, gravatar, full_url_for } = require('hexo-util');
const { generateRssFeed, generateAtomFeed } = require('feedsmith');
const { gravatar, full_url_for, encodeURL } = require('hexo-util');

env.addFilter('uriencode', str => {
return encodeURL(str);
});
function composePosts(posts, feedConfig) {
const { limit, order_by } = feedConfig;

env.addFilter('noControlChars', str => {
return str.replace(/[\x00-\x1F\x7F]/g, ''); // eslint-disable-line no-control-regex
});
let processedPosts = posts.sort(order_by || '-date');
processedPosts = processedPosts.filter(post => post.draft !== true);

module.exports = function(locals, type, path) {
const { config } = this;
const { email, feed, url: urlCfg } = config;
const { icon: iconCfg, limit, order_by, template: templateCfg, type: typeCfg } = feed;

env.addFilter('formatUrl', str => {
return full_url_for.call(this, str);
});
if (limit) processedPosts = processedPosts.limit(limit);

let tmplSrc = join(__dirname, `../${type}.xml`);
if (templateCfg) {
if (typeof templateCfg === 'string') tmplSrc = templateCfg;
else tmplSrc = templateCfg[typeCfg.indexOf(type)];
}
const template = nunjucks.compile(readFileSync(tmplSrc, 'utf8'), env);
return processedPosts;
}

let posts = locals.posts.sort(order_by || '-date');
posts = posts.filter(post => {
return post.draft !== true;
});
function composeFeed(config, path, context, posts) {
const { feed: feedConfig, url: urlConfig, email } = config;
const { icon: iconConfig, hub } = feedConfig;

if (posts.length <= 0) {
feed.autodiscovery = false;
return;
}

if (limit) posts = posts.limit(limit);

let url = urlCfg;
let url = urlConfig;
if (url[url.length - 1] !== '/') url += '/';

let icon = '';
if (iconCfg) icon = full_url_for.call(this, iconCfg);
if (iconConfig) icon = full_url_for.call(context, iconConfig);
else if (email) icon = gravatar(email);

const feed_url = full_url_for.call(this, path);
const feedUrl = full_url_for.call(context, path);
const currentYear = new Date().getFullYear();

const data = template.render({
config,
return {
title: config.title,
description: config.subtitle || config.description,
url,
feedUrl,
icon,
posts,
feed_url
});
hub,
language: config.language,
author: { name: config.author, email: config.email },
copyright: config.author && `All rights reserved ${currentYear}, ${config.author}`,
updated: posts.first().updated ? posts.first().updated.toDate() : posts.first().date.toDate()
};
}

function composeFeedLinks(feedUrl, hub, type) {
const links = [{ href: encodeURL(feedUrl), rel: 'self', type }];
if (hub) links.push({ href: encodeURL(hub), rel: 'hub' });
return links;
}

function composeItemDescription(post, feedConfig) {
const { content_limit, content_limit_delim } = feedConfig;

if (post.description) {
return post.description;
} else if (post.intro) {
return post.intro;
} else if (post.excerpt) {
return post.excerpt;
} else if (post.content) {
const short_content = post.content.substring(0, content_limit || 140);
if (content_limit_delim) {
const delim_pos = short_content.lastIndexOf(content_limit_delim);
if (delim_pos > -1) {
return short_content.substring(0, delim_pos);
}
}
return short_content;
}
return '';
}

function composeItemContent(post, feedConfig) {
const { content } = feedConfig;

if (content && post.content) {
return post.content.replace(/[\x00-\x1F\x7F]/g, ''); // eslint-disable-line no-control-regex
}
return '';
}

function composeItemCategories(post) {
const items = [
...(post.categories ? post.categories.toArray() : []),
...(post.tags ? post.tags.toArray() : [])
];
return items.map(item => ({ name: item.name, domain: item.permalink }));
}

function composeItem(post, feedConfig, context) {
return {
title: post.title,
link: encodeURL(full_url_for.call(context, post.permalink)),
description: composeItemDescription(post, feedConfig),
published: post.date.toDate(),
updated: post.updated ? post.updated.toDate() : post.date.toDate(),
content: composeItemContent(post, feedConfig),
enclosures: post.image && [{ url: full_url_for.call(context, post.image) }],
categories: composeItemCategories(post)
};
}

function composeRssItem(feed, item) {
return {
title: item.title,
link: item.link,
guid: item.link,
description: item.description,
pubDate: item.published,
authors: [feed.author],
content: { encoded: item.content },
enclosures: item.enclosures,
categories: item.categories
};
}

function composeAtomEntry(feed, item) {
const entryLinks = [
{ href: item.link },
...(item.enclosures || []).map(enclosure => ({ href: enclosure.url, rel: 'enclosure' }))
];

return {
title: item.title,
id: item.link,
links: entryLinks,
summary: item.description,
content: item.content,
published: item.published,
updated: item.updated || item.published,
authors: feed.author.name && [feed.author],
categories: item.categories.map(cat => ({ term: cat.name, scheme: cat.domain }))
};
}

function generateRss(feed, items) {
const links = composeFeedLinks(feed.feedUrl, feed.hub, 'application/rss+xml');

return generateRssFeed({
title: feed.title,
description: feed.description,
link: encodeURL(feed.url),
language: feed.language,
copyright: feed.copyright,
generator: 'Hexo',
lastBuildDate: feed.updated,
image: feed.icon && {
url: feed.icon,
title: feed.title,
link: encodeURL(feed.url)
},
atom: { links },
items: items.map(item => composeRssItem(feed, item))
}, { lenient: true });
}

function generateAtom(feed, items) {
const links = [
{ href: encodeURL(feed.url), rel: 'alternate' },
...composeFeedLinks(feed.feedUrl, feed.hub)
];

return generateAtomFeed({
title: feed.title,
id: encodeURL(feed.url),
subtitle: feed.description,
updated: feed.updated,
links,
generator: { text: 'Hexo', uri: 'https://hexo.io/' },
icon: feed.icon,
rights: feed.copyright,
authors: feed.author.name && [feed.author],
entries: items.map(item => composeAtomEntry(feed, item))
}, { lenient: true });
}

module.exports = function(locals, type, path) {
const { config } = this;
const { feed: feedConfig } = config;

const posts = composePosts(locals.posts, feedConfig);

if (posts.length <= 0) {
feedConfig.autodiscovery = false;
return;
}

const feed = composeFeed(config, path, this, posts);
const items = posts.toArray().map(post => composeItem(post, feedConfig, this));

let data;
switch (type) {
case 'rss2':
data = generateRss(feed, items);
break;
default:
data = generateAtom(feed, items);
}

return {
path,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
],
"license": "MIT",
"dependencies": {
"hexo-util": "^4.0.0",
"nunjucks": "^3.2.3"
"feedsmith": "^2.4.0",
"hexo-util": "^4.0.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
Expand Down
Loading