diff --git a/.gitignore b/.gitignore
index b6e79ca14..035eaac97 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,3 +53,9 @@ static-site/.next
# TypeScript
static-site/tsconfig.tsbuildinfo
+
+# Blog feeds
+static-site/public/blog/atom.xml
+static-site/public/blog/feed.json
+static-site/public/blog/rss2.xml
+
diff --git a/build.sh b/build.sh
index f77275c94..b3390d2c5 100755
--- a/build.sh
+++ b/build.sh
@@ -33,6 +33,7 @@ main() {
build-static() {
echo "Building the static Next.JS app (pages defined in static-site/pages, assets written to static-site/.next)"
+ npm run build:feeds
./node_modules/.bin/next build static-site
}
diff --git a/package.json b/package.json
index cab0d4cdf..ea49e67af 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
},
"scripts": {
"build": "./build.sh",
+ "build:feeds": "node scripts/generate-blog-feeds.js",
"lint": "npm run lint:server",
"lint:server": "DEBUG=eslint:cli-engine npx eslint --ext .js,.jsx .",
"lint:static-site": "cd static-site && DEBUG=eslint:cli-engine npx eslint --ext .js,.jsx,.ts,.tsx .",
diff --git a/scripts/generate-blog-feeds.js b/scripts/generate-blog-feeds.js
new file mode 100644
index 000000000..45fd76d65
--- /dev/null
+++ b/scripts/generate-blog-feeds.js
@@ -0,0 +1,3 @@
+import { generateBlogFeeds } from "../static-site/src/util/blogPosts.js";
+
+await generateBlogFeeds();
diff --git a/static-site/src/components/layout.jsx b/static-site/src/components/layout.jsx
index 5dad03d16..bfd71b45b 100644
--- a/static-site/src/components/layout.jsx
+++ b/static-site/src/components/layout.jsx
@@ -1,7 +1,7 @@
import React from "react";
import Helmet from "react-helmet";
import styled, {ThemeProvider} from "styled-components";
-import { siteTitle, siteDescription } from "../../data/SiteConfig";
+import { siteTitle, siteDescription, siteUrl } from "../../data/SiteConfig";
import {theme} from '../layouts/theme';
/**
@@ -23,6 +23,9 @@ export default class MainLayout extends React.Component {
{`${siteTitle}`}
+
+
+
diff --git a/static-site/src/util/blogPosts.js b/static-site/src/util/blogPosts.js
index 603b91490..d778e0a7b 100644
--- a/static-site/src/util/blogPosts.js
+++ b/static-site/src/util/blogPosts.js
@@ -2,17 +2,21 @@ import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import matter from 'gray-matter';
+import { Feed } from 'feed';
+import { parseMarkdown } from "./parseMarkdown.js";
import lodash from 'lodash';
const { startCase } = lodash;
+const NUMBER_OF_POSTS_IN_FEED = 10;
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
/**
* Scans the ./static-site/content/blog directory for .md files
* and returns a chronologically sorted array of posts, each with
* some basic metadata and the raw (unsanitized) markdown contents.
*/
export function getBlogPosts() {
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
const postsDirectory = path.join(__dirname, "..", "..", "content", "blog")
const markdownFiles = fs.readdirSync(postsDirectory)
.filter((fileName) => fileName.endsWith(".md"));
@@ -38,6 +42,76 @@ export function getBlogPosts() {
return blogPosts;
}
+/**
+ * Writes RSS2, Atom, and JSON feeds into the `public/blog` directory.
+ */
+export async function generateBlogFeeds() {
+ const feedDir = `${__dirname}/../../public/blog`;
+ const siteURL = "https://nextstrain.org/blog";
+
+ const feed = new Feed({
+ title: "Nextstrain.org Blog",
+ description: "FIXME",
+ id: siteURL,
+ link: siteURL,
+ image: "https://nextstrain.org/_next/static/media/nextstrain-logo-small.dcf6bdd2.png",
+ favicon: "https://nextstrain.org/favicon.ico",
+ copyright: "Copyright Trevor Bedford and Richard Neher.",
+ updated: new Date(),
+ feedLinks: {
+ atom: `${siteURL}/atom.xml`,
+ json: `${siteURL}/feed.json`,
+ rss2: `${siteURL}/rss2.xml`,
+ },
+ author: {
+ name: "The Nextstrain Team",
+ email: "hello@nextstrain.org",
+ link: "https://nextstrain.org",
+ },
+ });
+
+ const posts = getBlogPosts().slice(0, NUMBER_OF_POSTS_IN_FEED);
+
+ for (const post of posts) {
+ if (post) { // getBlogPosts() _might_ return `false` list members
+ try {
+ const content = parseMarkdown(post.mdstring);
+ const url = `${siteURL}/${post.blogUrlName}`;
+
+ feed.addItem({
+ author: [ { name: post.author } ],
+ content: content,
+ date: new Date(post.date),
+ id: url,
+ link: url,
+ title: post.title,
+ });
+ } catch (error) {
+ console.error(`Skipping post entitled "${post.title}" due to Markdown parsing error:\n${error}`);
+ }
+ }
+ }
+
+ const feeds = [
+ {
+ file: `${feedDir}/atom.xml`,
+ data: feed.atom1,
+ },
+ {
+ file: `${feedDir}/feed.json`,
+ data: feed.json1,
+ },
+ {
+ file: `${feedDir}/rss2.xml`,
+ data: feed.rss2,
+ },
+ ];
+
+ for (const feed of feeds) {
+ fs.writeFileSync(feed.file, feed.data());
+ }
+}
+
/**
* strip out the YYYY-MM-DD- leading string from blog-post filenames and return
* the rest of the filename converted to start case (first letter of each word