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
|
@ -19,6 +19,12 @@ const toc = require('./remark/toc');
|
|||
const unwrapMdxCodeBlocks = require('./remark/unwrapMdxCodeBlocks');
|
||||
const transformImage = require('./remark/transformImage');
|
||||
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 = {
|
||||
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) {
|
||||
const callback = this.async();
|
||||
|
||||
const filePath = this.resourcePath;
|
||||
const reqOptions = this.getOptions() || {};
|
||||
|
||||
const {frontMatter, content: contentWithTitle} = parseFrontMatter(fileString);
|
||||
|
@ -51,8 +97,6 @@ module.exports = async function docusaurusMdxLoader(fileString) {
|
|||
|
||||
const hasFrontMatter = Object.keys(frontMatter).length > 0;
|
||||
|
||||
const filePath = this.resourcePath;
|
||||
|
||||
const options = {
|
||||
...reqOptions,
|
||||
remarkPlugins: [
|
||||
|
@ -80,6 +124,11 @@ module.exports = async function docusaurusMdxLoader(fileString) {
|
|||
|
||||
let exportStr = ``;
|
||||
exportStr += `\nexport const frontMatter = ${stringifyObject(frontMatter)};`;
|
||||
exportStr += `\nexport const frontMatterAssets = ${createFrontMatterAssetsExportCode(
|
||||
filePath,
|
||||
frontMatter,
|
||||
reqOptions.frontMatterAssetKeys,
|
||||
)};`;
|
||||
exportStr += `\nexport const contentTitle = ${stringifyObject(
|
||||
contentTitle,
|
||||
)};`;
|
||||
|
|
|
@ -42,6 +42,12 @@ declare module '@theme/BlogPostPage' {
|
|||
readonly hide_table_of_contents?: boolean;
|
||||
};
|
||||
|
||||
export type FrontMatterAssets = {
|
||||
readonly image?: string;
|
||||
readonly author_image_url?: string;
|
||||
readonly authorImageURL?: string;
|
||||
};
|
||||
|
||||
export type Metadata = {
|
||||
readonly title: string;
|
||||
readonly date: string;
|
||||
|
@ -61,6 +67,7 @@ declare module '@theme/BlogPostPage' {
|
|||
|
||||
export type Content = {
|
||||
readonly frontMatter: FrontMatter;
|
||||
readonly frontMatterAssets: FrontMatterAssets;
|
||||
readonly metadata: Metadata;
|
||||
readonly toc: readonly TOCItem[];
|
||||
(): JSX.Element;
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -21,7 +21,7 @@ export type BlogPostFrontMatter = {
|
|||
tags?: (string | Tag)[];
|
||||
slug?: string;
|
||||
draft?: boolean;
|
||||
date?: Date;
|
||||
date?: Date | string; // Yaml automagically convert some string patterns as Date, but not all
|
||||
|
||||
author?: string;
|
||||
author_title?: string;
|
||||
|
|
|
@ -10,11 +10,10 @@ import chalk from 'chalk';
|
|||
import path from 'path';
|
||||
import readingTime from 'reading-time';
|
||||
import {Feed} from 'feed';
|
||||
import {keyBy, mapValues} from 'lodash';
|
||||
import {compact, keyBy, mapValues} from 'lodash';
|
||||
import {
|
||||
PluginOptions,
|
||||
BlogPost,
|
||||
DateLink,
|
||||
BlogContentPaths,
|
||||
BlogMarkdownLoaderOptions,
|
||||
} from './types';
|
||||
|
@ -44,15 +43,40 @@ export function getSourceToPermalink(
|
|||
);
|
||||
}
|
||||
|
||||
// YYYY-MM-DD-{name}.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?$/;
|
||||
const DATE_FILENAME_REGEX = /^(?<date>\d{4}[-\/]\d{1,2}[-\/]\d{1,2})[-\/]?(?<text>.*?)(\/index)?.mdx?$/;
|
||||
|
||||
function toUrl({date, link}: DateLink) {
|
||||
return `${date
|
||||
.toISOString()
|
||||
.substring(0, '2019-01-01'.length)
|
||||
.replace(/-/g, '/')}/${link}`;
|
||||
type ParsedBlogFileName = {
|
||||
date: Date | undefined;
|
||||
text: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
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 {
|
||||
|
@ -120,97 +144,87 @@ export async function generateBlogFeed(
|
|||
return feed;
|
||||
}
|
||||
|
||||
export async function generateBlogPosts(
|
||||
contentPaths: BlogContentPaths,
|
||||
{siteConfig, siteDir, i18n}: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Promise<BlogPost[]> {
|
||||
const {
|
||||
include,
|
||||
exclude,
|
||||
routeBasePath,
|
||||
truncateMarker,
|
||||
showReadingTime,
|
||||
editUrl,
|
||||
} = options;
|
||||
|
||||
if (!fs.existsSync(contentPaths.contentPath)) {
|
||||
return [];
|
||||
async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) {
|
||||
const result = await parseMarkdownFile(blogSourceAbsolute, {
|
||||
removeContentTitle: true,
|
||||
});
|
||||
return {
|
||||
...result,
|
||||
frontMatter: validateBlogPostFrontMatter(result.frontMatter),
|
||||
};
|
||||
}
|
||||
|
||||
const {baseUrl = ''} = siteConfig;
|
||||
const blogSourceFiles = await Globby(include, {
|
||||
cwd: contentPaths.contentPath,
|
||||
ignore: exclude,
|
||||
});
|
||||
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;
|
||||
|
||||
const blogPosts: BlogPost[] = [];
|
||||
|
||||
async function processBlogSourceFile(blogSourceFile: string) {
|
||||
// Lookup in localized folder in priority
|
||||
const blogDirPath = await getFolderContainingFile(
|
||||
getContentPathList(contentPaths),
|
||||
blogSourceFile,
|
||||
blogSourceRelative,
|
||||
);
|
||||
|
||||
const source = path.join(blogDirPath, blogSourceFile);
|
||||
const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative);
|
||||
|
||||
const {
|
||||
frontMatter: unsafeFrontMatter,
|
||||
frontMatter,
|
||||
content,
|
||||
contentTitle,
|
||||
excerpt,
|
||||
} = await parseMarkdownFile(source, {removeContentTitle: true});
|
||||
const frontMatter = validateBlogPostFrontMatter(unsafeFrontMatter);
|
||||
} = await parseBlogPostMarkdownFile(blogSourceAbsolute);
|
||||
|
||||
const aliasedSource = aliasedSitePath(source, siteDir);
|
||||
|
||||
const blogFileName = path.basename(blogSourceFile);
|
||||
const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
|
||||
|
||||
if (frontMatter.draft && process.env.NODE_ENV === 'production') {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (frontMatter.id) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`"id" header option is deprecated in ${blogFileName} file. Please use "slug" option instead.`,
|
||||
`"id" header option is deprecated in ${blogSourceRelative} 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;
|
||||
}
|
||||
const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
|
||||
|
||||
async function getDate(): Promise<Date> {
|
||||
// Prefer user-defined date.
|
||||
if (frontMatter.date) {
|
||||
date = new Date(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;
|
||||
}
|
||||
}
|
||||
|
||||
// Use file create time for blog.
|
||||
date = date ?? (await fs.stat(source)).birthtime;
|
||||
const date = await getDate();
|
||||
const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
|
||||
|
||||
const title = frontMatter.title ?? contentTitle ?? linkName;
|
||||
const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
|
||||
const description = frontMatter.description ?? excerpt ?? '';
|
||||
|
||||
const slug =
|
||||
frontMatter.slug ||
|
||||
(dateFilenameMatch ? toUrl({date, link: linkName}) : linkName);
|
||||
const slug = frontMatter.slug || parsedBlogFileName.slug;
|
||||
|
||||
const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
|
||||
|
||||
function getBlogEditUrl() {
|
||||
const blogPathRelative = path.relative(blogDirPath, path.resolve(source));
|
||||
const blogPathRelative = path.relative(
|
||||
blogDirPath,
|
||||
path.resolve(blogSourceAbsolute),
|
||||
);
|
||||
|
||||
if (typeof editUrl === 'function') {
|
||||
return editUrl({
|
||||
|
@ -237,7 +251,7 @@ export async function generateBlogPosts(
|
|||
}
|
||||
}
|
||||
|
||||
blogPosts.push({
|
||||
return {
|
||||
id: frontMatter.slug ?? title,
|
||||
metadata: {
|
||||
permalink,
|
||||
|
@ -251,13 +265,35 @@ export async function generateBlogPosts(
|
|||
readingTime: showReadingTime ? readingTime(content).minutes : undefined,
|
||||
truncated: truncateMarker?.test(content) || false,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateBlogPosts(
|
||||
contentPaths: BlogContentPaths,
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Promise<BlogPost[]> {
|
||||
const {include, exclude} = options;
|
||||
|
||||
if (!fs.existsSync(contentPaths.contentPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const blogSourceFiles = await Globby(include, {
|
||||
cwd: contentPaths.contentPath,
|
||||
ignore: exclude,
|
||||
});
|
||||
|
||||
const blogPosts: BlogPost[] = compact(
|
||||
await Promise.all(
|
||||
blogSourceFiles.map(async (blogSourceFile: string) => {
|
||||
try {
|
||||
return await processBlogSourceFile(blogSourceFile);
|
||||
return await processBlogSourceFile(
|
||||
blogSourceFile,
|
||||
contentPaths,
|
||||
context,
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
|
@ -267,6 +303,7 @@ export async function generateBlogPosts(
|
|||
throw e;
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
blogPosts.sort(
|
||||
|
|
|
@ -467,6 +467,12 @@ export default function pluginContentBlog(
|
|||
// For blog posts a title in markdown is always removed
|
||||
// Blog posts title are rendered separately
|
||||
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',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -32,7 +32,7 @@ export const DEFAULT_OPTIONS = {
|
|||
blogSidebarCount: 5,
|
||||
blogSidebarTitle: 'Recent posts',
|
||||
postsPerPage: 10,
|
||||
include: ['*.md', '*.mdx'],
|
||||
include: ['**/*.{md,mdx}'],
|
||||
exclude: GlobExcludeDefault,
|
||||
routeBasePath: 'blog',
|
||||
path: 'blog',
|
||||
|
|
|
@ -20,11 +20,6 @@ export interface BlogContent {
|
|||
blogTagsListPath: string | null;
|
||||
}
|
||||
|
||||
export interface DateLink {
|
||||
date: Date;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export type FeedType = 'rss' | 'atom';
|
||||
|
||||
export type EditUrlFunction = (editUrlParams: {
|
||||
|
|
|
@ -38,6 +38,7 @@ function BlogListPage(props: Props): JSX.Element {
|
|||
<BlogPostItem
|
||||
key={BlogPostContent.metadata.permalink}
|
||||
frontMatter={BlogPostContent.frontMatter}
|
||||
frontMatterAssets={BlogPostContent.frontMatterAssets}
|
||||
metadata={BlogPostContent.metadata}
|
||||
truncated={BlogPostContent.metadata.truncated}>
|
||||
<BlogPostContent />
|
||||
|
|
|
@ -43,6 +43,7 @@ function BlogPostItem(props: Props): JSX.Element {
|
|||
const {
|
||||
children,
|
||||
frontMatter,
|
||||
frontMatterAssets,
|
||||
metadata,
|
||||
truncated,
|
||||
isBlogPostPage = false,
|
||||
|
@ -56,12 +57,17 @@ function BlogPostItem(props: Props): JSX.Element {
|
|||
title,
|
||||
editUrl,
|
||||
} = metadata;
|
||||
const {author, image, keywords} = frontMatter;
|
||||
const {author, keywords} = frontMatter;
|
||||
|
||||
const image = frontMatterAssets.image ?? frontMatter.image;
|
||||
|
||||
const authorURL = frontMatter.author_url || frontMatter.authorURL;
|
||||
const authorTitle = frontMatter.author_title || frontMatter.authorTitle;
|
||||
const authorImageURL =
|
||||
frontMatter.author_image_url || frontMatter.authorImageURL;
|
||||
frontMatterAssets.author_image_url ||
|
||||
frontMatterAssets.authorImageURL ||
|
||||
frontMatter.author_image_url ||
|
||||
frontMatter.authorImageURL;
|
||||
|
||||
const renderPostHeader = () => {
|
||||
const TitleHeading = isBlogPostPage ? 'h1' : 'h2';
|
||||
|
|
|
@ -14,7 +14,7 @@ import {ThemeClassNames} from '@docusaurus/theme-common';
|
|||
|
||||
function BlogPostPage(props: Props): JSX.Element {
|
||||
const {content: BlogPostContents, sidebar} = props;
|
||||
const {frontMatter, metadata} = BlogPostContents;
|
||||
const {frontMatter, frontMatterAssets, metadata} = BlogPostContents;
|
||||
const {title, description, nextItem, prevItem} = metadata;
|
||||
const {hide_table_of_contents: hideTableOfContents} = frontMatter;
|
||||
|
||||
|
@ -32,6 +32,7 @@ function BlogPostPage(props: Props): JSX.Element {
|
|||
}>
|
||||
<BlogPostItem
|
||||
frontMatter={frontMatter}
|
||||
frontMatterAssets={frontMatterAssets}
|
||||
metadata={metadata}
|
||||
isBlogPostPage>
|
||||
<BlogPostContents />
|
||||
|
|
|
@ -71,6 +71,7 @@ function BlogTagsPostPage(props: Props): JSX.Element {
|
|||
<BlogPostItem
|
||||
key={BlogPostContent.metadata.permalink}
|
||||
frontMatter={BlogPostContent.frontMatter}
|
||||
frontMatterAssets={BlogPostContent.frontMatterAssets}
|
||||
metadata={BlogPostContent.metadata}
|
||||
truncated>
|
||||
<BlogPostContent />
|
||||
|
|
|
@ -27,10 +27,15 @@ declare module '@theme/BlogListPaginator' {
|
|||
}
|
||||
|
||||
declare module '@theme/BlogPostItem' {
|
||||
import type {FrontMatter, Metadata} from '@theme/BlogPostPage';
|
||||
import type {
|
||||
FrontMatter,
|
||||
FrontMatterAssets,
|
||||
Metadata,
|
||||
} from '@theme/BlogPostPage';
|
||||
|
||||
export type Props = {
|
||||
readonly frontMatter: FrontMatter;
|
||||
readonly frontMatterAssets: FrontMatterAssets;
|
||||
readonly metadata: Metadata;
|
||||
readonly truncated?: string | boolean;
|
||||
readonly isBlogPostPage?: boolean;
|
||||
|
|
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 488 KiB After Width: | Height: | Size: 488 KiB |
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 200 KiB |
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
Before Width: | Height: | Size: 728 KiB After Width: | Height: | Size: 728 KiB |
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
@ -3,17 +3,17 @@ title: Announcing Docusaurus 2 Beta
|
|||
author: Sébastien Lorber
|
||||
authorTitle: Docusaurus maintainer
|
||||
authorURL: https://sebastienlorber.com
|
||||
authorImageURL: https://github.com/slorber.png
|
||||
authorImageURL: ./img/author.jpeg
|
||||
authorTwitter: sebastienlorber
|
||||
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)**.
|
||||
|
||||
With the announcement of this beta, the team is even more confident that Docusaurus 2 is **ready for mainstream adoption**!
|
||||
|
||||

|
||||

|
||||
|
||||
<!--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:
|
||||
|
||||
[](https://www.npmtrends.com/docusaurus-vs-@docusaurus/core)
|
||||
[](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!
|
||||
|
||||
Don't miss our [favorite](https://docusaurus.io/showcase?tags=favorite) sites; they all stand out with something unique:
|
||||
|
||||
[](https://docusaurus.io/showcase?tags=favorite)
|
||||
[](https://docusaurus.io/showcase?tags=favorite)
|
||||
|
||||
## Why was Docusaurus v2 in alpha for so long?
|
||||
|
|
@ -86,7 +86,7 @@ module.exports = {
|
|||
blogSidebarCount: 5,
|
||||
blogSidebarTitle: 'All our posts',
|
||||
routeBasePath: 'blog',
|
||||
include: ['*.md', '*.mdx'],
|
||||
include: ['**/*.{md,mdx}'],
|
||||
exclude: [
|
||||
'**/_*.{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_title` | `string` | `undefined` | A description of the author. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
|
|
|
@ -3,6 +3,14 @@ id: 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}
|
||||
|
||||
To setup your site's blog, start by creating a `blog` directory.
|
||||
|
@ -26,9 +34,9 @@ module.exports = {
|
|||
|
||||
## 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
|
||||
---
|
||||
|
@ -51,6 +59,34 @@ This is my first post on Docusaurus 2.
|
|||
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).
|
||||
|
||||
## Blog list {#blog-list}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
id: creating-pages
|
||||
title: Creating Pages
|
||||
slug: /creating-pages
|
||||
sidebar_label: Pages
|
||||
---
|
||||
|
||||
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}
|
||||
|
||||
Create a file `/src/pages/helloReact.js`:
|
||||
|
|
|
@ -7,6 +7,12 @@ slug: /docs-introduction
|
|||
|
||||
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}
|
||||
|
||||
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.
|
||||
|
|