From cabb768473bf5231eeb19769dab7df99b47103a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 6 Aug 2021 17:51:59 +0200 Subject: [PATCH] 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 --- packages/docusaurus-mdx-loader/src/index.js | 55 ++- .../docusaurus-plugin-content-blog/index.d.ts | 7 + .../src/__tests__/blogUtils.test.ts | 94 +++++ .../src/blogFrontMatter.ts | 2 +- .../src/blogUtils.ts | 323 ++++++++++-------- .../src/index.ts | 6 + .../src/pluginOptionSchema.ts | 2 +- .../src/types.ts | 5 - .../src/theme/BlogListPage/index.tsx | 1 + .../src/theme/BlogPostItem/index.tsx | 10 +- .../src/theme/BlogPostPage/index.tsx | 3 +- .../src/theme/BlogTagsPostsPage/index.tsx | 1 + .../docusaurus-theme-classic/src/types.d.ts | 7 +- .../img/author.jpeg | Bin 0 -> 3993 bytes .../img}/favorites.png | Bin .../img}/image.png | Bin .../img}/image_cropped.png | Bin .../img}/social-card.png | Bin .../img}/trend.png | Bin .../index.md} | 10 +- .../docs/api/plugins/plugin-content-blog.md | 4 +- website/docs/blog.md | 40 ++- website/docs/guides/creating-pages.md | 7 + website/docs/guides/docs/docs-introduction.md | 6 + 24 files changed, 417 insertions(+), 166 deletions(-) create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts create mode 100644 website/blog/2021-05-12-announcing-docusaurus-two-beta/img/author.jpeg rename website/{static/img/blog/2021-05-12-announcing-docusaurus-two-beta => blog/2021-05-12-announcing-docusaurus-two-beta/img}/favorites.png (100%) rename website/{static/img/blog/2021-05-12-announcing-docusaurus-two-beta => blog/2021-05-12-announcing-docusaurus-two-beta/img}/image.png (100%) rename website/{static/img/blog/2021-05-12-announcing-docusaurus-two-beta => blog/2021-05-12-announcing-docusaurus-two-beta/img}/image_cropped.png (100%) rename website/{static/img/blog/2021-05-12-announcing-docusaurus-two-beta => blog/2021-05-12-announcing-docusaurus-two-beta/img}/social-card.png (100%) rename website/{static/img/blog/2021-05-12-announcing-docusaurus-two-beta => blog/2021-05-12-announcing-docusaurus-two-beta/img}/trend.png (100%) rename website/blog/{2021-05-12-announcing-docusaurus-two-beta.md => 2021-05-12-announcing-docusaurus-two-beta/index.md} (91%) diff --git a/packages/docusaurus-mdx-loader/src/index.js b/packages/docusaurus-mdx-loader/src/index.js index 82cb639524..bc555c1be5 100644 --- a/packages/docusaurus-mdx-loader/src/index.js +++ b/packages/docusaurus-mdx-loader/src/index.js @@ -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, )};`; diff --git a/packages/docusaurus-plugin-content-blog/index.d.ts b/packages/docusaurus-plugin-content-blog/index.d.ts index d16f8cdde7..48cd3c38da 100644 --- a/packages/docusaurus-plugin-content-blog/index.d.ts +++ b/packages/docusaurus-plugin-content-blog/index.d.ts @@ -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; diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts new file mode 100644 index 0000000000..99d5f1713b --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts @@ -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', + }); + }); +}); diff --git a/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts b/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts index c7524d9077..a9ad7c16d7 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts @@ -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; diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 0a8c6868d3..6bba53b7cb 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -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 = /^(?\d{4}[-\/]\d{1,2}[-\/]\d{1,2})[-\/]?(?.*?)(\/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,153 +144,166 @@ export async function generateBlogFeed( 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 { + 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 { + // 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( contentPaths: BlogContentPaths, - {siteConfig, siteDir, i18n}: LoadContext, + context: LoadContext, options: PluginOptions, ): Promise { - const { - include, - exclude, - routeBasePath, - truncateMarker, - showReadingTime, - editUrl, - } = options; + const {include, exclude} = options; if (!fs.existsSync(contentPaths.contentPath)) { return []; } - const {baseUrl = ''} = siteConfig; const blogSourceFiles = await Globby(include, { cwd: contentPaths.contentPath, ignore: exclude, }); - const blogPosts: BlogPost[] = []; - - async function processBlogSourceFile(blogSourceFile: string) { - // Lookup in localized folder in priority - const blogDirPath = await getFolderContainingFile( - getContentPathList(contentPaths), - blogSourceFile, - ); - - const source = path.join(blogDirPath, blogSourceFile); - - const { - frontMatter: unsafeFrontMatter, - content, - contentTitle, - excerpt, - } = await parseMarkdownFile(source, {removeContentTitle: true}); - 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; - } - }), + const blogPosts: BlogPost[] = compact( + await Promise.all( + blogSourceFiles.map(async (blogSourceFile: string) => { + try { + return await processBlogSourceFile( + blogSourceFile, + contentPaths, + context, + options, + ); + } catch (e) { + console.error( + chalk.red( + `Processing of blog source file failed for path "${blogSourceFile}"`, + ), + ); + throw e; + } + }), + ), ); blogPosts.sort( diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 5bce9142dd..de672bae40 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -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', + ], }, }, { diff --git a/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts b/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts index e487b9d3c8..3e79dafb27 100644 --- a/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts +++ b/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts @@ -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', diff --git a/packages/docusaurus-plugin-content-blog/src/types.ts b/packages/docusaurus-plugin-content-blog/src/types.ts index e9fe5eb3fb..f4b59e305a 100644 --- a/packages/docusaurus-plugin-content-blog/src/types.ts +++ b/packages/docusaurus-plugin-content-blog/src/types.ts @@ -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: { diff --git a/packages/docusaurus-theme-classic/src/theme/BlogListPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogListPage/index.tsx index 282c5d4733..27d61f3186 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogListPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogListPage/index.tsx @@ -38,6 +38,7 @@ function BlogListPage(props: Props): JSX.Element { diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx index d5abe7a300..7ed64193b3 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx @@ -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'; diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostPage/index.tsx index c2c311c611..455cfa7f48 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostPage/index.tsx @@ -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 { }> diff --git a/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx index 4b2c9d66be..899407741b 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx @@ -71,6 +71,7 @@ function BlogTagsPostPage(props: Props): JSX.Element { diff --git a/packages/docusaurus-theme-classic/src/types.d.ts b/packages/docusaurus-theme-classic/src/types.d.ts index 4241309622..3ef1ff7dd5 100644 --- a/packages/docusaurus-theme-classic/src/types.d.ts +++ b/packages/docusaurus-theme-classic/src/types.d.ts @@ -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; diff --git a/website/blog/2021-05-12-announcing-docusaurus-two-beta/img/author.jpeg b/website/blog/2021-05-12-announcing-docusaurus-two-beta/img/author.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..be9fdfdbf43914eb6dfe8d06911a2cc57cc92cb4 GIT binary patch literal 3993 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 U!XZ|Y_!bLx@Vd^_J99MhKUx3=G5`Po literal 0 HcmV?d00001 diff --git a/website/static/img/blog/2021-05-12-announcing-docusaurus-two-beta/favorites.png b/website/blog/2021-05-12-announcing-docusaurus-two-beta/img/favorites.png similarity index 100% rename from website/static/img/blog/2021-05-12-announcing-docusaurus-two-beta/favorites.png rename to website/blog/2021-05-12-announcing-docusaurus-two-beta/img/favorites.png diff --git a/website/static/img/blog/2021-05-12-announcing-docusaurus-two-beta/image.png b/website/blog/2021-05-12-announcing-docusaurus-two-beta/img/image.png similarity index 100% rename from website/static/img/blog/2021-05-12-announcing-docusaurus-two-beta/image.png rename to website/blog/2021-05-12-announcing-docusaurus-two-beta/img/image.png diff --git a/website/static/img/blog/2021-05-12-announcing-docusaurus-two-beta/image_cropped.png b/website/blog/2021-05-12-announcing-docusaurus-two-beta/img/image_cropped.png similarity index 100% rename from website/static/img/blog/2021-05-12-announcing-docusaurus-two-beta/image_cropped.png rename to website/blog/2021-05-12-announcing-docusaurus-two-beta/img/image_cropped.png diff --git a/website/static/img/blog/2021-05-12-announcing-docusaurus-two-beta/social-card.png b/website/blog/2021-05-12-announcing-docusaurus-two-beta/img/social-card.png similarity index 100% rename from website/static/img/blog/2021-05-12-announcing-docusaurus-two-beta/social-card.png rename to website/blog/2021-05-12-announcing-docusaurus-two-beta/img/social-card.png diff --git a/website/static/img/blog/2021-05-12-announcing-docusaurus-two-beta/trend.png b/website/blog/2021-05-12-announcing-docusaurus-two-beta/img/trend.png similarity index 100% rename from website/static/img/blog/2021-05-12-announcing-docusaurus-two-beta/trend.png rename to website/blog/2021-05-12-announcing-docusaurus-two-beta/img/trend.png diff --git a/website/blog/2021-05-12-announcing-docusaurus-two-beta.md b/website/blog/2021-05-12-announcing-docusaurus-two-beta/index.md similarity index 91% rename from website/blog/2021-05-12-announcing-docusaurus-two-beta.md rename to website/blog/2021-05-12-announcing-docusaurus-two-beta/index.md index 38790117f5..c25cc2f616 100644 --- a/website/blog/2021-05-12-announcing-docusaurus-two-beta.md +++ b/website/blog/2021-05-12-announcing-docusaurus-two-beta/index.md @@ -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**! -![Docusaurus beta party](/img/blog/2021-05-12-announcing-docusaurus-two-beta/image_cropped.png) +![Docusaurus beta party](./img/image_cropped.png) @@ -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 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! 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? diff --git a/website/docs/api/plugins/plugin-content-blog.md b/website/docs/api/plugins/plugin-content-blog.md index 32f28b5a1d..23e4ba8264 100644 --- a/website/docs/api/plugins/plugin-content-blog.md +++ b/website/docs/api/plugins/plugin-content-blog.md @@ -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. | diff --git a/website/docs/blog.md b/website/docs/blog.md index effe5aeee7..6e7184e0ee 100644 --- a/website/docs/blog.md +++ b/website/docs/blog.md @@ -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. + +
+Example supported patterns + +- `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` +- ... + +
+ +::: + +:::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} diff --git a/website/docs/guides/creating-pages.md b/website/docs/guides/creating-pages.md index a30f27b4d0..c622ae5fcd 100644 --- a/website/docs/guides/creating-pages.md +++ b/website/docs/guides/creating-pages.md @@ -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`: diff --git a/website/docs/guides/docs/docs-introduction.md b/website/docs/guides/docs/docs-introduction.md index 9203488ed4..b2cd257e91 100644 --- a/website/docs/guides/docs/docs-introduction.md +++ b/website/docs/guides/docs/docs-introduction.md @@ -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.