feat: blog posts support /YYYY/MM/DD/blog-post/index.md pattern + blog frontmatter can reference relative images (#5309)

* POC of blog post folder

* add parseBlogFileName with tests + refactor and extract processBlogSourceFile in separate method

* improve blog date pattern doc + link from content plugin guides to API ref docs

* Some FrontMatter fields should be able to reference relative image assets, converted to Webpack require calls and exposed as frontMatterAssets

* remove log
This commit is contained in:
Sébastien Lorber 2021-08-06 17:51:59 +02:00 committed by GitHub
parent 34e9080232
commit cabb768473
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 417 additions and 166 deletions

View file

@ -19,6 +19,12 @@ const toc = require('./remark/toc');
const unwrapMdxCodeBlocks = require('./remark/unwrapMdxCodeBlocks'); const unwrapMdxCodeBlocks = require('./remark/unwrapMdxCodeBlocks');
const transformImage = require('./remark/transformImage'); const transformImage = require('./remark/transformImage');
const transformLinks = require('./remark/transformLinks'); const transformLinks = require('./remark/transformLinks');
const {escapePath} = require('@docusaurus/utils');
const {getFileLoaderUtils} = require('@docusaurus/core/lib/webpack/utils');
const {
loaders: {inlineMarkdownImageFileLoader},
} = getFileLoaderUtils();
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
rehypePlugins: [], rehypePlugins: [],
@ -38,9 +44,49 @@ async function readMetadataPath(metadataPath) {
} }
} }
// For some specific FrontMatter fields, we want to allow referencing local relative assets so that they enter the Webpack asset pipeline
// We don't do that for all frontMatters, only for the configured keys
// {image: "./myImage.png"} => {image: require("./myImage.png")}
function createFrontMatterAssetsExportCode(
filePath,
frontMatter,
frontMatterAssetKeys = [],
) {
if (frontMatterAssetKeys.length === 0) {
return 'undefined';
}
function createFrontMatterAssetRequireCode(value) {
// Only process string values starting with ./
// We could enhance this logic and check if file exists on disc?
if (typeof value === 'string' && value.startsWith('./')) {
// TODO do we have other use-cases than image assets?
// Probably not worth adding more support, as we want to move to Webpack 5 new asset system (https://github.com/facebook/docusaurus/pull/4708)
const inlineLoader = inlineMarkdownImageFileLoader;
return `require("${inlineLoader}${escapePath(value)}").default`;
}
return undefined;
}
const frontMatterAssetEntries = Object.entries(frontMatter).filter(([key]) =>
frontMatterAssetKeys.includes(key),
);
const lines = frontMatterAssetEntries
.map(([key, value]) => {
const assetRequireCode = createFrontMatterAssetRequireCode(value);
return assetRequireCode ? `"${key}": ${assetRequireCode},` : undefined;
})
.filter(Boolean);
const exportValue = `{\n${lines.join('\n')}\n}`;
return exportValue;
}
module.exports = async function docusaurusMdxLoader(fileString) { module.exports = async function docusaurusMdxLoader(fileString) {
const callback = this.async(); const callback = this.async();
const filePath = this.resourcePath;
const reqOptions = this.getOptions() || {}; const reqOptions = this.getOptions() || {};
const {frontMatter, content: contentWithTitle} = parseFrontMatter(fileString); const {frontMatter, content: contentWithTitle} = parseFrontMatter(fileString);
@ -51,8 +97,6 @@ module.exports = async function docusaurusMdxLoader(fileString) {
const hasFrontMatter = Object.keys(frontMatter).length > 0; const hasFrontMatter = Object.keys(frontMatter).length > 0;
const filePath = this.resourcePath;
const options = { const options = {
...reqOptions, ...reqOptions,
remarkPlugins: [ remarkPlugins: [
@ -80,6 +124,11 @@ module.exports = async function docusaurusMdxLoader(fileString) {
let exportStr = ``; let exportStr = ``;
exportStr += `\nexport const frontMatter = ${stringifyObject(frontMatter)};`; exportStr += `\nexport const frontMatter = ${stringifyObject(frontMatter)};`;
exportStr += `\nexport const frontMatterAssets = ${createFrontMatterAssetsExportCode(
filePath,
frontMatter,
reqOptions.frontMatterAssetKeys,
)};`;
exportStr += `\nexport const contentTitle = ${stringifyObject( exportStr += `\nexport const contentTitle = ${stringifyObject(
contentTitle, contentTitle,
)};`; )};`;

View file

@ -42,6 +42,12 @@ declare module '@theme/BlogPostPage' {
readonly hide_table_of_contents?: boolean; readonly hide_table_of_contents?: boolean;
}; };
export type FrontMatterAssets = {
readonly image?: string;
readonly author_image_url?: string;
readonly authorImageURL?: string;
};
export type Metadata = { export type Metadata = {
readonly title: string; readonly title: string;
readonly date: string; readonly date: string;
@ -61,6 +67,7 @@ declare module '@theme/BlogPostPage' {
export type Content = { export type Content = {
readonly frontMatter: FrontMatter; readonly frontMatter: FrontMatter;
readonly frontMatterAssets: FrontMatterAssets;
readonly metadata: Metadata; readonly metadata: Metadata;
readonly toc: readonly TOCItem[]; readonly toc: readonly TOCItem[];
(): JSX.Element; (): JSX.Element;

View file

@ -0,0 +1,94 @@
/**
* 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 {parseBlogFileName} from '../blogUtils';
describe('parseBlogFileName', () => {
test('parse file', () => {
expect(parseBlogFileName('some-post.md')).toEqual({
date: undefined,
text: 'some-post',
slug: '/some-post',
});
});
test('parse folder', () => {
expect(parseBlogFileName('some-post/index.md')).toEqual({
date: undefined,
text: 'some-post',
slug: '/some-post',
});
});
test('parse nested file', () => {
expect(parseBlogFileName('some-post/some-file.md')).toEqual({
date: undefined,
text: 'some-post/some-file',
slug: '/some-post/some-file',
});
});
test('parse nested folder', () => {
expect(parseBlogFileName('some-post/some-subfolder/index.md')).toEqual({
date: undefined,
text: 'some-post/some-subfolder',
slug: '/some-post/some-subfolder',
});
});
test('parse file respecting date convention', () => {
expect(
parseBlogFileName('2021-05-12-announcing-docusaurus-two-beta.md'),
).toEqual({
date: new Date('2021-05-12Z'),
text: 'announcing-docusaurus-two-beta',
slug: '/2021/05/12/announcing-docusaurus-two-beta',
});
});
test('parse folder name respecting date convention', () => {
expect(
parseBlogFileName('2021-05-12-announcing-docusaurus-two-beta/index.md'),
).toEqual({
date: new Date('2021-05-12Z'),
text: 'announcing-docusaurus-two-beta',
slug: '/2021/05/12/announcing-docusaurus-two-beta',
});
});
test('parse folder tree respecting date convention', () => {
expect(
parseBlogFileName('2021/05/12/announcing-docusaurus-two-beta/index.md'),
).toEqual({
date: new Date('2021-05-12Z'),
text: 'announcing-docusaurus-two-beta',
slug: '/2021/05/12/announcing-docusaurus-two-beta',
});
});
test('parse folder name/tree (mixed) respecting date convention', () => {
expect(
parseBlogFileName('2021/05-12-announcing-docusaurus-two-beta/index.md'),
).toEqual({
date: new Date('2021-05-12Z'),
text: 'announcing-docusaurus-two-beta',
slug: '/2021/05/12/announcing-docusaurus-two-beta',
});
});
test('parse nested folder tree respecting date convention', () => {
expect(
parseBlogFileName(
'2021/05/12/announcing-docusaurus-two-beta/subfolder/subfile.md',
),
).toEqual({
date: new Date('2021-05-12Z'),
text: 'announcing-docusaurus-two-beta/subfolder/subfile',
slug: '/2021/05/12/announcing-docusaurus-two-beta/subfolder/subfile',
});
});
});

View file

@ -21,7 +21,7 @@ export type BlogPostFrontMatter = {
tags?: (string | Tag)[]; tags?: (string | Tag)[];
slug?: string; slug?: string;
draft?: boolean; draft?: boolean;
date?: Date; date?: Date | string; // Yaml automagically convert some string patterns as Date, but not all
author?: string; author?: string;
author_title?: string; author_title?: string;

View file

@ -10,11 +10,10 @@ import chalk from 'chalk';
import path from 'path'; import path from 'path';
import readingTime from 'reading-time'; import readingTime from 'reading-time';
import {Feed} from 'feed'; import {Feed} from 'feed';
import {keyBy, mapValues} from 'lodash'; import {compact, keyBy, mapValues} from 'lodash';
import { import {
PluginOptions, PluginOptions,
BlogPost, BlogPost,
DateLink,
BlogContentPaths, BlogContentPaths,
BlogMarkdownLoaderOptions, BlogMarkdownLoaderOptions,
} from './types'; } from './types';
@ -44,15 +43,40 @@ export function getSourceToPermalink(
); );
} }
// YYYY-MM-DD-{name}.mdx? const DATE_FILENAME_REGEX = /^(?<date>\d{4}[-\/]\d{1,2}[-\/]\d{1,2})[-\/]?(?<text>.*?)(\/index)?.mdx?$/;
// Prefer named capture, but older Node versions do not support it.
const DATE_FILENAME_PATTERN = /^(\d{4}-\d{1,2}-\d{1,2})-?(.*?).mdx?$/;
function toUrl({date, link}: DateLink) { type ParsedBlogFileName = {
return `${date date: Date | undefined;
.toISOString() text: string;
.substring(0, '2019-01-01'.length) slug: string;
.replace(/-/g, '/')}/${link}`; };
export function parseBlogFileName(
blogSourceRelative: string,
): ParsedBlogFileName {
const dateFilenameMatch = blogSourceRelative.match(DATE_FILENAME_REGEX);
if (dateFilenameMatch) {
const dateString = dateFilenameMatch.groups!.date!;
const text = dateFilenameMatch.groups!.text!;
// Always treat dates as UTC by adding the `Z`
const date = new Date(`${dateString}Z`);
// TODO use replaceAll once we require NodeJS 16
const slugDate = dateString.replace('-', '/').replace('-', '/');
const slug = `/${slugDate}/${text}`;
return {
date,
text,
slug,
};
} else {
const text = blogSourceRelative.replace(/(\/index)?\.mdx?$/, '');
const slug = `/${text}`;
return {
date: undefined,
text,
slug,
};
}
} }
function formatBlogPostDate(locale: string, date: Date): string { function formatBlogPostDate(locale: string, date: Date): string {
@ -120,153 +144,166 @@ export async function generateBlogFeed(
return feed; return feed;
} }
async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) {
const result = await parseMarkdownFile(blogSourceAbsolute, {
removeContentTitle: true,
});
return {
...result,
frontMatter: validateBlogPostFrontMatter(result.frontMatter),
};
}
async function processBlogSourceFile(
blogSourceRelative: string,
contentPaths: BlogContentPaths,
context: LoadContext,
options: PluginOptions,
): Promise<BlogPost | undefined> {
const {
siteConfig: {baseUrl},
siteDir,
i18n,
} = context;
const {routeBasePath, truncateMarker, showReadingTime, editUrl} = options;
// Lookup in localized folder in priority
const blogDirPath = await getFolderContainingFile(
getContentPathList(contentPaths),
blogSourceRelative,
);
const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative);
const {
frontMatter,
content,
contentTitle,
excerpt,
} = await parseBlogPostMarkdownFile(blogSourceAbsolute);
const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
if (frontMatter.draft && process.env.NODE_ENV === 'production') {
return undefined;
}
if (frontMatter.id) {
console.warn(
chalk.yellow(
`"id" header option is deprecated in ${blogSourceRelative} file. Please use "slug" option instead.`,
),
);
}
const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
async function getDate(): Promise<Date> {
// Prefer user-defined date.
if (frontMatter.date) {
return new Date(frontMatter.date);
} else if (parsedBlogFileName.date) {
return parsedBlogFileName.date;
} else {
// Fallback to file create time
return (await fs.stat(blogSourceAbsolute)).birthtime;
}
}
const date = await getDate();
const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
const description = frontMatter.description ?? excerpt ?? '';
const slug = frontMatter.slug || parsedBlogFileName.slug;
const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
function getBlogEditUrl() {
const blogPathRelative = path.relative(
blogDirPath,
path.resolve(blogSourceAbsolute),
);
if (typeof editUrl === 'function') {
return editUrl({
blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
blogPath: posixPath(blogPathRelative),
permalink,
locale: i18n.currentLocale,
});
} else if (typeof editUrl === 'string') {
const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
const fileContentPath =
isLocalized && options.editLocalizedFiles
? contentPaths.contentPathLocalized
: contentPaths.contentPath;
const contentPathEditUrl = normalizeUrl([
editUrl,
posixPath(path.relative(siteDir, fileContentPath)),
]);
return getEditUrl(blogPathRelative, contentPathEditUrl);
} else {
return undefined;
}
}
return {
id: frontMatter.slug ?? title,
metadata: {
permalink,
editUrl: getBlogEditUrl(),
source: aliasedSource,
title,
description,
date,
formattedDate,
tags: frontMatter.tags ?? [],
readingTime: showReadingTime ? readingTime(content).minutes : undefined,
truncated: truncateMarker?.test(content) || false,
},
};
}
export async function generateBlogPosts( export async function generateBlogPosts(
contentPaths: BlogContentPaths, contentPaths: BlogContentPaths,
{siteConfig, siteDir, i18n}: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
): Promise<BlogPost[]> { ): Promise<BlogPost[]> {
const { const {include, exclude} = options;
include,
exclude,
routeBasePath,
truncateMarker,
showReadingTime,
editUrl,
} = options;
if (!fs.existsSync(contentPaths.contentPath)) { if (!fs.existsSync(contentPaths.contentPath)) {
return []; return [];
} }
const {baseUrl = ''} = siteConfig;
const blogSourceFiles = await Globby(include, { const blogSourceFiles = await Globby(include, {
cwd: contentPaths.contentPath, cwd: contentPaths.contentPath,
ignore: exclude, ignore: exclude,
}); });
const blogPosts: BlogPost[] = []; const blogPosts: BlogPost[] = compact(
await Promise.all(
async function processBlogSourceFile(blogSourceFile: string) { blogSourceFiles.map(async (blogSourceFile: string) => {
// Lookup in localized folder in priority try {
const blogDirPath = await getFolderContainingFile( return await processBlogSourceFile(
getContentPathList(contentPaths), blogSourceFile,
blogSourceFile, contentPaths,
); context,
options,
const source = path.join(blogDirPath, blogSourceFile); );
} catch (e) {
const { console.error(
frontMatter: unsafeFrontMatter, chalk.red(
content, `Processing of blog source file failed for path "${blogSourceFile}"`,
contentTitle, ),
excerpt, );
} = await parseMarkdownFile(source, {removeContentTitle: true}); throw e;
const frontMatter = validateBlogPostFrontMatter(unsafeFrontMatter); }
}),
const aliasedSource = aliasedSitePath(source, siteDir); ),
const blogFileName = path.basename(blogSourceFile);
if (frontMatter.draft && process.env.NODE_ENV === 'production') {
return;
}
if (frontMatter.id) {
console.warn(
chalk.yellow(
`"id" header option is deprecated in ${blogFileName} file. Please use "slug" option instead.`,
),
);
}
let date: Date | undefined;
// Extract date and title from filename.
const dateFilenameMatch = blogFileName.match(DATE_FILENAME_PATTERN);
let linkName = blogFileName.replace(/\.mdx?$/, '');
if (dateFilenameMatch) {
const [, dateString, name] = dateFilenameMatch;
// Always treat dates as UTC by adding the `Z`
date = new Date(`${dateString}Z`);
linkName = name;
}
// Prefer user-defined date.
if (frontMatter.date) {
date = new Date(frontMatter.date);
}
// Use file create time for blog.
date = date ?? (await fs.stat(source)).birthtime;
const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
const title = frontMatter.title ?? contentTitle ?? linkName;
const description = frontMatter.description ?? excerpt ?? '';
const slug =
frontMatter.slug ||
(dateFilenameMatch ? toUrl({date, link: linkName}) : linkName);
const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
function getBlogEditUrl() {
const blogPathRelative = path.relative(blogDirPath, path.resolve(source));
if (typeof editUrl === 'function') {
return editUrl({
blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
blogPath: posixPath(blogPathRelative),
permalink,
locale: i18n.currentLocale,
});
} else if (typeof editUrl === 'string') {
const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
const fileContentPath =
isLocalized && options.editLocalizedFiles
? contentPaths.contentPathLocalized
: contentPaths.contentPath;
const contentPathEditUrl = normalizeUrl([
editUrl,
posixPath(path.relative(siteDir, fileContentPath)),
]);
return getEditUrl(blogPathRelative, contentPathEditUrl);
} else {
return undefined;
}
}
blogPosts.push({
id: frontMatter.slug ?? title,
metadata: {
permalink,
editUrl: getBlogEditUrl(),
source: aliasedSource,
title,
description,
date,
formattedDate,
tags: frontMatter.tags ?? [],
readingTime: showReadingTime ? readingTime(content).minutes : undefined,
truncated: truncateMarker?.test(content) || false,
},
});
}
await Promise.all(
blogSourceFiles.map(async (blogSourceFile: string) => {
try {
return await processBlogSourceFile(blogSourceFile);
} catch (e) {
console.error(
chalk.red(
`Processing of blog source file failed for path "${blogSourceFile}"`,
),
);
throw e;
}
}),
); );
blogPosts.sort( blogPosts.sort(

View file

@ -467,6 +467,12 @@ export default function pluginContentBlog(
// For blog posts a title in markdown is always removed // For blog posts a title in markdown is always removed
// Blog posts title are rendered separately // Blog posts title are rendered separately
removeContentTitle: true, removeContentTitle: true,
// those frontMatter fields will be exported as "frontMatterAssets" and eventually be converted to require() calls for relative file paths
frontMatterAssetKeys: [
'image',
'authorImageURL',
'author_image_URL',
],
}, },
}, },
{ {

View file

@ -32,7 +32,7 @@ export const DEFAULT_OPTIONS = {
blogSidebarCount: 5, blogSidebarCount: 5,
blogSidebarTitle: 'Recent posts', blogSidebarTitle: 'Recent posts',
postsPerPage: 10, postsPerPage: 10,
include: ['*.md', '*.mdx'], include: ['**/*.{md,mdx}'],
exclude: GlobExcludeDefault, exclude: GlobExcludeDefault,
routeBasePath: 'blog', routeBasePath: 'blog',
path: 'blog', path: 'blog',

View file

@ -20,11 +20,6 @@ export interface BlogContent {
blogTagsListPath: string | null; blogTagsListPath: string | null;
} }
export interface DateLink {
date: Date;
link: string;
}
export type FeedType = 'rss' | 'atom'; export type FeedType = 'rss' | 'atom';
export type EditUrlFunction = (editUrlParams: { export type EditUrlFunction = (editUrlParams: {

View file

@ -38,6 +38,7 @@ function BlogListPage(props: Props): JSX.Element {
<BlogPostItem <BlogPostItem
key={BlogPostContent.metadata.permalink} key={BlogPostContent.metadata.permalink}
frontMatter={BlogPostContent.frontMatter} frontMatter={BlogPostContent.frontMatter}
frontMatterAssets={BlogPostContent.frontMatterAssets}
metadata={BlogPostContent.metadata} metadata={BlogPostContent.metadata}
truncated={BlogPostContent.metadata.truncated}> truncated={BlogPostContent.metadata.truncated}>
<BlogPostContent /> <BlogPostContent />

View file

@ -43,6 +43,7 @@ function BlogPostItem(props: Props): JSX.Element {
const { const {
children, children,
frontMatter, frontMatter,
frontMatterAssets,
metadata, metadata,
truncated, truncated,
isBlogPostPage = false, isBlogPostPage = false,
@ -56,12 +57,17 @@ function BlogPostItem(props: Props): JSX.Element {
title, title,
editUrl, editUrl,
} = metadata; } = metadata;
const {author, image, keywords} = frontMatter; const {author, keywords} = frontMatter;
const image = frontMatterAssets.image ?? frontMatter.image;
const authorURL = frontMatter.author_url || frontMatter.authorURL; const authorURL = frontMatter.author_url || frontMatter.authorURL;
const authorTitle = frontMatter.author_title || frontMatter.authorTitle; const authorTitle = frontMatter.author_title || frontMatter.authorTitle;
const authorImageURL = const authorImageURL =
frontMatter.author_image_url || frontMatter.authorImageURL; frontMatterAssets.author_image_url ||
frontMatterAssets.authorImageURL ||
frontMatter.author_image_url ||
frontMatter.authorImageURL;
const renderPostHeader = () => { const renderPostHeader = () => {
const TitleHeading = isBlogPostPage ? 'h1' : 'h2'; const TitleHeading = isBlogPostPage ? 'h1' : 'h2';

View file

@ -14,7 +14,7 @@ import {ThemeClassNames} from '@docusaurus/theme-common';
function BlogPostPage(props: Props): JSX.Element { function BlogPostPage(props: Props): JSX.Element {
const {content: BlogPostContents, sidebar} = props; const {content: BlogPostContents, sidebar} = props;
const {frontMatter, metadata} = BlogPostContents; const {frontMatter, frontMatterAssets, metadata} = BlogPostContents;
const {title, description, nextItem, prevItem} = metadata; const {title, description, nextItem, prevItem} = metadata;
const {hide_table_of_contents: hideTableOfContents} = frontMatter; const {hide_table_of_contents: hideTableOfContents} = frontMatter;
@ -32,6 +32,7 @@ function BlogPostPage(props: Props): JSX.Element {
}> }>
<BlogPostItem <BlogPostItem
frontMatter={frontMatter} frontMatter={frontMatter}
frontMatterAssets={frontMatterAssets}
metadata={metadata} metadata={metadata}
isBlogPostPage> isBlogPostPage>
<BlogPostContents /> <BlogPostContents />

View file

@ -71,6 +71,7 @@ function BlogTagsPostPage(props: Props): JSX.Element {
<BlogPostItem <BlogPostItem
key={BlogPostContent.metadata.permalink} key={BlogPostContent.metadata.permalink}
frontMatter={BlogPostContent.frontMatter} frontMatter={BlogPostContent.frontMatter}
frontMatterAssets={BlogPostContent.frontMatterAssets}
metadata={BlogPostContent.metadata} metadata={BlogPostContent.metadata}
truncated> truncated>
<BlogPostContent /> <BlogPostContent />

View file

@ -27,10 +27,15 @@ declare module '@theme/BlogListPaginator' {
} }
declare module '@theme/BlogPostItem' { declare module '@theme/BlogPostItem' {
import type {FrontMatter, Metadata} from '@theme/BlogPostPage'; import type {
FrontMatter,
FrontMatterAssets,
Metadata,
} from '@theme/BlogPostPage';
export type Props = { export type Props = {
readonly frontMatter: FrontMatter; readonly frontMatter: FrontMatter;
readonly frontMatterAssets: FrontMatterAssets;
readonly metadata: Metadata; readonly metadata: Metadata;
readonly truncated?: string | boolean; readonly truncated?: string | boolean;
readonly isBlogPostPage?: boolean; readonly isBlogPostPage?: boolean;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -3,17 +3,17 @@ title: Announcing Docusaurus 2 Beta
author: Sébastien Lorber author: Sébastien Lorber
authorTitle: Docusaurus maintainer authorTitle: Docusaurus maintainer
authorURL: https://sebastienlorber.com authorURL: https://sebastienlorber.com
authorImageURL: https://github.com/slorber.png authorImageURL: ./img/author.jpeg
authorTwitter: sebastienlorber authorTwitter: sebastienlorber
tags: [release, beta] tags: [release, beta]
image: /img/blog/2021-05-12-announcing-docusaurus-two-beta/social-card.png image: ./img/social-card.png
--- ---
After a lengthy alpha stage in order to ensure feature parity and quality, we are excited to officially release the first **[Docusaurus 2 beta](https://github.com/facebook/docusaurus/releases/tag/v2.0.0-beta.0)**. After a lengthy alpha stage in order to ensure feature parity and quality, we are excited to officially release the first **[Docusaurus 2 beta](https://github.com/facebook/docusaurus/releases/tag/v2.0.0-beta.0)**.
With the announcement of this beta, the team is even more confident that Docusaurus 2 is **ready for mainstream adoption**! With the announcement of this beta, the team is even more confident that Docusaurus 2 is **ready for mainstream adoption**!
![Docusaurus beta party](/img/blog/2021-05-12-announcing-docusaurus-two-beta/image_cropped.png) ![Docusaurus beta party](./img/image_cropped.png)
<!--truncate--> <!--truncate-->
@ -23,13 +23,13 @@ With the announcement of this beta, the team is even more confident that Docusau
Docusaurus 2 is widely adopted and growing fast: Docusaurus 2 is widely adopted and growing fast:
[![Docusaurus growth](/img/blog/2021-05-12-announcing-docusaurus-two-beta/trend.png)](https://www.npmtrends.com/docusaurus-vs-@docusaurus/core) [![Docusaurus growth](./img/trend.png)](https://www.npmtrends.com/docusaurus-vs-@docusaurus/core)
To get a fuller understanding of the quality of current Docusaurus 2 sites, our new [showcase](https://docusaurus.io/showcase) page allows you to filter Docusaurus sites by features, so you may get inspired by real-world production sites with a similar use-case as yours! To get a fuller understanding of the quality of current Docusaurus 2 sites, our new [showcase](https://docusaurus.io/showcase) page allows you to filter Docusaurus sites by features, so you may get inspired by real-world production sites with a similar use-case as yours!
Don't miss our [favorite](https://docusaurus.io/showcase?tags=favorite) sites; they all stand out with something unique: Don't miss our [favorite](https://docusaurus.io/showcase?tags=favorite) sites; they all stand out with something unique:
[![Docusaurus growth](/img/blog/2021-05-12-announcing-docusaurus-two-beta/favorites.png)](https://docusaurus.io/showcase?tags=favorite) [![Docusaurus growth](./img/favorites.png)](https://docusaurus.io/showcase?tags=favorite)
## Why was Docusaurus v2 in alpha for so long? ## Why was Docusaurus v2 in alpha for so long?

View file

@ -86,7 +86,7 @@ module.exports = {
blogSidebarCount: 5, blogSidebarCount: 5,
blogSidebarTitle: 'All our posts', blogSidebarTitle: 'All our posts',
routeBasePath: 'blog', routeBasePath: 'blog',
include: ['*.md', '*.mdx'], include: ['**/*.{md,mdx}'],
exclude: [ exclude: [
'**/_*.{js,jsx,ts,tsx,md,mdx}', '**/_*.{js,jsx,ts,tsx,md,mdx}',
'**/_*/**', '**/_*/**',
@ -132,7 +132,7 @@ Accepted fields:
| `author_image_url` | `string` | `undefined` | The URL to the author's thumbnail image. | | `author_image_url` | `string` | `undefined` | The URL to the author's thumbnail image. |
| `author_title` | `string` | `undefined` | A description of the author. | | `author_title` | `string` | `undefined` | A description of the author. |
| `title` | `string` | Markdown title | The blog post title. | | `title` | `string` | Markdown title | The blog post title. |
| `date` | `string` | File name or file creation time | The blog post creation date. If not specified, this could be extracted from the file name, e.g, `2021-04-15-blog-post.mdx`. Otherwise, it is the Markdown file creation time. | | `date` | `string` | File name or file creation time | The blog post creation date. If not specified, this can be extracted from the file or folder name, e.g, `2021-04-15-blog-post.mdx`, `2021-04-15-blog-post/index.mdx`, `2021/04/15/blog-post.mdx`. Otherwise, it is the Markdown file creation time. |
| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your post. | | `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your post. |
| `draft` | `boolean` | `false` | A boolean flag to indicate that the blog post is work-in-progress and therefore should not be published yet. However, draft blog posts will be displayed during development. | | `draft` | `boolean` | `false` | A boolean flag to indicate that the blog post is work-in-progress and therefore should not be published yet. However, draft blog posts will be displayed during development. |
| `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. | | `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. |

View file

@ -3,6 +3,14 @@ id: blog
title: Blog title: Blog
--- ---
The blog feature enables you to deploy in no time a full-featured blog.
:::info
Check the [Blog Plugin API Reference documentation](./api/plugins/plugin-content-blog.md) for an exhaustive list of options.
:::
## Initial setup {#initial-setup} ## Initial setup {#initial-setup}
To setup your site's blog, start by creating a `blog` directory. To setup your site's blog, start by creating a `blog` directory.
@ -26,9 +34,9 @@ module.exports = {
## Adding posts {#adding-posts} ## Adding posts {#adding-posts}
To publish in the blog, create a file within the blog directory with a formatted name of `YYYY-MM-DD-my-blog-post-title.md`. The post date is extracted from the file name. To publish in the blog, create a Markdown file within the blog directory.
For example, at `my-website/blog/2019-09-05-hello-docusaurus-v2.md`: For example, create a file at `my-website/blog/2019-09-05-hello-docusaurus-v2.md`:
```yml ```yml
--- ---
@ -51,6 +59,34 @@ This is my first post on Docusaurus 2.
A whole bunch of exploration to follow. A whole bunch of exploration to follow.
``` ```
:::note
Docusaurus will extract a `YYYY-MM-DD` date from a file/folder name such as `YYYY-MM-DD-my-blog-post-title.md`.
This naming convention is optional, and you can provide the date as FrontMatter.
<details>
<summary>Example supported patterns</summary>
- `2021-05-28-my-blog-post-title.md`
- `2021-05-28-my-blog-post-title.mdx`
- `2021-05-28-my-blog-post-title/index.md`
- `2021-05-28/my-blog-post-title.md`
- `2021/05/28/my-blog-post-title.md`
- `2021/05-28-my-blog-post-title.md`
- `2021/05/28/my-blog-post-title/index.md`
- ...
</details>
:::
:::tip
Using a folder can be convenient to co-locate blog post images alongside the Markdown file.
:::
The only required field in the front matter is `title`; however, we provide options to add more metadata to your blog post, for example, author information. For all possible fields, see [the API documentation](api/plugins/plugin-content-blog.md#markdown-frontmatter). The only required field in the front matter is `title`; however, we provide options to add more metadata to your blog post, for example, author information. For all possible fields, see [the API documentation](api/plugins/plugin-content-blog.md#markdown-frontmatter).
## Blog list {#blog-list} ## Blog list {#blog-list}

View file

@ -2,6 +2,7 @@
id: creating-pages id: creating-pages
title: Creating Pages title: Creating Pages
slug: /creating-pages slug: /creating-pages
sidebar_label: Pages
--- ---
In this section, we will learn about creating pages in Docusaurus. In this section, we will learn about creating pages in Docusaurus.
@ -18,6 +19,12 @@ Pages do not have sidebars, only [docs](./docs/docs-introduction.md) do.
::: :::
:::info
Check the [Pages Plugin API Reference documentation](./../api/plugins/plugin-content-pages.md) for an exhaustive list of options.
:::
## Add a React page {#add-a-react-page} ## Add a React page {#add-a-react-page}
Create a file `/src/pages/helloReact.js`: Create a file `/src/pages/helloReact.js`:

View file

@ -7,6 +7,12 @@ slug: /docs-introduction
The docs feature provides users with a way to organize Markdown files in a hierarchical format. The docs feature provides users with a way to organize Markdown files in a hierarchical format.
:::info
Check the [Docs Plugin API Reference documentation](./../../api/plugins/plugin-content-docs.md) for an exhaustive list of options.
:::
## Document ID {#document-id} ## Document ID {#document-id}
Every document has a unique `id`. By default, a document `id` is the name of the document (without the extension) relative to the root docs directory. Every document has a unique `id`. By default, a document `id` is the name of the document (without the extension) relative to the root docs directory.