fix(content-blog): temporarily swallow feed mdxToHtml errors + feed refactor (#5753)

This commit is contained in:
Sébastien Lorber 2021-10-21 11:57:47 +02:00 committed by GitHub
parent fd41239f4f
commit 29d13351a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 234 additions and 109 deletions

View file

@ -0,0 +1,14 @@
---
title: MDX Blog Sample with require calls
date: 2021-03-06
---
Test MDX with require calls
import useBaseUrl from '@docusaurus/useBaseUrl';
<img src={useBaseUrl('/img/docusaurus.png')} />
<img src={require('../static/img/docusaurus.png').default} />
<img src={require('@site/static/img/docusaurus.png').default} />

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

View file

@ -1,18 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`blogFeed atom should not show feed without posts 1`] = `null`;
exports[`blogFeed atom shows feed item for each post 1`] = ` exports[`blogFeed atom shows feed item for each post 1`] = `
"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?> "<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>
<feed xmlns=\\"http://www.w3.org/2005/Atom\\"> <feed xmlns=\\"http://www.w3.org/2005/Atom\\">
<id>https://docusaurus.io/myBaseUrl/blog</id> <id>https://docusaurus.io/myBaseUrl/blog</id>
<title>Hello Blog</title> <title>Hello Blog</title>
<updated>2021-03-05T00:00:00.000Z</updated> <updated>2021-03-06T00:00:00.000Z</updated>
<generator>https://github.com/jpmonette/feed</generator> <generator>https://github.com/jpmonette/feed</generator>
<link rel=\\"alternate\\" href=\\"https://docusaurus.io/myBaseUrl/blog\\"/> <link rel=\\"alternate\\" href=\\"https://docusaurus.io/myBaseUrl/blog\\"/>
<subtitle>Hello Blog</subtitle> <subtitle>Hello Blog</subtitle>
<icon>https://docusaurus.io/myBaseUrl/image/favicon.ico</icon> <icon>https://docusaurus.io/myBaseUrl/image/favicon.ico</icon>
<rights>Copyright</rights> <rights>Copyright</rights>
<entry>
<title type=\\"html\\"><![CDATA[MDX Blog Sample with require calls]]></title>
<id>MDX Blog Sample with require calls</id>
<link href=\\"https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post\\"/>
<updated>2021-03-06T00:00:00.000Z</updated>
<summary type=\\"html\\"><![CDATA[Test MDX with require calls]]></summary>
</entry>
<entry> <entry>
<title type=\\"html\\"><![CDATA[Full Blog Sample]]></title> <title type=\\"html\\"><![CDATA[Full Blog Sample]]></title>
<id>Full Blog Sample</id> <id>Full Blog Sample</id>
@ -81,8 +86,6 @@ exports[`blogFeed atom shows feed item for each post 1`] = `
</feed>" </feed>"
`; `;
exports[`blogFeed rss should not show feed without posts 1`] = `null`;
exports[`blogFeed rss shows feed item for each post 1`] = ` exports[`blogFeed rss shows feed item for each post 1`] = `
"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?> "<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>
<rss version=\\"2.0\\" xmlns:dc=\\"http://purl.org/dc/elements/1.1/\\" xmlns:content=\\"http://purl.org/rss/1.0/modules/content/\\"> <rss version=\\"2.0\\" xmlns:dc=\\"http://purl.org/dc/elements/1.1/\\" xmlns:content=\\"http://purl.org/rss/1.0/modules/content/\\">
@ -90,10 +93,17 @@ exports[`blogFeed rss shows feed item for each post 1`] = `
<title>Hello Blog</title> <title>Hello Blog</title>
<link>https://docusaurus.io/myBaseUrl/blog</link> <link>https://docusaurus.io/myBaseUrl/blog</link>
<description>Hello Blog</description> <description>Hello Blog</description>
<lastBuildDate>Fri, 05 Mar 2021 00:00:00 GMT</lastBuildDate> <lastBuildDate>Sat, 06 Mar 2021 00:00:00 GMT</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs> <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<generator>https://github.com/jpmonette/feed</generator> <generator>https://github.com/jpmonette/feed</generator>
<copyright>Copyright</copyright> <copyright>Copyright</copyright>
<item>
<title><![CDATA[MDX Blog Sample with require calls]]></title>
<link>https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post</link>
<guid>MDX Blog Sample with require calls</guid>
<pubDate>Sat, 06 Mar 2021 00:00:00 GMT</pubDate>
<description><![CDATA[Test MDX with require calls]]></description>
</item>
<item> <item>
<title><![CDATA[Full Blog Sample]]></title> <title><![CDATA[Full Blog Sample]]></title>
<link>https://docusaurus.io/myBaseUrl/blog/mdx-blog-post</link> <link>https://docusaurus.io/myBaseUrl/blog/mdx-blog-post</link>

