mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-18 03:26:57 +02:00
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"><]]></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">< 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"><]]></summary> </entry> </feed> ``` * new feedOptions type 'all' and use correct path
This commit is contained in:
parent
c507028cb0
commit
ff83e6f8bc
12 changed files with 405 additions and 84 deletions
147
packages/docusaurus-plugin-content-blog/src/blogUtils.ts
Normal file
147
packages/docusaurus-plugin-content-blog/src/blogUtils.ts
Normal 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue