feat(v2): enhance @docusaurus/plugin-content-blog (#1311)

* feat(v2): @docusaurus/plugin-content-blog

* address code review
This commit is contained in:
Endilie Yacop Sucipto 2019-03-26 01:39:58 +07:00 committed by GitHub
parent bd0cdbc701
commit 4bd5d3937e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 156 additions and 55 deletions

View file

@ -6,11 +6,10 @@
*/ */
import React, {useContext} from 'react'; import React, {useContext} from 'react';
import Link from '@docusaurus/Link';
import Head from '@docusaurus/Head'; import Head from '@docusaurus/Head';
import Layout from '@theme/Layout'; // eslint-disable-line import Layout from '@theme/Layout'; // eslint-disable-line
import DocusaurusContext from '@docusaurus/context'; import DocusaurusContext from '@docusaurus/context';
import Post from '../Post';
function BlogPage(props) { function BlogPage(props) {
const context = useContext(DocusaurusContext); const context = useContext(DocusaurusContext);
@ -30,15 +29,13 @@ function BlogPage(props) {
{language && <meta name="docsearch:language" content={language} />} {language && <meta name="docsearch:language" content={language} />}
</Head> </Head>
<div> <div>
<ul> {BlogPosts.map((PostContent, index) => (
{posts.map(metadata => ( <Post
<li key={metadata.permalink}> key={index}
<Link to={metadata.permalink}>{metadata.permalink}</Link> truncated={posts[index].truncatedSource}
</li> metadata={posts[index]}>
))} <PostContent />
</ul> </Post>
{BlogPosts.map((BlogPost, index) => (
<BlogPost key={index} />
))} ))}
</div> </div>
</Layout> </Layout>

View file

@ -0,0 +1,43 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {useContext} from 'react';
import Head from '@docusaurus/Head';
import Layout from '@theme/Layout'; // eslint-disable-line
import DocusaurusContext from '@docusaurus/context';
import Post from '../Post';
import styles from './styles.module.css';
function BlogPost(props) {
const {metadata: contextMetadata = {}, siteConfig = {}} = useContext(
DocusaurusContext,
);
const {baseUrl, favicon} = siteConfig;
const {language, title} = contextMetadata;
const {modules, metadata} = props;
const BlogPostContents = modules[0];
return (
<Layout>
<Head defaultTitle={siteConfig.title}>
{title && <title>{title}</title>}
{favicon && <link rel="shortcut icon" href={baseUrl + favicon} />}
{language && <html lang={language} />}
</Head>
{BlogPostContents && (
<div className={styles.blogPostContainer}>
<Post metadata={metadata}>
<BlogPostContents />
</Post>
</div>
)}
</Layout>
);
}
export default BlogPost;

View file

@ -0,0 +1,15 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.blogPostContainer {
border-bottom: 1px solid #e0e0e0;
border-radius: 3px;
padding-bottom: 20px;
margin: 50px auto;
text-align: left;
max-width: 1100px;
}

View file

@ -7,21 +7,16 @@
import React from 'react'; import React from 'react';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import Head from '@docusaurus/Head'; import classnames from 'classnames'; // eslint-disable-line
import classnames from 'classnames';
import Layout from '@theme/Layout'; // eslint-disable-line
import DocusaurusContext from '@docusaurus/context';
import styles from './styles.module.css'; import styles from './styles.module.css';
class BlogPost extends React.Component { function Post(props) {
renderPostHeader() { const {metadata, children, truncated} = props;
const {metadata} = this.props; const renderPostHeader = () => {
if (!metadata) { if (!metadata) {
return null; return null;
} }
const { const {
date, date,
author, author,
@ -82,29 +77,21 @@ class BlogPost extends React.Component {
</div> </div>
</header> </header>
); );
} };
render() {
const {metadata = {}, siteConfig = {}} = this.context;
const {baseUrl, favicon} = siteConfig;
const {language, title} = metadata;
const {modules} = this.props;
const BlogPostContents = modules[0];
return ( return (
<Layout> <div className={styles.postContainer}>
<Head defaultTitle={siteConfig.title}> {renderPostHeader()}
{title && <title>{title}</title>} {children}
{favicon && <link rel="shortcut icon" href={baseUrl + favicon} />} {truncated && (
{language && <html lang={language} />} <div className={styles.readMoreContainer}>
</Head> <Link className={styles.readMoreLink} to={metadata.permalink}>
{this.renderPostHeader()} Read More
<BlogPostContents /> </Link>
</Layout> </div>
)}
</div>
); );
} }
}
BlogPost.contextType = DocusaurusContext; export default Post;
export default BlogPost;

View file

@ -45,3 +45,38 @@
height: 50px; height: 50px;
width: 50px; width: 50px;
} }
.readMoreContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.readMoreLink {
border: 1px solid #2e8555;
border-radius: 3px;
color: #2e8555;
display: inline-block;
font-size: 14px;
font-weight: 400;
line-height: 1.2em;
padding: 10px;
text-decoration: none!important;
text-transform: uppercase;
transition: background .3s,color .3s;
}
.readMoreLink:hover {
background-color: #00a388;
color: #fff;
}
.postContainer {
border-bottom: 1px solid #e0e0e0;
border-radius: 3px;
padding-bottom: 20px;
margin: 50px auto;
text-align: left;
width: 600px;
max-width: 1100px;
}

View file

@ -8,7 +8,7 @@
const globby = require('globby'); const globby = require('globby');
const path = require('path'); const path = require('path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const {parse, idx, normalizeUrl} = require('@docusaurus/utils'); const {parse, idx, normalizeUrl, generate} = require('@docusaurus/utils');
function fileToUrl(fileName) { function fileToUrl(fileName) {
return fileName return fileName
@ -25,10 +25,12 @@ const DEFAULT_OPTIONS = {
routeBasePath: 'blog', // URL Route. routeBasePath: 'blog', // URL Route.
include: ['*.md, *.mdx'], // Extensions to include. include: ['*.md, *.mdx'], // Extensions to include.
pageCount: 10, // How many entries per page. pageCount: 10, // How many entries per page.
blogPageComponent: '@theme/BlogPage', blogPageComponent: path.resolve(__dirname, './components/BlogPage'),
blogPostComponent: '@theme/BlogPost', blogPostComponent: path.resolve(__dirname, './components/BlogPost'),
}; };
const TRUNCATE_MARKER = /<!--\s*truncate\s*-->/;
class DocusaurusPluginContentBlog { class DocusaurusPluginContentBlog {
constructor(opts, context) { constructor(opts, context) {
this.options = {...DEFAULT_OPTIONS, ...opts}; this.options = {...DEFAULT_OPTIONS, ...opts};
@ -43,7 +45,7 @@ class DocusaurusPluginContentBlog {
// Fetches blog contents and returns metadata for the contents. // Fetches blog contents and returns metadata for the contents.
async loadContent() { async loadContent() {
const {pageCount, include, routeBasePath} = this.options; const {pageCount, include, routeBasePath} = this.options;
const {env, siteConfig} = this.context; const {env, generatedFilesDir, siteConfig} = this.context;
const blogDir = this.contentPath; const blogDir = this.contentPath;
const {baseUrl} = siteConfig; const {baseUrl} = siteConfig;
@ -71,7 +73,20 @@ class DocusaurusPluginContentBlog {
); );
const fileString = await fs.readFile(source, 'utf-8'); const fileString = await fs.readFile(source, 'utf-8');
const {metadata: rawMetadata} = parse(fileString); const {metadata: rawMetadata, content} = parse(fileString);
let truncatedSource;
const isTruncated = TRUNCATE_MARKER.test(content);
if (isTruncated) {
const pluginContentDir = path.join(generatedFilesDir, this.getName());
await generate(
pluginContentDir,
blogFileName,
content.split(TRUNCATE_MARKER)[0],
);
truncatedSource = path.join(pluginContentDir, blogFileName);
}
const metadata = { const metadata = {
permalink: normalizeUrl([ permalink: normalizeUrl([
baseUrl, baseUrl,
@ -79,6 +94,7 @@ class DocusaurusPluginContentBlog {
fileToUrl(blogFileName), fileToUrl(blogFileName),
]), ]),
source, source,
truncatedSource,
...rawMetadata, ...rawMetadata,
date, date,
language: defaultLangTag, language: defaultLangTag,
@ -119,7 +135,9 @@ class DocusaurusPluginContentBlog {
path: permalink, path: permalink,
component: blogPageComponent, component: blogPageComponent,
metadata: metadataItem, metadata: metadataItem,
modules: metadataItem.posts.map(post => post.source), modules: metadataItem.posts.map(
post => post.truncatedSource || post.source,
),
}); });
return; return;
} }

View file

@ -3,10 +3,15 @@
"version": "1.0.0", "version": "1.0.0",
"description": "Blog content plugin for Docusaurus", "description": "Blog content plugin for Docusaurus",
"main": "index.js", "main": "index.js",
"files": [
"components"
],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@docusaurus/utils": "^1.0.0", "@docusaurus/utils": "^1.0.0",
"classnames": "^2.2.6",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"globby": "^9.1.0" "globby": "^9.1.0",
"react": "^16.8.5"
} }
} }

View file

@ -14,6 +14,7 @@ const genCache = new Map();
async function generate(generatedFilesDir, file, content) { async function generate(generatedFilesDir, file, content) {
const cached = genCache.get(file); const cached = genCache.get(file);
if (cached !== content) { if (cached !== content) {
await fs.ensureDir(generatedFilesDir);
await fs.writeFile(path.join(generatedFilesDir, file), content); await fs.writeFile(path.join(generatedFilesDir, file), content);
genCache.set(file, content); genCache.set(file, content);
} }

View file

@ -73,7 +73,7 @@ module.exports = async function load(siteDir) {
// Process plugins. // Process plugins.
const pluginConfigs = siteConfig.plugins || []; const pluginConfigs = siteConfig.plugins || [];
const context = {env, siteDir, siteConfig}; const context = {env, siteDir, generatedFilesDir, siteConfig};
const {plugins, pluginRouteConfigs} = await loadPlugins({ const {plugins, pluginRouteConfigs} = await loadPlugins({
pluginConfigs, pluginConfigs,
context, context,

View file

@ -16,8 +16,6 @@ module.exports = function loadConfig(siteDir) {
const themeComponents = [ const themeComponents = [
'Doc', 'Doc',
'BlogPost',
'BlogPage',
'Pages', 'Pages',
'Loading', 'Loading',
'NotFound', 'NotFound',
@ -26,7 +24,9 @@ module.exports = function loadConfig(siteDir) {
]; ];
themeComponents.forEach(component => { themeComponents.forEach(component => {
if (!require.resolve(path.join(themePath, component))) { try {
require.resolve(path.join(themePath, component));
} catch (e) {
throw new Error( throw new Error(
`Failed to load ${themePath}/${component}. It does not exist.`, `Failed to load ${themePath}/${component}. It does not exist.`,
); );

View file

@ -10736,7 +10736,7 @@ react-youtube@^7.9.0:
prop-types "^15.5.3" prop-types "^15.5.3"
youtube-player "^5.5.1" youtube-player "^5.5.1"
react@^16.5.0, react@^16.8.4: react@^16.5.0, react@^16.8.4, react@^16.8.5:
version "16.8.5" version "16.8.5"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.5.tgz#49be3b655489d74504ad994016407e8a0445de66" resolved "https://registry.yarnpkg.com/react/-/react-16.8.5.tgz#49be3b655489d74504ad994016407e8a0445de66"
integrity sha512-daCb9TD6FZGvJ3sg8da1tRAtIuw29PbKZW++NN4wqkbEvxL+bZpaaYb4xuftW/SpXmgacf1skXl/ddX6CdOlDw== integrity sha512-daCb9TD6FZGvJ3sg8da1tRAtIuw29PbKZW++NN4wqkbEvxL+bZpaaYb4xuftW/SpXmgacf1skXl/ddX6CdOlDw==