View file

@ -6,10 +6,12 @@
*/ */
import path from 'path'; import path from 'path';
import {generateBlogFeed} from '../blogUtils'; import {generateBlogFeed} from '../feed';
import {LoadContext, I18n} from '@docusaurus/types'; import {LoadContext, I18n} from '@docusaurus/types';
import {PluginOptions, BlogContentPaths} from '../types'; import {PluginOptions, BlogContentPaths} from '../types';
import {DEFAULT_OPTIONS} from '../pluginOptionSchema'; import {DEFAULT_OPTIONS} from '../pluginOptionSchema';
import {generateBlogPosts} from '../blogUtils';
import {Feed} from 'feed';
const DefaultI18N: I18n = { const DefaultI18N: I18n = {
currentLocale: 'en', currentLocale: 'en',
@ -30,6 +32,23 @@ function getBlogContentPaths(siteDir: string): BlogContentPaths {
}; };
} }
async function testGenerateFeeds(
context: LoadContext,
options: PluginOptions,
): Promise<Feed | null> {
const blogPosts = await generateBlogPosts(
getBlogContentPaths(context.siteDir),
context,
options,
);
return generateBlogFeed({
blogPosts,
options,
siteConfig: context.siteConfig,
});
}
describe('blogFeed', () => { describe('blogFeed', () => {
(['atom', 'rss'] as const).forEach((feedType) => { (['atom', 'rss'] as const).forEach((feedType) => {
describe(`${feedType}`, () => { describe(`${feedType}`, () => {
@ -42,8 +61,7 @@ describe('blogFeed', () => {
favicon: 'image/favicon.ico', favicon: 'image/favicon.ico',
}; };
const feed = await generateBlogFeed( const feed = await testGenerateFeeds(
getBlogContentPaths(siteDir),
{ {
siteDir, siteDir,
siteConfig, siteConfig,
@ -61,9 +79,8 @@ describe('blogFeed', () => {
}, },
} as PluginOptions, } as PluginOptions,
); );
const feedContent =
feed && (feedType === 'rss' ? feed.rss2() : feed.atom1()); expect(feed).toEqual(null);
expect(feedContent).toMatchSnapshot();
}); });
test('shows feed item for each post', async () => { test('shows feed item for each post', async () => {
@ -76,8 +93,7 @@ describe('blogFeed', () => {
favicon: 'image/favicon.ico', favicon: 'image/favicon.ico',
}; };
const feed = await generateBlogFeed( const feed = await testGenerateFeeds(
getBlogContentPaths(siteDir),
{ {
siteDir, siteDir,
siteConfig, siteConfig,

View file

@ -246,26 +246,29 @@ describe('loadBlog', () => {
test('simple website blog dates localized', async () => { test('simple website blog dates localized', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website'); const siteDir = path.join(__dirname, '__fixtures__', 'website');
const blogPostsFrench = await getBlogPosts(siteDir, {}, getI18n('fr')); const blogPostsFrench = await getBlogPosts(siteDir, {}, getI18n('fr'));
expect(blogPostsFrench).toHaveLength(7); expect(blogPostsFrench).toHaveLength(8);
expect(blogPostsFrench[0].metadata.formattedDate).toMatchInlineSnapshot( expect(blogPostsFrench[0].metadata.formattedDate).toMatchInlineSnapshot(
`"5 mars 2021"`, `"6 mars 2021"`,
); );
expect(blogPostsFrench[1].metadata.formattedDate).toMatchInlineSnapshot( expect(blogPostsFrench[1].metadata.formattedDate).toMatchInlineSnapshot(
`"16 août 2020"`, `"5 mars 2021"`,
); );
expect(blogPostsFrench[2].metadata.formattedDate).toMatchInlineSnapshot( expect(blogPostsFrench[2].metadata.formattedDate).toMatchInlineSnapshot(
`"15 août 2020"`, `"16 août 2020"`,
); );
expect(blogPostsFrench[3].metadata.formattedDate).toMatchInlineSnapshot( expect(blogPostsFrench[3].metadata.formattedDate).toMatchInlineSnapshot(
`"27 février 2020"`, `"15 août 2020"`,
); );
expect(blogPostsFrench[4].metadata.formattedDate).toMatchInlineSnapshot( expect(blogPostsFrench[4].metadata.formattedDate).toMatchInlineSnapshot(
`"2 janvier 2019"`, `"27 février 2020"`,
); );
expect(blogPostsFrench[5].metadata.formattedDate).toMatchInlineSnapshot( expect(blogPostsFrench[5].metadata.formattedDate).toMatchInlineSnapshot(
`"1 janvier 2019"`, `"2 janvier 2019"`,
); );
expect(blogPostsFrench[6].metadata.formattedDate).toMatchInlineSnapshot( expect(blogPostsFrench[6].metadata.formattedDate).toMatchInlineSnapshot(
`"1 janvier 2019"`,
);
expect(blogPostsFrench[7].metadata.formattedDate).toMatchInlineSnapshot(
`"14 décembre 2018"`, `"14 décembre 2018"`,
); );
}); });
@ -295,7 +298,8 @@ describe('loadBlog', () => {
expect(blogPost.metadata.editUrl).toEqual(hardcodedEditUrl); expect(blogPost.metadata.editUrl).toEqual(hardcodedEditUrl);
}); });
expect(editUrlFunction).toHaveBeenCalledTimes(7); expect(editUrlFunction).toHaveBeenCalledTimes(8);
expect(editUrlFunction).toHaveBeenCalledWith({ expect(editUrlFunction).toHaveBeenCalledWith({
blogDirPath: 'blog', blogDirPath: 'blog',
blogPath: 'date-matter.md', blogPath: 'date-matter.md',
@ -314,6 +318,12 @@ describe('loadBlog', () => {
permalink: '/blog/mdx-blog-post', permalink: '/blog/mdx-blog-post',
locale: 'en', locale: 'en',
}); });
expect(editUrlFunction).toHaveBeenCalledWith({
blogDirPath: 'blog',
blogPath: 'mdx-require-blog-post.mdx',
permalink: '/blog/mdx-require-blog-post',
locale: 'en',
});
expect(editUrlFunction).toHaveBeenCalledWith({ expect(editUrlFunction).toHaveBeenCalledWith({
blogDirPath: 'blog', blogDirPath: 'blog',
blogPath: 'complex-slug.md', blogPath: 'complex-slug.md',

View file

@ -9,7 +9,6 @@ import fs from 'fs-extra';
import chalk from 'chalk'; import chalk from 'chalk';
import path from 'path'; import path from 'path';
import readingTime from 'reading-time'; import readingTime from 'reading-time';
import {Feed, Author as FeedAuthor} from 'feed';
import {compact, keyBy, mapValues} from 'lodash'; import {compact, keyBy, mapValues} from 'lodash';
import { import {
PluginOptions, PluginOptions,
@ -17,7 +16,6 @@ import {
BlogContentPaths, BlogContentPaths,
BlogMarkdownLoaderOptions, BlogMarkdownLoaderOptions,
BlogTags, BlogTags,
Author,
} from './types'; } from './types';
import { import {
parseMarkdownFile, parseMarkdownFile,
@ -26,7 +24,6 @@ import {
getEditUrl, getEditUrl,
getFolderContainingFile, getFolderContainingFile,
posixPath, posixPath,
mdxToHtml,
replaceMarkdownLinks, replaceMarkdownLinks,
Globby, Globby,
normalizeFrontMatterTags, normalizeFrontMatterTags,
@ -104,66 +101,6 @@ function formatBlogPostDate(locale: string, date: Date): string {
} }
} }
export async function generateBlogFeed(
contentPaths: BlogContentPaths,
context: LoadContext,
options: PluginOptions,
): Promise<Feed | null> {
if (!options.feedOptions) {
throw new Error(
'Invalid options: "feedOptions" is not expected to be null.',
);
}
const {siteConfig} = context;
const blogPosts = await generateBlogPosts(contentPaths, context, options);
if (!blogPosts.length) {
return null;
}
const {feedOptions, routeBasePath} = options;
const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, 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: favicon ? normalizeUrl([siteUrl, baseUrl, favicon]) : undefined,
copyright: feedOptions.copyright,
});
function toFeedAuthor(author: Author): FeedAuthor {
// TODO ask author emails?
// RSS feed requires email to render authors
return {name: author.name, link: author.url};
}
blogPosts.forEach((post) => {
const {
id,
metadata: {title: metadataTitle, permalink, date, description, authors},
} = post;
feed.addItem({
title: metadataTitle,
id,
link: normalizeUrl([siteUrl, permalink]),
date,
description,
content: mdxToHtml(post.content),
author: authors.map(toFeedAuthor),
});
});
return feed;
}
async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) { async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) {
const result = await parseMarkdownFile(blogSourceAbsolute, { const result = await parseMarkdownFile(blogSourceAbsolute, {
removeContentTitle: true, removeContentTitle: true,

View file

@ -0,0 +1,129 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {Feed, Author as FeedAuthor} from 'feed';
import {PluginOptions, Author, BlogPost, FeedType} from './types';
import {normalizeUrl, mdxToHtml} from '@docusaurus/utils';
import {DocusaurusConfig} from '@docusaurus/types';
import path from 'path';
import fs from 'fs-extra';
// TODO this is temporary until we handle mdxToHtml better
// It's hard to convert reliably JSX/require calls to an html feed content
// See https://github.com/facebook/docusaurus/issues/5664
function mdxToFeedContent(mdxContent: string): string | undefined {
try {
return mdxToHtml(mdxContent);
} catch (e) {
// TODO will we need a plugin option to configure how to handle such an error
// Swallow the error on purpose for now, until we understand better the problem space
return undefined;
}
}
export async function generateBlogFeed({
blogPosts,
options,
siteConfig,
}: {
blogPosts: BlogPost[];
options: PluginOptions;
siteConfig: DocusaurusConfig;
}): Promise<Feed | null> {
if (!blogPosts.length) {
return null;
}
const {feedOptions, routeBasePath} = options;
const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
const updated =
(blogPosts[0] && blogPosts[0].metadata.date) ||
new Date('2015-10-25T16:29:00.000-07:00'); // weird legacy magic date
const feed = new Feed({
id: blogBaseUrl,
title: feedOptions.title || `${title} Blog`,
updated,
language: feedOptions.language,
link: blogBaseUrl,
description: feedOptions.description || `${siteConfig.title} Blog`,
favicon: favicon ? normalizeUrl([siteUrl, baseUrl, favicon]) : undefined,
copyright: feedOptions.copyright,
});
function toFeedAuthor(author: Author): FeedAuthor {
// TODO ask author emails?
// RSS feed requires email to render authors
return {name: author.name, link: author.url};
}
blogPosts.forEach((post) => {
const {
id,
metadata: {title: metadataTitle, permalink, date, description, authors},
} = post;
feed.addItem({
title: metadataTitle,
id,
link: normalizeUrl([siteUrl, permalink]),
date,
description,
content: mdxToFeedContent(post.content),
author: authors.map(toFeedAuthor),
});
});
return feed;
}
async function createBlogFeedFile({
feed,
feedType,
filePath,
}: {
feed: Feed;
feedType: FeedType;
filePath: string;
}) {
const feedContent = feedType === 'rss' ? feed.rss2() : feed.atom1();
try {
await fs.outputFile(filePath, feedContent);
} catch (err) {
throw new Error(`Generating ${feedType} feed failed: ${err}.`);
}
}
export async function createBlogFeedFiles({
blogPosts,
options,
siteConfig,
outDir,
}: {
blogPosts: BlogPost[];
options: PluginOptions;
siteConfig: DocusaurusConfig;
outDir: string;
}): Promise<void> {
const feed = await generateBlogFeed({blogPosts, options, siteConfig});
const feedTypes = options.feedOptions.type;
if (!feed || !feedTypes) {
return;
}
await Promise.all(
feedTypes.map(async function (feedType) {
await createBlogFeedFile({
feed,
feedType,
filePath: path.join(outDir, options.routeBasePath, `${feedType}.xml`),
});
}),
);
}

View file

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import admonitions from 'remark-admonitions'; import admonitions from 'remark-admonitions';
import { import {
@ -49,13 +48,13 @@ import {
} from '@docusaurus/types'; } from '@docusaurus/types';
import {Configuration} from 'webpack'; import {Configuration} from 'webpack';
import { import {
generateBlogFeed,
generateBlogPosts, generateBlogPosts,
getContentPathList, getContentPathList,
getSourceToPermalink, getSourceToPermalink,
getBlogTags, getBlogTags,
} from './blogUtils'; } from './blogUtils';
import {BlogPostFrontMatter} from './blogFrontMatter'; import {BlogPostFrontMatter} from './blogFrontMatter';
import {createBlogFeedFiles} from './feed';
export default function pluginContentBlog( export default function pluginContentBlog(
context: LoadContext, context: LoadContext,
@ -69,10 +68,11 @@ export default function pluginContentBlog(
const { const {
siteDir, siteDir,
siteConfig: {onBrokenMarkdownLinks, baseUrl}, siteConfig,
generatedFilesDir, generatedFilesDir,
i18n: {currentLocale}, i18n: {currentLocale},
} = context; } = context;
const {onBrokenMarkdownLinks, baseUrl} = siteConfig;
const contentPaths: BlogContentPaths = { const contentPaths: BlogContentPaths = {
contentPath: path.resolve(siteDir, options.path), contentPath: path.resolve(siteDir, options.path),
@ -519,29 +519,18 @@ export default function pluginContentBlog(
return; return;
} }
const feed = await generateBlogFeed(contentPaths, context, options); // TODO: we shouldn't need to re-read the posts here!
// postBuild should receive loadedContent
if (!feed) { const blogPosts = await generateBlogPosts(contentPaths, context, options);
if (blogPosts.length) {
return; return;
} }
await createBlogFeedFiles({
const feedTypes = options.feedOptions.type; blogPosts,
options,
await Promise.all( outDir,
feedTypes.map(async (feedType) => { siteConfig,
const feedPath = path.join( });
outDir,
options.routeBasePath,
`${feedType}.xml`,
);
const feedContent = feedType === 'rss' ? feed.rss2() : feed.atom1();
try {
await fs.outputFile(feedPath, feedContent);
} catch (err) {
throw new Error(`Generating ${feedType} feed failed: ${err}.`);
}
}),
);
}, },
injectHtmlTags({content}) { injectHtmlTags({content}) {

View file

@ -0,0 +1,20 @@
---
title: Blog post MDX require Feed tests
authors:
- slorber
tags: [blog, docusaurus, long-long, long-long-long, long-long-long-long]
---
Some MDX tests, mostly to test how the RSS feed render those
<!-- truncate -->
Test MDX with require calls
import useBaseUrl from '@docusaurus/useBaseUrl';
<img src={useBaseUrl('/img/docusaurus.png')} />
<img src={require('../../static/img/docusaurus.png').default} />
<img src={require('@site/static/img/docusaurus.png').default} />