Website: Add RSS feeds for articles (#9526)

Closes: https://github.com/fleetdm/fleet/issues/6493

Changes:
- Added a new action, `get-one-rss-feed.js`. This action generates and
returns RSS feeds for article categories on fleetdm.com.
    - This action has one required input: `categoryName`.
- Lives at `/rss/[Article Category Name]` e.g.,
`fleetdm.com/rss/releases`
- If `articles` is provided as the category, it returns an RSS feed of
all articles published on our blog.
- Updated `view-basic-article.js` to set an `articleCategorySlug`
variable, that is used to link to the RSS feed for an article category
from an article page.
- Added a "subscribe" button to articles and article category pages that
links to an RSS feed for that category.
This commit is contained in:
Eric
2023-01-27 16:58:53 -06:00
committed by GitHub
parent 091ee35ba6
commit c238b90ecc
8 changed files with 192 additions and 6 deletions
@@ -45,6 +45,8 @@ module.exports = {
}
}
let articleCategorySlug = pageUrlSuffix.split('/')[0];
// Setting the pages meta title and description from the articles meta tags, as well as an article image, if provided.
// Note: Every article page should have a 'articleTitle' and a 'authorFullName' meta tag.
// Note: Leaving title and description as `undefined` in our view means we'll default to the generic title and description set in layout.ejs.
@@ -68,6 +70,7 @@ module.exports = {
pageTitleForMeta,
pageDescriptionForMeta,
pageImageForMeta: thisPage.meta.articleImageUrl || undefined,
articleCategorySlug
};
}
+141
View File
@@ -0,0 +1,141 @@
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"><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 for osquery';
categoryDescription = 'Read all articles from Fleet\'s blog.';
}
let rssFeedTitle = `<title>${_.escape(articleCategoryTitle)}</title>`;
let rssFeedDescription = `<description>${_.escape(categoryDescription)}</description>`;
let rsslastBuildDate = `<lastBuildDate>${_.escape(new Date(Date.now()))}</lastBuildDate>`;
let rssFeedImage = `<image><link>${_.escape('https://fleetdm.com'+categoryName)}</link><title>${_.escape(articleCategoryTitle)}</title><url>${_.escape('https://fleetdm.com/images/fleet-logo-square@2x.png')}</url></image>`;
rssFeedXml += `${rssFeedTitle}${rssFeedDescription}${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;
}
});//∞
}
// Iterate through the filtered array of articles, adding <item> elements for each article.
for (let pageInfo of articlesToAddToFeed) {
let rssItemTitle = `<title>${_.escape(pageInfo.meta.articleTitle)}</title>`;
let rssItemDescription = `<description>${_.escape(pageInfo.meta.description)}</description>`;
let rssItemLink = `<link>${_.escape('https://fleetdm.com'+pageInfo.url)}</link>`;
let rssItemPublishDate = `<pubDate>${_.escape(new Date(pageInfo.meta.publishedOn).toJSON())}</pubDate>`;
// Add the article to the feed.
rssFeedXml += `<item>${rssItemTitle}${rssItemDescription}${rssItemLink}${rssItemPublishDate}</item>`;
}
rssFeedXml += `</channel></rss>`;
// Set the response type
this.res.type('text/xml');
// Return the generated RSS feed
return rssFeedXml;
}
};
+15
View File
@@ -33,6 +33,21 @@
padding-top: 80px;
padding-bottom: 40px;
}
[purpose='rss-button'] {
padding: 4px 8px;
display: inline;
max-width: min-content;
color: #192147;
font-size: 14px;
text-decoration: none;
span::before {
font-family: 'FontAwesome';
content: '\f09e';
color: #192147;
padding-right: 8px;
font-size: 16px;
}
}
[purpose='articles'] {
padding-bottom: 80px;
+16
View File
@@ -24,6 +24,22 @@
}
}
[purpose='rss-button'] {
padding: 4px 8px;
display: inline;
max-width: min-content;
color: #192147;
text-decoration: none;
font-size: 14px;
span::before {
vertical-align: baseline;
font-family: 'FontAwesome';
content: '\f09e';
color: #192147;
padding-right: 8px;
font-size: 16px;
}
}
[purpose='article-details'] {
font-size: 14px;
line-height: 20px;
+1
View File
@@ -50,4 +50,5 @@ module.exports.policies = {
'view-fleet-mdm': true,
'deliver-mdm-beta-signup': true,
'deliver-apple-csr': true,
'download-rss-feed': true,
};
+4
View File
@@ -355,6 +355,10 @@ module.exports.routes = {
// XML file, which helps search engines know which pages are available on the website.
'GET /sitemap.xml': { action: 'download-sitemap' },
// RSS feeds
// =============================================================================================================
'GET /rss/:categoryName': {action: 'download-rss-feed'},
// Potential future pages
// =============================================================================================================
// Things that are not webpages here (in the Sails app) yet, but could be in the future. For now they are just
+4 -1
View File
@@ -19,7 +19,10 @@
</div>
<div purpose="category-title" v-else>
<h1>{{articleCategory}}</h1>
<p>{{categoryDescription}}</p>
<div class="d-flex flex-sm-row flex-column justify-content-between">
<p>{{categoryDescription}}</p>
<a purpose="rss-button" class="px-0 px-sm-2 pt-sm-1" :href="'/rss/'+category" target="_blank"><span>Subscribe</span></a>
</div>
</div>
<div purpose="articles" class="card-deck d-flex justify-content-center" v-if="selectedArticles.length > 0">
<div purpose="article-card" class="card" v-for="article in selectedArticles">
+8 -5
View File
@@ -4,11 +4,14 @@
<h1><%=thisPage.meta.articleTitle %></h1>
<h2 v-if="articleHasSubtitle && articleSubtitle !== undefined">{{articleSubtitle}}</h2>
</div>
<div purpose="article-details" class="d-flex flex-row align-items-center">
<span><js-timestamp format="billing" :at="thisPage.meta.publishedOn"></js-timestamp></span>
<span class="px-2">|</span>
<img style="height: 28px; width: 28px; border-radius: 100%;" alt="The author's GitHub profile picture" :src="'https://github.com/'+thisPage.meta.authorGitHubUsername+'.png?size=200'">
<p class="pl-2 font-weight-bold"><%=thisPage.meta.authorFullName %></p>
<div class="d-flex flex-sm-row flex-column justify-content-between">
<div purpose="article-details" class="d-flex flex-row align-items-center">
<span><js-timestamp format="billing" :at="thisPage.meta.publishedOn"></js-timestamp></span>
<span class="px-2">|</span>
<img style="height: 28px; width: 28px; border-radius: 100%;" alt="The author's GitHub profile picture" :src="'https://github.com/'+thisPage.meta.authorGitHubUsername+'.png?size=200'">
<p class="pl-2 font-weight-bold"><%=thisPage.meta.authorFullName %></p>
</div>
<a purpose="rss-button" class="px-0 px-sm-2 pt-3 pt-sm-1" taget="_blank" :href="'/rss/'+articleCategorySlug"><span>Subscribe</span></a>
</div>
<div purpose="article-content" class="d-flex flex-column" parasails-has-no-page-script>
<%- partial(path.relative(path.dirname(__filename), path.resolve( sails.config.appPath, path.join(sails.config.builtStaticContent.compiledPagePartialsAppPath, thisPage.htmlId)))) %>