diff --git a/packages/docusaurus-mdx-loader/src/index.js b/packages/docusaurus-mdx-loader/src/index.js index a7f509a79c1c..ea9efc1240af 100644 --- a/packages/docusaurus-mdx-loader/src/index.js +++ b/packages/docusaurus-mdx-loader/src/index.js @@ -31,9 +31,8 @@ module.exports = async function docusaurusMdxLoader(fileString) { const {frontMatter, content: contentWithTitle} = parseFrontMatter(fileString); - // By default, will remove the markdown title from the content - const {content} = parseMarkdownContentTitle(contentWithTitle, { - keepContentTitle: reqOptions.keepContentTitle, + const {content, contentTitle} = parseMarkdownContentTitle(contentWithTitle, { + removeContentTitle: reqOptions.removeContentTitle, }); const hasFrontMatter = Object.keys(frontMatter).length > 0; @@ -69,7 +68,11 @@ module.exports = async function docusaurusMdxLoader(fileString) { return callback(err); } - let exportStr = `export const frontMatter = ${stringifyObject(frontMatter)};`; + let exportStr = ``; + exportStr += `\nexport const frontMatter = ${stringifyObject(frontMatter)};`; + exportStr += `\nexport const contentTitle = ${stringifyObject( + contentTitle, + )};`; // Read metadata for this MDX and export it. if (options.metadataPath && typeof options.metadataPath === 'function') { diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index dbcb29eefd5d..84e173caaeb7 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -146,7 +146,7 @@ export async function generateBlogPosts( content, contentTitle, excerpt, - } = await parseMarkdownFile(source); + } = await parseMarkdownFile(source, {removeContentTitle: true}); const frontMatter = validateBlogPostFrontMatter(unsafeFrontMatter); const aliasedSource = aliasedSitePath(source, siteDir); diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index d0228b6720f5..1d47eb628cb3 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -459,6 +459,9 @@ export default function pluginContentBlog( `${docuHash(aliasedPath)}.json`, ); }, + // For blog posts a title in markdown is always removed + // Blog posts title are rendered separately + removeContentTitle: true, }, }, { diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap index 9572c7a04d3f..722dd3df6475 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap @@ -168,7 +168,7 @@ Object { \\"unversionedId\\": \\"foo/bar\\", \\"id\\": \\"foo/bar\\", \\"isDocsHomePage\\": false, - \\"title\\": \\"Remarkable\\", + \\"title\\": \\"Bar\\", \\"description\\": \\"This is custom description\\", \\"source\\": \\"@site/docs/foo/bar.md\\", \\"sourceDirName\\": \\"foo\\", @@ -190,7 +190,7 @@ Object { \\"unversionedId\\": \\"foo/baz\\", \\"id\\": \\"foo/baz\\", \\"isDocsHomePage\\": false, - \\"title\\": \\"Baz markdown title\\", + \\"title\\": \\"baz\\", \\"description\\": \\"Images\\", \\"source\\": \\"@site/docs/foo/baz.md\\", \\"sourceDirName\\": \\"foo\\", @@ -418,12 +418,12 @@ Object { \\"items\\": [ { \\"type\\": \\"link\\", - \\"label\\": \\"Remarkable\\", + \\"label\\": \\"Bar\\", \\"href\\": \\"/docs/foo/bar\\" }, { \\"type\\": \\"link\\", - \\"label\\": \\"Baz markdown title\\", + \\"label\\": \\"baz\\", \\"href\\": \\"/docs/foo/bazSlug.html\\" } ] diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts index 9d123cc6cc99..d23cc7385269 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts @@ -180,7 +180,7 @@ describe('simple site', () => { isDocsHomePage: false, permalink: '/docs/foo/bar', slug: '/foo/bar', - title: 'Remarkable', + title: 'Bar', description: 'This is custom description', frontMatter: { description: 'This is custom description', @@ -254,7 +254,7 @@ describe('simple site', () => { isDocsHomePage: true, permalink: '/docs/', slug: '/', - title: 'Remarkable', + title: 'Bar', description: 'This is custom description', frontMatter: { description: 'This is custom description', @@ -286,7 +286,7 @@ describe('simple site', () => { isDocsHomePage: false, permalink: '/docs/foo/bazSlug.html', slug: '/foo/bazSlug.html', - title: 'Baz markdown title', + title: 'baz', editUrl: 'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md', description: 'Images', @@ -345,7 +345,7 @@ describe('simple site', () => { isDocsHomePage: false, permalink: '/docs/foo/bazSlug.html', slug: '/foo/bazSlug.html', - title: 'Baz markdown title', + title: 'baz', editUrl: hardcodedEditUrl, description: 'Images', frontMatter: { diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index 3bd8944ed6c7..77c8d3db4d33 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -375,7 +375,7 @@ describe('simple website', () => { 'foo', 'bar.md', ), - title: 'Remarkable', + title: 'Bar', description: 'This is custom description', frontMatter: { description: 'This is custom description', diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 12809041a8cd..b12e2df7c821 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -202,11 +202,9 @@ export function processDocMetadata({ numberPrefixParser: options.numberPrefixParser, }); - // TODO expose both headingTitle+metaTitle to theme? - // Different fallbacks order on purpose! - // See https://github.com/facebook/docusaurus/issues/4665#issuecomment-825831367 - const headingTitle: string = contentTitle ?? frontMatter.title ?? baseID; - // const metaTitle: string = frontMatter.title ?? contentTitle ?? baseID; + // Note: the title is used by default for page title, sidebar label, pagination buttons... + // frontMatter.title should be used in priority over contentTitle (because it can contain markdown/JSX syntax) + const title: string = frontMatter.title ?? contentTitle ?? baseID; const description: string = frontMatter.description ?? excerpt ?? ''; @@ -245,7 +243,7 @@ export function processDocMetadata({ unversionedId, id, isDocsHomePage, - title: headingTitle, + title, description, source: aliasedSitePath(filePath, siteDir), sourceDirName, diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 319549a36ace..76eb8af6d843 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -199,11 +199,7 @@ export default function pluginContentDocs( nextId, } = sidebarsUtils.getDocNavigation(doc.id); const toDocNavLink = (navDocId: string): DocNavLink => ({ - // Use frontMatter.title in priority over a potential # title found in markdown - // See https://github.com/facebook/docusaurus/issues/4665#issuecomment-825831367 - title: - docsBaseById[navDocId].frontMatter.title || - docsBaseById[navDocId].title, + title: docsBaseById[navDocId].title, permalink: docsBaseById[navDocId].permalink, }); return { diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index 87b6c07d07e5..abef806debfb 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -82,6 +82,7 @@ declare module '@theme/DocItem' { readonly frontMatter: FrontMatter; readonly metadata: Metadata; readonly toc: readonly TOCItem[]; + readonly contentTitle: string | undefined; (): JSX.Element; }; }; diff --git a/packages/docusaurus-plugin-content-pages/src/index.ts b/packages/docusaurus-plugin-content-pages/src/index.ts index d94075e9f745..9e28197a934a 100644 --- a/packages/docusaurus-plugin-content-pages/src/index.ts +++ b/packages/docusaurus-plugin-content-pages/src/index.ts @@ -222,7 +222,6 @@ export default function pluginContentPages( rehypePlugins, beforeDefaultRehypePlugins, beforeDefaultRemarkPlugins, - keepContentTitle: true, staticDir: path.join(siteDir, STATIC_DIR_NAME), // Note that metadataPath must be the same/in-sync as // the path from createData for each MDX. diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx index 2307b57da905..f4b76b68d659 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx @@ -47,8 +47,8 @@ function BlogPostItem(props: Props): JSX.Element { truncated, isBlogPostPage = false, } = props; - const {date, formattedDate, permalink, tags, readingTime} = metadata; - const {author, title, image, keywords} = frontMatter; + const {date, formattedDate, permalink, tags, readingTime, title} = metadata; + const {author, image, keywords} = frontMatter; const authorURL = frontMatter.author_url || frontMatter.authorURL; const authorTitle = frontMatter.author_title || frontMatter.authorTitle; diff --git a/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx index 40d1b9b6a300..9146400f7966 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx @@ -13,6 +13,7 @@ import LastUpdated from '@theme/LastUpdated'; import type {Props} from '@theme/DocItem'; import TOC from '@theme/TOC'; import EditThisPage from '@theme/EditThisPage'; +import {MainHeading} from '@theme/Heading'; import clsx from 'clsx'; import styles from './styles.module.css'; @@ -49,13 +50,15 @@ function DocItem(props: Props): JSX.Element { // See https://github.com/facebook/docusaurus/issues/3362 const showVersionBadge = versions.length > 1; - // For meta title, using frontMatter.title in priority over a potential # title found in markdown - // See https://github.com/facebook/docusaurus/issues/4665#issuecomment-825831367 - const metaTitle = frontMatter.title || title; + // We only add a title if: + // - user asks to hide it with frontmatter + // - the markdown content does not already contain a top-level h1 heading + const shouldAddTitle = + !hideTitle && typeof DocContent.contentTitle === 'undefined'; return ( <> - +
)} - {!hideTitle && ( -
-

{title}

-
- )}
+ {/* + Title can be declared inside md content or declared through frontmatter and added manually + To make both cases consistent, the added title is added under the same div.markdown block + See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120 + */} + {shouldAddTitle && {title}}
diff --git a/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css index 283d21b71b62..7d428e957a56 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css @@ -5,11 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -.docTitle { - font-size: 3rem; - margin-bottom: calc(var(--ifm-leading-desktop) * var(--ifm-leading)); -} - .docItemContainer { margin: 0 auto; padding: 0 0.5rem; diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx index 6c0cc35b2747..4daaebf30c62 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx @@ -16,7 +16,24 @@ import {useThemeConfig} from '@docusaurus/theme-common'; import './styles.css'; import styles from './styles.module.css'; -const Heading = (Tag: HeadingType): ((props: Props) => JSX.Element) => +type HeadingComponent = (props: Props) => JSX.Element; + +export const MainHeading: HeadingComponent = function MainHeading({...props}) { + return ( +
+

+ {props.children} +

+
+ ); +}; + +const createAnchorHeading = ( + Tag: HeadingType, +): ((props: Props) => JSX.Element) => function TargetComponent({id, ...props}) { const { navbar: {hideOnScroll}, @@ -51,4 +68,8 @@ const Heading = (Tag: HeadingType): ((props: Props) => JSX.Element) => ); }; +const Heading = (headingType: HeadingType): ((props: Props) => JSX.Element) => { + return headingType === 'h1' ? MainHeading : createAnchorHeading(headingType); +}; + export default Heading; diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css b/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css index 4e24477449a5..3f94fe995476 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/Heading/styles.module.css @@ -8,3 +8,8 @@ .enhancedAnchor { top: calc(var(--ifm-navbar-height) * -1 - 0.5rem); } + +.h1Heading { + font-size: 3rem; + margin-bottom: calc(var(--ifm-leading-desktop) * var(--ifm-leading)); +} diff --git a/packages/docusaurus-theme-classic/src/types.d.ts b/packages/docusaurus-theme-classic/src/types.d.ts index 114e977f7705..3174b1f6b0d4 100644 --- a/packages/docusaurus-theme-classic/src/types.d.ts +++ b/packages/docusaurus-theme-classic/src/types.d.ts @@ -114,6 +114,7 @@ declare module '@theme/Heading' { const Heading: (Tag: HeadingType) => (props: Props) => JSX.Element; export default Heading; + export const MainHeading: (props: Props) => JSX.Element; } declare module '@theme/hooks/useAnnouncementBar' { diff --git a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts b/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts index 0edb4c0cb09b..2f36481e4b41 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts @@ -141,11 +141,71 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ + content: markdown, + contentTitle: 'Markdown Title', + }); + }); + + test('Should parse markdown h1 title at the top and remove it', () => { + const markdown = dedent` + + # Markdown Title + + Lorem Ipsum + + `; + expect( + parseMarkdownContentTitle(markdown, {removeContentTitle: true}), + ).toEqual({ content: 'Lorem Ipsum', contentTitle: 'Markdown Title', }); }); + test('Should parse markdown h1 title at the top and unwrap inline code block', () => { + const markdown = dedent` + + # \`Markdown Title\` + + Lorem Ipsum + + `; + expect(parseMarkdownContentTitle(markdown)).toEqual({ + content: markdown, + contentTitle: 'Markdown Title', + }); + }); + + test('Should parse markdown h1 title and trim content', () => { + const markdown = ` + +# Markdown Title + +Lorem Ipsum + + + +`; + + expect(parseMarkdownContentTitle(markdown)).toEqual({ + content: markdown.trim(), + contentTitle: 'Markdown Title', + }); + }); + + test('Should parse not parse markdown h1 title and trim content', () => { + const markdown = ` + +Lorem Ipsum + +`; + + expect(parseMarkdownContentTitle(markdown)).toEqual({ + content: markdown.trim(), + contentTitle: undefined, + }); + }); + test('Should parse markdown h1 title with fixed anchor-id syntax', () => { const markdown = dedent` @@ -155,7 +215,7 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: 'Lorem Ipsum', + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -169,7 +229,7 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: 'Lorem Ipsum', + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -185,12 +245,7 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: dedent` - ## Heading 2 - - Lorem Ipsum - - `, + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -206,12 +261,7 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: dedent` - # Markdown Title 2 - - Lorem Ipsum - - `, + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -227,20 +277,27 @@ describe('parseMarkdownContentTitle', () => { `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: dedent` + content: markdown, + contentTitle: undefined, + }); + }); - Lorem Ipsum + test('Should parse markdown h1 alternate title', () => { + const markdown = dedent` - # Markdown Title 2 + Markdown Title + ================ Lorem Ipsum - `, - contentTitle: undefined, + `; + expect(parseMarkdownContentTitle(markdown)).toEqual({ + content: markdown, + contentTitle: 'Markdown Title', }); }); - test('Should parse markdown h1 alternate title', () => { + test('Should parse markdown h1 alternate title and remove it', () => { const markdown = dedent` Markdown Title @@ -249,7 +306,9 @@ describe('parseMarkdownContentTitle', () => { Lorem Ipsum `; - expect(parseMarkdownContentTitle(markdown)).toEqual({ + expect( + parseMarkdownContentTitle(markdown, {removeContentTitle: true}), + ).toEqual({ content: 'Lorem Ipsum', contentTitle: 'Markdown Title', }); @@ -271,17 +330,7 @@ describe('parseMarkdownContentTitle', () => { // remove the useless line breaks? Does not matter too much expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: dedent` - import Component1 from '@site/src/components/Component1'; - - import Component2 from '@site/src/components/Component2' - import Component3 from '@site/src/components/Component3' - import './styles.css'; - - - - Lorem Ipsum - `, + content: markdown, contentTitle: 'Markdown Title', }); }); @@ -306,10 +355,16 @@ import "module-name" # Markdown Title Lorem Ipsum - `; +`; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: ` + content: markdown.trim(), + contentTitle: 'Markdown Title', + }); + }); + + test('Should parse markdown h1 title placed after various import declarations and remove it', () => { + const markdown = ` import DefaultComponent from '@site/src/components/Component1'; import DefaultComponent2 from '../relative/path/Component2'; import * as EntireComponent from './relative/path/Component3'; @@ -325,16 +380,22 @@ import './styles.css'; import _ from 'underscore'; import "module-name" - +# Markdown Title Lorem Ipsum - `.trim(), +`; + + expect( + parseMarkdownContentTitle(markdown, {removeContentTitle: true}), + ).toEqual({ + content: markdown.trim().replace('# Markdown Title', ''), contentTitle: 'Markdown Title', }); }); test('Should parse markdown h1 alternate title placed after import declarations', () => { const markdown = dedent` + import Component from '@site/src/components/Component'; import Component from '@site/src/components/Component' import './styles.css'; @@ -346,41 +407,40 @@ Lorem Ipsum `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: dedent` - import Component from '@site/src/components/Component'; - import Component from '@site/src/components/Component' - import './styles.css'; - - Lorem Ipsum - `, + content: markdown, contentTitle: 'Markdown Title', }); }); - test('Should parse title-only', () => { - const markdown = '# Document With Only A Title '; - expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: '', - contentTitle: 'Document With Only A Title', - }); - }); - - test('Should parse markdown h1 title at the top but keep it in content', () => { + test('Should parse markdown h1 alternate title placed after import declarations and remove it', () => { const markdown = dedent` - # Markdown Title + import Component from '@site/src/components/Component'; + import Component from '@site/src/components/Component' + import './styles.css'; + + Markdown Title + ============== Lorem Ipsum `; expect( - parseMarkdownContentTitle(markdown, {keepContentTitle: true}), + parseMarkdownContentTitle(markdown, {removeContentTitle: true}), ).toEqual({ - content: markdown.trim(), + content: markdown.replace('Markdown Title\n==============\n\n', ''), contentTitle: 'Markdown Title', }); }); + test('Should parse title-only', () => { + const markdown = '# Document With Only A Title'; + expect(parseMarkdownContentTitle(markdown)).toEqual({ + content: markdown, + contentTitle: 'Document With Only A Title', + }); + }); + test('Should not parse markdown h1 title in the middle of a doc', () => { const markdown = dedent` @@ -439,7 +499,13 @@ Lorem Ipsum `; expect(parseMarkdownContentTitle(markdown)).toEqual({ - content: dedent` + content: markdown, + contentTitle: 'Markdown Title', + }); + }); + + test('Should parse markdown h1 title placed after multiple import declarations and remove it', () => { + const markdown = dedent` import Component1 from '@site/src/components/Component1'; import Component2 from '@site/src/components/Component2'; import Component3 from '@site/src/components/Component3'; @@ -456,11 +522,16 @@ Lorem Ipsum import Component14 from '@site/src/components/Component14'; import Component15 from '@site/src/components/Component15'; - + # Markdown Title Lorem Ipsum - `, + `; + + expect( + parseMarkdownContentTitle(markdown, {removeContentTitle: true}), + ).toEqual({ + content: markdown.replace('# Markdown Title', ''), contentTitle: 'Markdown Title', }); }); @@ -497,7 +568,9 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "Some text", + "content": "# Markdown Title + + Some text", "contentTitle": "Markdown Title", "excerpt": "Some text", "frontMatter": Object {}, @@ -518,7 +591,9 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "Some text", + "content": "# Markdown Title + + Some text", "contentTitle": "Markdown Title", "excerpt": "Some text", "frontMatter": Object { @@ -542,36 +617,11 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "Some text", - "contentTitle": "Markdown Title alternate", - "excerpt": "Some text", - "frontMatter": Object { - "title": "Frontmatter title", - }, - } - `); - }); - - test('should not warn for duplicate title if keepContentTitle=true', () => { - expect( - parseMarkdownString( - dedent` - --- - title: Frontmatter title - --- - - # Markdown Title - - Some text - `, - {keepContentTitle: true}, - ), - ).toMatchInlineSnapshot(` - Object { - "content": "# Markdown Title + "content": "Markdown Title alternate + ================ Some text", - "contentTitle": "Markdown Title", + "contentTitle": "Markdown Title alternate", "excerpt": "Some text", "frontMatter": Object { "title": "Frontmatter title", @@ -605,24 +655,6 @@ describe('parseMarkdownString', () => { `); }); - test('should parse markdown title and keep it in content', () => { - expect( - parseMarkdownString( - dedent` - # Markdown Title - `, - {keepContentTitle: true}, - ), - ).toMatchInlineSnapshot(` - Object { - "content": "# Markdown Title", - "contentTitle": "Markdown Title", - "excerpt": undefined, - "frontMatter": Object {}, - } - `); - }); - test('should delete only first heading', () => { expect( parseMarkdownString(dedent` @@ -636,7 +668,9 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "test test test # test bar + "content": "# Markdown Title + + test test test # test bar # Markdown Title 2 @@ -692,7 +726,7 @@ describe('parseMarkdownString', () => { test('should parse title only', () => { expect(parseMarkdownString('# test')).toMatchInlineSnapshot(` Object { - "content": "", + "content": "# test", "contentTitle": "test", "excerpt": undefined, "frontMatter": Object {}, @@ -708,7 +742,8 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "", + "content": "test + ===", "contentTitle": "test", "excerpt": undefined, "frontMatter": Object {}, @@ -726,7 +761,7 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "", + "content": "# test", "contentTitle": "test", "excerpt": undefined, "frontMatter": Object { @@ -766,7 +801,9 @@ describe('parseMarkdownString', () => { `), ).toMatchInlineSnapshot(` Object { - "content": "test test test test test test + "content": "# test + + test test test test test test test test test # test bar # test2 ### test diff --git a/packages/docusaurus-utils/src/markdownParser.ts b/packages/docusaurus-utils/src/markdownParser.ts index 9bdfc02772ca..6270a791c133 100644 --- a/packages/docusaurus-utils/src/markdownParser.ts +++ b/packages/docusaurus-utils/src/markdownParser.ts @@ -80,11 +80,21 @@ export function parseFrontMatter( }; } +// Try to convert markdown heading as text +// Does not need to be perfect, it is only used as a fallback when frontMatter.title is not provided +// For now, we just unwrap possible inline code blocks (# `config.js`) +function toTextContentTitle(contentTitle: string): string { + if (contentTitle.startsWith('`') && contentTitle.endsWith('`')) { + return contentTitle.substring(1, contentTitle.length - 1); + } + return contentTitle; +} + export function parseMarkdownContentTitle( contentUntrimmed: string, - options?: {keepContentTitle?: boolean}, + options?: {removeContentTitle?: boolean}, ): {content: string; contentTitle: string | undefined} { - const keepContentTitleOption = options?.keepContentTitle ?? false; + const removeContentTitleOption = options?.removeContentTitle ?? false; const content = contentUntrimmed.trim(); @@ -108,16 +118,15 @@ export function parseMarkdownContentTitle( if (!pattern || !title) { return {content, contentTitle: undefined}; + } else { + const newContent = removeContentTitleOption + ? content.replace(pattern, '') + : content; + return { + content: newContent.trim(), + contentTitle: toTextContentTitle(title.trim()).trim(), + }; } - - const newContent = keepContentTitleOption - ? content - : content.replace(pattern, ''); - - return { - content: newContent.trim(), - contentTitle: title.trim(), - }; } type ParsedMarkdown = { @@ -129,22 +138,16 @@ type ParsedMarkdown = { export function parseMarkdownString( markdownFileContent: string, - options?: { - keepContentTitle?: boolean; - }, + options?: {removeContentTitle?: boolean}, ): ParsedMarkdown { try { - const keepContentTitle = options?.keepContentTitle ?? false; - const {frontMatter, content: contentWithoutFrontMatter} = parseFrontMatter( markdownFileContent, ); const {content, contentTitle} = parseMarkdownContentTitle( contentWithoutFrontMatter, - { - keepContentTitle, - }, + options, ); const excerpt = createExcerpt(content); @@ -166,10 +169,11 @@ This can happen if you use special characters like : in frontmatter values (try export async function parseMarkdownFile( source: string, + options?: {removeContentTitle?: boolean}, ): Promise { const markdownString = await fs.readFile(source, 'utf-8'); try { - return parseMarkdownString(markdownString); + return parseMarkdownString(markdownString, options); } catch (e) { throw new Error( `Error while parsing markdown file ${source} diff --git a/website/docs/api/docusaurus.config.js.md b/website/docs/api/docusaurus.config.js.md index 5af7569d277c..b44fb6a32c22 100644 --- a/website/docs/api/docusaurus.config.js.md +++ b/website/docs/api/docusaurus.config.js.md @@ -1,10 +1,11 @@ --- id: docusaurus.config.js -title: docusaurus.config.js description: API reference for Docusaurus configuration file. slug: /docusaurus.config.js --- +# `docusaurus.config.js` + ## Overview {#overview} `docusaurus.config.js` contains configurations for your site and is placed in the root directory of your site. diff --git a/website/docs/api/plugins/plugin-content-docs.md b/website/docs/api/plugins/plugin-content-docs.md index 2861ce4f72c9..a51eaf5f7447 100644 --- a/website/docs/api/plugins/plugin-content-docs.md +++ b/website/docs/api/plugins/plugin-content-docs.md @@ -197,7 +197,7 @@ Markdown documents can use the following markdown frontmatter metadata fields, e - `id`: A unique document id. If this field is not present, the document's `id` will default to its file name (without the extension) - `title`: The title of your document. If this field is not present, the document's `title` will default to its `id` -- `hide_title`: Whether to hide the title at the top of the doc. By default, it is `false` +- `hide_title`: Whether to hide the title at the top of the doc. It only hides a title declared through the frontmatter, and have no effect on a title at the top of your Markdown document. By default, it is `false` - `hide_table_of_contents`: Whether to hide the table of contents to the right. By default it is `false` - `sidebar_label`: The text shown in the document sidebar and in the next/previous button for this document. If this field is not present, the document's `sidebar_label` will default to its `title` - `sidebar_position`: Permits to control the position of a doc inside the generated sidebar slice, when using `autogenerated` sidebar items. Can be Int or Float.