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