fleet/website/api/controllers/download-rss-feed.js
kilo-code-bot[bot] 262234f5ac
Fix fleetdm.com RSS feed to pass W3C Feed Validator (#42068)
## Summary

Fixes all errors and warnings reported by the [W3C Feed Validation
Service](https://validator.w3.org/feed/check.cgi?url=https%3A%2F%2Ffleetdm.com%2Frss%2Farticles)
for the `/rss/articles` endpoint (and all other `/rss/:categoryName`
endpoints).

## Changes

Only one file modified: `website/api/controllers/download-rss-feed.js`

### Errors fixed
- **`lastBuildDate` not RFC-822 format**: Changed from `new
Date(Date.now())` (which produces JS `toString()` format like `Thu Mar
19 2026 14:45:30 GMT+0000 (Coordinated Universal Time)`) to `new
Date().toUTCString()` (which produces RFC-822 format like `Thu, 19 Mar
2026 14:45:30 GMT`)
- **`pubDate` not RFC-822 format** (431 occurrences): Changed from
`.toJSON()` (ISO 8601) to `.toUTCString()` (RFC-822)
- **Missing channel `<link>` element**: Added `<link>` element at the
channel level pointing to the category page

### Warnings fixed
- **Missing `guid` on items** (431 occurrences): Added `<guid
isPermaLink="true">` to each item using the article's permalink URL
- **Missing `atom:link` with `rel="self"`**: Added `xmlns:atom`
namespace to the `<rss>` element and an `<atom:link href="..."
rel="self" type="application/rss+xml"/>` element in the channel

### Additional fix
- Fixed a minor bug where the image `<link>` URL was missing a `/`
separator between the domain and category name (`fleetdm.comarticles` →
`fleetdm.com/articles`)

### Not addressed
- The "Invalid HTML: Named entity expected" warning about `&#39;`
entities in descriptions. This is produced by Lodash's `_.escape()`
which correctly escapes apostrophes for XML content. The `&#39;` entity
is valid XML — the validator flags it only in an HTML parsing context,
and it does not affect feed validity or reader interoperability.

---

Built for [Brock
Walters](https://fleetdm.slack.com/archives/C097P4TAPRR/p1773932018039599)
by [Kilo for Slack](https://kilo.ai/features/slack-integration)

---------

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
Co-authored-by: Eric <[email protected]>
2026-03-20 11:42:26 -05:00

145 lines
5.4 KiB
JavaScript
Vendored

module.exports = {
friendlyName: 'Download rss feed',
description: 'Generate and return an RSS feed for a category of Fleet\'s articles',
inputs: {
categoryName: {
type: 'string',
required: true,
isIn: [
'success-stories',
'securing',
'releases',
'engineering',
'guides',
'announcements',
'deploy',
'podcasts',
'report',
'articles',
],
}
},
exits: {
success: { outputFriendlyName: 'RSS feed XML', outputType: 'string' },
badConfig: { responseType: 'badConfig' },
},
fn: async function ({categoryName}) {
if (!_.isObject(sails.config.builtStaticContent)) {
throw {badConfig: 'builtStaticContent'};
} else if (!_.isArray(sails.config.builtStaticContent.markdownPages)) {
throw {badConfig: 'builtStaticContent.markdownPages'};
}
// Start building the rss feed
let rssFeedXml = '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel>';
// Build the description and title for this RSS feed.
let articleCategoryTitle = '';
let categoryDescription = '';
switch(categoryName) {
case 'success-stories':
articleCategoryTitle = 'Success stories | Fleet blog';
categoryDescription = 'Read about how others are using Fleet and osquery.';
break;
case 'securing':
articleCategoryTitle = 'Security | Fleet blog';
categoryDescription = 'Learn more about how we secure Fleet.';
break;
case 'releases':
articleCategoryTitle = 'Releases | Fleet blog';
categoryDescription = 'Read about the latest release of Fleet.';
break;
case 'engineering':
articleCategoryTitle = 'Engineering | Fleet blog';
categoryDescription = 'Read about engineering at Fleet and beyond.';
break;
case 'guides':
articleCategoryTitle = 'Guides | Fleet blog';
categoryDescription = 'Learn more about how to use Fleet to accomplish your goals.';
break;
case 'announcements':
articleCategoryTitle = 'Announcements | Fleet blog';
categoryDescription = 'The latest news from Fleet.';
break;
case 'deploy':
articleCategoryTitle = 'Deployment guides | Fleet blog';
categoryDescription = 'Learn more about how to deploy Fleet.';
break;
case 'podcasts':
articleCategoryTitle = 'Podcasts | Fleet blog';
categoryDescription = 'Listen to the Future of Device Management podcast';
break;
case 'report':
articleCategoryTitle = 'Reports | Fleet blog';
categoryDescription = '';
break;
case 'articles':
articleCategoryTitle = 'Fleet blog | Fleet';
categoryDescription = 'Read all articles from Fleet\'s blog.';
}
let rssFeedTitle = `<title>${_.escape(articleCategoryTitle)}</title>`;
let rssFeedDescription = `<description>${_.escape(categoryDescription)}</description>`;
let rssFeedLink = `<link>${_.escape('https://fleetdm.com/'+categoryName)}</link>`;
let rssFeedAtomLink = `<atom:link href="${_.escape('https://fleetdm.com/rss/'+categoryName)}" rel="self" type="application/rss+xml"/>`;
let rsslastBuildDate = `<lastBuildDate>${_.escape(new Date().toUTCString())}</lastBuildDate>`;
let rssFeedImage = `<image><link>${_.escape('https://fleetdm.com/'+categoryName)}</link><title>${_.escape(articleCategoryTitle)}</title><url>${_.escape('https://fleetdm.com/images/[email protected]')}</url></image>`;
rssFeedXml += `${rssFeedTitle}${rssFeedDescription}${rssFeedLink}${rssFeedAtomLink}${rsslastBuildDate}${rssFeedImage}`;
// Determine the subset of articles that will be used to squirt out an XML string.
let articlesToAddToFeed = [];
if (categoryName === 'articles') {
// If the category is `articles` we'll build a rss feed that contains all articles
articlesToAddToFeed = sails.config.builtStaticContent.markdownPages.filter((page)=>{
if(_.startsWith(page.htmlId, 'articles')) {
return page;
}
});//∞
} else {
// If the user requested a specific category, we'll only build a feed with articles in that category
articlesToAddToFeed = sails.config.builtStaticContent.markdownPages.filter((page)=>{
if(_.startsWith(page.url, '/'+categoryName)) {
return page;
}
});//∞
}
articlesToAddToFeed = _.sortByOrder(articlesToAddToFeed, 'meta.publishedOn', 'ASC');
// Iterate through the filtered array of articles, adding <item> elements for each article.
for (let pageInfo of articlesToAddToFeed) {
let articleUrl = 'https://fleetdm.com'+pageInfo.url;
let rssItemTitle = `<title>${_.escape(pageInfo.meta.articleTitle)}</title>`;
let rssItemDescription = `<description>${_.escape(pageInfo.meta.description)}</description>`;
let rssItemLink = `<link>${_.escape(articleUrl)}</link>`;
let rssItemGuid = `<guid isPermaLink="true">${_.escape(articleUrl)}</guid>`;
let rssItemPublishDate = `<pubDate>${_.escape(new Date(pageInfo.meta.publishedOn).toUTCString())}</pubDate>`;
// Add the article to the feed.
rssFeedXml += `<item>${rssItemTitle}${rssItemDescription}${rssItemLink}${rssItemGuid}${rssItemPublishDate}</item>`;
}
rssFeedXml += `</channel></rss>`;
// Set the response type
this.res.type('text/xml');
// Return the generated RSS feed
return rssFeedXml;
}
};