feat(v2): Implement plugin creating feed for blog posts (#1916)

* feat(v2): Implement feed for blog posts

Fixes: #1698

Test plan:
- added tests

Ran `yarn build` on website with the following config (and disabled blog
from preset-classic):

```js
[
'@docusaurus/plugin-content-blog',
  {
    path: '../website-1.x/blog',
    feedOptions: {
      copyright: 'Copy',
      type: 'atom',
    },
  },
],
```
which genereted the following feed:
```xml
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://v2.docusaurus.io/blog</id>
    <title>Docusaurus Blog</title>
    <updated>2018-12-14T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://v2.docusaurus.io/blog"/>
    <subtitle>Docusaurus Blog</subtitle>
    <icon>https://v2.docusaurus.io/img/docusaurus.ico</icon>
    <rights>Copy</rights>
    <entry>
        <title type="html"><![CDATA[Happy 1st Birthday Slash!]]></title>
        <id>Happy 1st Birthday Slash!</id>
        <link href="https://v2.docusaurus.io/blog/2018/12/14/Happy-First-Birthday-Slash"/>
        <updated>2018-12-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[![First Birthday Slash](/img/docusaurus-slash-first-birthday.svg)]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Towards Docusaurus 2]]></title>
        <id>Towards Docusaurus 2</id>
        <link href="https://v2.docusaurus.io/blog/2018/09/11/Towards-Docusaurus-2"/>
        <updated>2018-09-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Docusaurus was [officially announced](https://docusaurus.io/blog/2017/12/14/introducing-docusaurus) over nine months ago as a way to easily build open source documentation websites. Since then, it has amassed over 8,600 GitHub Stars, and is used by many popular open source projects such as [React Native](https://facebook.github.io/react-native/), [Babel](https://babeljs.io/), [Jest](https://jestjs.io/), [Reason](https://reasonml.github.io/) and [Prettier](https://prettier.io/).]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[How I Converted Profilo to Docusaurus in Under 2 Hours]]></title>
        <id>How I Converted Profilo to Docusaurus in Under 2 Hours</id>
        <link href="https://v2.docusaurus.io/blog/2018/04/30/How-I-Converted-Profilo-To-Docusaurus"/>
        <updated>2018-04-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[> _“Joel and I were discussing having a website and how it would have been great to launch with it. So I challenged myself to add Docusaurus support. It took just over an hour and a half. I'm going to send you a PR with the addition so you can take a look and see if you like it. Your workflow for adding docs wouldn't be much different from editing those markdown files.”_]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Introducing Docusaurus]]></title>
        <id>Introducing Docusaurus</id>
        <link href="https://v2.docusaurus.io/blog/2017/12/14/introducing-docusaurus"/>
        <updated>2017-12-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[![Introducing Slash](/img/slash-introducing.svg)]]></summary>
    </entry>
</feed>
```

* new feedOptions type 'all' and use correct path
This commit is contained in:
Pawel Kadluczka 2019-11-05 23:45:31 -08:00 committed by Endi
parent c507028cb0
commit ff83e6f8bc
12 changed files with 405 additions and 84 deletions

View file

@ -0,0 +1,147 @@
import fs from 'fs-extra';
import globby from 'globby';
import path from 'path';
import {Feed} from 'feed';
import {PluginOptions, BlogPost, DateLink} from './types';
import {parse, normalizeUrl} from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types';
export function truncate(fileString: string, truncateMarker: RegExp | string) {
const truncated =
typeof truncateMarker === 'string'
? fileString.includes(truncateMarker)
: truncateMarker.test(fileString);
return truncated ? fileString.split(truncateMarker)[0] : fileString;
}
// YYYY-MM-DD-{name}.mdx?
// prefer named capture, but old node version do not support
const FILENAME_PATTERN = /^(\d{4}-\d{1,2}-\d{1,2})-?(.*?).mdx?$/;
function toUrl({date, link}: DateLink) {
return `${date
.toISOString()
.substring(0, '2019-01-01'.length)
.replace(/-/g, '/')}/${link}`;
}
export async function generateBlogFeed(
context: LoadContext,
options: PluginOptions,
) {
if (!options.feedOptions) {
throw new Error(
'Invalid options - `feedOptions` is not expected to be null.',
);
}
const {siteDir, siteConfig} = context;
const contentPath = path.resolve(siteDir, options.path);
const blogPosts = await generateBlogPosts(contentPath, context, options);
if (blogPosts == null) {
return null;
}
const {feedOptions, routeBasePath} = options;
const {url: siteUrl, title, favicon} = siteConfig;
const blogBaseUrl = normalizeUrl([siteUrl, routeBasePath]);
const updated =
(blogPosts[0] && blogPosts[0].metadata.date) ||
new Date('2015-10-25T16:29:00.000-07:00');
const feed = new Feed({
id: blogBaseUrl,
title: feedOptions.title || `${title} Blog`,
updated,
language: feedOptions.language,
link: blogBaseUrl,
description: feedOptions.description || `${siteConfig.title} Blog`,
favicon: normalizeUrl([siteUrl, favicon]),
copyright: feedOptions.copyright,
});
blogPosts.forEach(post => {
const {
id,
metadata: {title, permalink, date, description},
} = post;
feed.addItem({
title,
id: id,
link: normalizeUrl([siteUrl, permalink]),
date,
description,
});
});
return feed;
}
export async function generateBlogPosts(
blogDir: string,
{siteConfig, siteDir}: LoadContext,
options: PluginOptions,
) {
const {include, routeBasePath} = options;
if (!fs.existsSync(blogDir)) {
return null;
}
const {baseUrl = ''} = siteConfig;
const blogFiles = await globby(include, {
cwd: blogDir,
});
const blogPosts: BlogPost[] = [];
await Promise.all(
blogFiles.map(async (relativeSource: string) => {
// Cannot use path.join() as it resolves '../' and removes the '@site'. Let webpack loader resolve it.
const source = path.join(blogDir, relativeSource);
const aliasedSource = `@site/${path.relative(siteDir, source)}`;
const blogFileName = path.basename(relativeSource);
const fileString = await fs.readFile(source, 'utf-8');
const {frontMatter, excerpt} = parse(fileString);
let date;
// extract date and title from filename
const match = blogFileName.match(FILENAME_PATTERN);
let linkName = blogFileName.replace(/\.mdx?$/, '');
if (match) {
const [, dateString, name] = match;
date = new Date(dateString);
linkName = name;
}
// prefer usedefined date
if (frontMatter.date) {
date = new Date(frontMatter.date);
}
// use file create time for blog
date = date || (await fs.stat(source)).birthtime;
frontMatter.title = frontMatter.title || linkName;
blogPosts.push({
id: frontMatter.id || frontMatter.title,
metadata: {
permalink: normalizeUrl([
baseUrl,
routeBasePath,
frontMatter.id || toUrl({date, link: linkName}),
]),
source: aliasedSource,
description: frontMatter.description || excerpt,
date,
tags: frontMatter.tags,
title: frontMatter.title,
},
});
}),
);
blogPosts.sort(
(a, b) => b.metadata.date.getTime() - a.metadata.date.getTime(),
);
return blogPosts;
}