From 8101efe2d8f15be9e5fe5d89013196ded51a14c0 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sat, 23 Jul 2022 17:06:18 +0200 Subject: [PATCH] fix(nextra): use `rehype-mdx-title` to determine page title (#515) * fix(nextra): use `rehype-mdx-title` to determine page title * Update packages/nextra/src/loader.ts * Update packages/nextra/src/loader.ts * add changeset --- .changeset/breezy-ghosts-argue.md | 5 + packages/nextra-theme-blog/src/index.tsx | 5 +- .../__snapshots__/compile.test.ts.snap | 140 +++++++++++++++++- packages/nextra/__test__/compile.test.ts | 19 ++- .../__test__/fixture/headings/code-h1.mdx | 1 + .../fixture/headings/code-with-text-h1.mdx | 1 + .../nextra/__test__/fixture/headings/index.ts | 16 -- packages/nextra/package.json | 1 + packages/nextra/src/compile.ts | 11 +- packages/nextra/src/loader.ts | 32 ++-- packages/nextra/src/mdx-plugins/remark.ts | 17 +-- packages/nextra/src/types.ts | 1 - pnpm-lock.yaml | 38 +++++ 13 files changed, 223 insertions(+), 64 deletions(-) create mode 100644 .changeset/breezy-ghosts-argue.md create mode 100644 packages/nextra/__test__/fixture/headings/code-h1.mdx create mode 100644 packages/nextra/__test__/fixture/headings/code-with-text-h1.mdx delete mode 100644 packages/nextra/__test__/fixture/headings/index.ts diff --git a/.changeset/breezy-ghosts-argue.md b/.changeset/breezy-ghosts-argue.md new file mode 100644 index 0000000000..410710e29e --- /dev/null +++ b/.changeset/breezy-ghosts-argue.md @@ -0,0 +1,5 @@ +--- +'nextra': patch +--- + +fix(nextra): use `rehype-mdx-title` to determine page title diff --git a/packages/nextra-theme-blog/src/index.tsx b/packages/nextra-theme-blog/src/index.tsx index 8cfd51b4dd..c97aa21ab8 100644 --- a/packages/nextra-theme-blog/src/index.tsx +++ b/packages/nextra-theme-blog/src/index.tsx @@ -34,7 +34,6 @@ const BlogLayout = ({ config, contentNodes, opts }: LayoutProps) => { let navPages: PageMapItem[] = [] const type = opts.meta.type || 'post' const route = opts.route - const hasH1 = opts.hasH1 let back: string | null = null // This only renders once per page if (type === 'posts' || type === 'tag' || type === 'page') { @@ -93,8 +92,6 @@ const BlogLayout = ({ config, contentNodes, opts }: LayoutProps) => { const { theme, resolvedTheme } = useTheme() const { query } = router const tagName = type === 'tag' ? query.tag : null - const pageTitle = opts.meta.title || opts.titleText || '' - const mdxTitle = hasH1 && !pageTitle let comments if (config.cusdis) { @@ -168,6 +165,7 @@ const BlogLayout = ({ config, contentNodes, opts }: LayoutProps) => { ) : null const ref = React.useRef(null) + const pageTitle = opts.meta.title || opts.titleText || '' return ( <> @@ -186,7 +184,6 @@ const BlogLayout = ({ config, contentNodes, opts }: LayoutProps) => {
{pageTitle ?

{pageTitle}

: null} - {mdxTitle ?

: null} {type === 'post' ? ( /** @ts-expect-error */ diff --git a/packages/nextra/__test__/__snapshots__/compile.test.ts.snap b/packages/nextra/__test__/__snapshots__/compile.test.ts.snap index 3b8d3ce9e5..54eed9337f 100644 --- a/packages/nextra/__test__/__snapshots__/compile.test.ts.snap +++ b/packages/nextra/__test__/__snapshots__/compile.test.ts.snap @@ -1,8 +1,141 @@ // Vitest Snapshot v1 +exports[`process heading > code-h1 1`] = ` +{ + "headings": [ + { + "children": [ + { + "position": { + "end": { + "column": 16, + "line": 1, + "offset": 15, + }, + "start": { + "column": 3, + "line": 1, + "offset": 2, + }, + }, + "type": "inlineCode", + "value": "codegen.yml", + }, + ], + "depth": 1, + "position": { + "end": { + "column": 16, + "line": 1, + "offset": 15, + }, + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "heading", + "value": "codegen.yml", + }, + ], + "result": "/*@jsxRuntime automatic @jsxImportSource react*/ +import {useMDXComponents as _provideComponents} from \\"@mdx-js/react\\"; +export const titleText = \\"codegen.yml\\"; +function _createMdxContent(props) { + const _components = Object.assign({ + h1: \\"h1\\", + code: \\"code\\" + }, _provideComponents(), props.components); + return <_components.h1><_components.code>{\\"codegen.yml\\"}; +} +function MDXContent(props = {}) { + const {wrapper: MDXLayout} = Object.assign({}, _provideComponents(), props.components); + return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props); +} +export default MDXContent; +", + "structurizedData": {}, +} +`; + +exports[`process heading > code-with-text-h1 1`] = ` +{ + "headings": [ + { + "children": [ + { + "position": { + "end": { + "column": 16, + "line": 1, + "offset": 15, + }, + "start": { + "column": 3, + "line": 1, + "offset": 2, + }, + }, + "type": "inlineCode", + "value": "codegen.yml", + }, + { + "position": { + "end": { + "column": 21, + "line": 1, + "offset": 20, + }, + "start": { + "column": 16, + "line": 1, + "offset": 15, + }, + }, + "type": "text", + "value": " file", + }, + ], + "depth": 1, + "position": { + "end": { + "column": 21, + "line": 1, + "offset": 20, + }, + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "heading", + "value": "codegen.yml file", + }, + ], + "result": "/*@jsxRuntime automatic @jsxImportSource react*/ +import {useMDXComponents as _provideComponents} from \\"@mdx-js/react\\"; +export const titleText = \\"codegen.yml file\\"; +function _createMdxContent(props) { + const _components = Object.assign({ + h1: \\"h1\\", + code: \\"code\\" + }, _provideComponents(), props.components); + return <_components.h1><_components.code>{\\"codegen.yml\\"}{\\" file\\"}; +} +function MDXContent(props = {}) { + const {wrapper: MDXLayout} = Object.assign({}, _provideComponents(), props.components); + return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props); +} +export default MDXContent; +", + "structurizedData": {}, +} +`; + exports[`process heading > dynamic-h1 1`] = ` { - "hasH1": true, "headings": [ { "children": [ @@ -79,6 +212,7 @@ exports[`process heading > dynamic-h1 1`] = ` ], "result": "/*@jsxRuntime automatic @jsxImportSource react*/ import {useMDXComponents as _provideComponents} from \\"@mdx-js/react\\"; +export const titleText = \\"Posts Tagged with “”\\"; import {useRouter} from 'next/router'; export const TagName = () => { const {tag} = useRouter().query; @@ -102,7 +236,6 @@ export default MDXContent; exports[`process heading > no-h1 1`] = ` { - "hasH1": false, "headings": [ { "children": [ @@ -160,7 +293,6 @@ export default MDXContent; exports[`process heading > static-h1 1`] = ` { - "hasH1": true, "headings": [ { "children": [ @@ -200,6 +332,7 @@ exports[`process heading > static-h1 1`] = ` ], "result": "/*@jsxRuntime automatic @jsxImportSource react*/ import {useMDXComponents as _provideComponents} from \\"@mdx-js/react\\"; +export const titleText = \\"Hello World\\"; function _createMdxContent(props) { const _components = Object.assign({ h1: \\"h1\\" @@ -213,6 +346,5 @@ function MDXContent(props = {}) { export default MDXContent; ", "structurizedData": {}, - "titleText": "Hello World", } `; diff --git a/packages/nextra/__test__/compile.test.ts b/packages/nextra/__test__/compile.test.ts index 5016cb4868..ac8f146a93 100644 --- a/packages/nextra/__test__/compile.test.ts +++ b/packages/nextra/__test__/compile.test.ts @@ -3,7 +3,14 @@ import { it, describe, expect } from 'vitest' import path from 'path' import fs from 'fs/promises' -function loadFixture(name: string) { +type Name = + | 'code-h1.mdx' + | 'code-with-text-h1.mdx' + | 'dynamic-h1.mdx' + | 'no-h1.mdx' + | 'static-h1.mdx' + +function loadFixture(name: Name): string { const filePath = path.join( process.cwd(), '__test__', @@ -15,6 +22,16 @@ function loadFixture(name: string) { } describe('process heading', () => { + it('code-h1', async () => { + const data = await loadFixture('code-h1.mdx') + const result = await compileMdx(data) + expect(result).toMatchSnapshot() + }) + it('code-with-text-h1', async () => { + const data = await loadFixture('code-with-text-h1.mdx') + const result = await compileMdx(data) + expect(result).toMatchSnapshot() + }) it('static-h1', async () => { const data = await loadFixture('static-h1.mdx') const result = await compileMdx(data) diff --git a/packages/nextra/__test__/fixture/headings/code-h1.mdx b/packages/nextra/__test__/fixture/headings/code-h1.mdx new file mode 100644 index 0000000000..98eca62de6 --- /dev/null +++ b/packages/nextra/__test__/fixture/headings/code-h1.mdx @@ -0,0 +1 @@ +# `codegen.yml` diff --git a/packages/nextra/__test__/fixture/headings/code-with-text-h1.mdx b/packages/nextra/__test__/fixture/headings/code-with-text-h1.mdx new file mode 100644 index 0000000000..9fe3623d79 --- /dev/null +++ b/packages/nextra/__test__/fixture/headings/code-with-text-h1.mdx @@ -0,0 +1 @@ +# `codegen.yml` file diff --git a/packages/nextra/__test__/fixture/headings/index.ts b/packages/nextra/__test__/fixture/headings/index.ts deleted file mode 100644 index defdfe8aa7..0000000000 --- a/packages/nextra/__test__/fixture/headings/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import fs from 'fs/promises' -import path from 'path' - -type Name = 'static-h1.mdx' | 'dynamic-h1.mdx' | 'no-h1.mdx' - -export async function loadFixture(name: Name) { - const filePath = path.join( - process.cwd(), - 'src', - '__test__', - 'fixture', - 'headings', - name - ) - return fs.readFile(filePath, 'utf8') -} diff --git a/packages/nextra/package.json b/packages/nextra/package.json index 3fa771906d..369eef8d98 100644 --- a/packages/nextra/package.json +++ b/packages/nextra/package.json @@ -32,6 +32,7 @@ "github-slugger": "^1.4.0", "graceful-fs": "^4.2.10", "gray-matter": "^4.0.3", + "rehype-mdx-title": "^1.0.0", "rehype-pretty-code": "^0.1.0", "remark-gfm": "^3.0.1", "shiki": "0.10.1", diff --git a/packages/nextra/src/compile.ts b/packages/nextra/src/compile.ts index 3ed9d1af57..07647f9e1c 100644 --- a/packages/nextra/src/compile.ts +++ b/packages/nextra/src/compile.ts @@ -1,8 +1,9 @@ import { createProcessor, ProcessorOptions } from '@mdx-js/mdx' import remarkGfm from 'remark-gfm' import rehypePrettyCode from 'rehype-pretty-code' +import { rehypeMdxTitle } from 'rehype-mdx-title'; import { remarkStaticImage } from './mdx-plugins/static-image' -import remarkHandler, { HeadingMeta } from './mdx-plugins/remark' +import { remarkHeadings, HeadingMeta } from './mdx-plugins/remark' import { LoaderOptions } from './types' import structurize from './mdx-plugins/structurize' import { parseMeta, attachMeta } from './mdx-plugins/rehype-handler' @@ -13,7 +14,6 @@ import theme from './theme.json' const createCompiler = (mdxOptions: ProcessorOptions) => { const compiler = createProcessor(mdxOptions) compiler.data('headingMeta', { - hasH1: false, headings: [] }) return compiler @@ -50,9 +50,9 @@ export async function compileMdx( LoaderOptions, 'unstable_staticImage' | 'unstable_flexsearch' > = {}, - resourcePath: string + resourcePath = '' ) { - let structurizedData = {} + const structurizedData = {} const compiler = createCompiler({ jsx: mdxOptions.jsx ?? true, outputFormat: mdxOptions.outputFormat, @@ -60,7 +60,7 @@ export async function compileMdx( remarkPlugins: [ ...(mdxOptions.remarkPlugins || []), remarkGfm, - remarkHandler, + remarkHeadings, ...(nextraOptions.unstable_staticImage ? [remarkStaticImage] : []), ...(nextraOptions.unstable_flexsearch ? [structurize(structurizedData, nextraOptions.unstable_flexsearch)] @@ -74,6 +74,7 @@ export async function compileMdx( rehypePrettyCode, { ...rehypePrettyCodeOptions, ...mdxOptions.rehypePrettyCodeOptions } ], + [rehypeMdxTitle, { name: 'titleText' }], attachMeta ].filter(Boolean) }) diff --git a/packages/nextra/src/loader.ts b/packages/nextra/src/loader.ts index a1c8cfbe24..4d3f410fe0 100644 --- a/packages/nextra/src/loader.ts +++ b/packages/nextra/src/loader.ts @@ -14,7 +14,7 @@ import { collectFiles } from './plugin' import { MARKDOWN_EXTENSION_REGEX, IS_PRODUCTION } from './constants' // TODO: create this as a webpack plugin. -const indexContentEmitted = new Set() +const indexContentEmitted = new Set() const pagesDir = path.resolve(findPagesDir()) @@ -93,7 +93,7 @@ async function loader( } // Extract frontMatter information if it exists - let { data, content } = grayMatter(source) + const { data, content } = grayMatter(source) let layout = theme let layoutConfig = themeConfig || null @@ -110,17 +110,15 @@ async function loader( unstable_flexsearch = false } - const { result, titleText, headings, hasH1, structurizedData } = - await compileMdx( - content, - mdxOptions, - { - unstable_staticImage, - unstable_flexsearch - }, - resourcePath - ) - content = result.replace('export default MDXContent;', '') + const { result, headings, structurizedData } = await compileMdx( + content, + mdxOptions, + { + unstable_staticImage, + unstable_flexsearch + }, + resourcePath + ) if (unstable_flexsearch) { // We only add .MD and .MDX contents @@ -171,6 +169,7 @@ async function loader( import __nextra_withLayout__ from '${layout}' import { withSSG as __nextra_withSSG__ } from 'nextra/ssg' ${layoutConfigImport} +${result.replace('export default MDXContent;', '')} const __nextra_pageMap__ = ${JSON.stringify(pageMap)} @@ -184,9 +183,8 @@ const NextraLayout = __nextra_withSSG__(__nextra_withLayout__({ route: "${slash(route)}", meta: ${JSON.stringify(data)}, pageMap: __nextra_pageMap__, - titleText: ${JSON.stringify(titleText)}, + titleText: typeof titleText === 'string' ? titleText : undefined, headings: ${JSON.stringify(headings)}, - hasH1: ${JSON.stringify(hasH1)}, ${timestamp ? `timestamp: ${timestamp},\n` : ''} ${ unstable_flexsearch @@ -195,8 +193,6 @@ const NextraLayout = __nextra_withSSG__(__nextra_withLayout__({ } }, ${layoutConfig ? '__nextra_layoutConfig__' : 'null'})) -${content} - function NextraPage(props) { return ( @@ -214,7 +210,7 @@ export default function syncLoader( this: LoaderContext, source: string, callback: (err?: null | Error, content?: string | Buffer) => void -) { +): void { loader(this, source) .then(result => callback(null, result)) .catch(err => callback(err)) diff --git a/packages/nextra/src/mdx-plugins/remark.ts b/packages/nextra/src/mdx-plugins/remark.ts index 84392ba31e..d186909da0 100644 --- a/packages/nextra/src/mdx-plugins/remark.ts +++ b/packages/nextra/src/mdx-plugins/remark.ts @@ -2,8 +2,6 @@ import { Processor } from '@mdx-js/mdx/lib/core' import { Root, Heading, Parent } from 'mdast' export interface HeadingMeta { - titleText?: string - hasH1: boolean headings: Heading[] } @@ -32,7 +30,7 @@ export function getFlattenedValue(node: Parent): string { .join('') } -export default function remarkHeadings(this: Processor) { +export function remarkHeadings(this: Processor) { const data = this.data() as any return (tree: Root, _file: any, done: () => void) => { visit( @@ -51,18 +49,7 @@ export default function remarkHeadings(this: Processor) { ...(node as Heading), value: getFlattenedValue(node) } - const headingMeta = data.headingMeta as HeadingMeta - if (node.depth === 1) { - headingMeta.hasH1 = true - if (Array.isArray(node.children) && node.children.length === 1) { - const child = node.children[0] - if (child.type === 'text') { - headingMeta.titleText = child.value - } - } - } - - headingMeta.headings.push(heading) + data.headingMeta.headings.push(heading) } else if (node.name === 'summary' || node.name === 'details') { // Replace the and
with customized components. if (node.data) { diff --git a/packages/nextra/src/types.ts b/packages/nextra/src/types.ts index 53c458af64..53dbd21055 100644 --- a/packages/nextra/src/types.ts +++ b/packages/nextra/src/types.ts @@ -32,7 +32,6 @@ export interface PageOpt { pageMap: PageMapItem[] titleText: string | null headings?: Heading[] - hasH1: boolean unstable_flexsearch?: | boolean | { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3b3f7736c..ff5cdad254 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,7 @@ importers: github-slugger: ^1.4.0 graceful-fs: ^4.2.10 gray-matter: ^4.0.3 + rehype-mdx-title: ^1.0.0 rehype-pretty-code: ^0.1.0 remark-gfm: ^3.0.1 shiki: 0.10.1 @@ -117,6 +118,7 @@ importers: github-slugger: 1.4.0 graceful-fs: 4.2.10 gray-matter: 4.0.3 + rehype-mdx-title: 1.0.0 rehype-pretty-code: 0.1.0_shiki@0.10.1 remark-gfm: 3.0.1 shiki: 0.10.1 @@ -2235,6 +2237,10 @@ packages: estree-walker: 3.0.1 dev: false + /estree-util-is-identifier-name/1.1.0: + resolution: {integrity: sha512-OVJZ3fGGt9By77Ix9NhaRbzfbDV/2rx9EP7YIDJTmsZSEc5kYn2vWcNccYyahJL2uAQZK2a5Or2i0wtIKTPoRQ==} + dev: false + /estree-util-is-identifier-name/2.0.1: resolution: {integrity: sha512-rxZj1GkQhY4x1j/CSnybK9cGuMFQYFPLq0iNyopqf14aOVLFtMv7Esika+ObJWPWiOHuMOAHz3YkWoLYYRnzWQ==} dev: false @@ -2569,6 +2575,10 @@ packages: - supports-color dev: false + /hast-util-to-string/1.0.4: + resolution: {integrity: sha512-eK0MxRX47AV2eZ+Lyr18DCpQgodvaS3fAQO2+b9Two9F5HEoRPhiUMNzoXArMJfZi2yieFzUBMRl3HNJ3Jus3w==} + dev: false + /hast-util-whitespace/2.0.0: resolution: {integrity: sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==} dev: false @@ -4569,6 +4579,15 @@ packages: functions-have-names: 1.2.3 dev: true + /rehype-mdx-title/1.0.0: + resolution: {integrity: sha512-5B/53Y+KQHm4/nrE6pIIPc9Ie2fbPMCLs8WwMGYWWHr+5g3TkmEijRkr8TGYHULtc+C7bOoPR8LIF5DpGROIDg==} + engines: {node: '>=12.2.0'} + dependencies: + estree-util-is-identifier-name: 1.1.0 + hast-util-to-string: 1.0.4 + unist-util-visit: 2.0.3 + dev: false + /rehype-pretty-code/0.1.0_shiki@0.10.1: resolution: {integrity: sha512-7SaedCOn5VcDGdllQeu3kBWC+/zbkD6ZE7iOALJzK79+v0K0qQpa1WqBwoxetTbCLwBTY9wC4CabEsDS3qj55g==} engines: {node: ^12.16.0 || >=13.2.0} @@ -5402,6 +5421,10 @@ packages: resolution: {integrity: sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==} dev: false + /unist-util-is/4.1.0: + resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + dev: false + /unist-util-is/5.1.1: resolution: {integrity: sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==} dev: false @@ -5431,6 +5454,13 @@ packages: '@types/unist': 2.0.6 dev: false + /unist-util-visit-parents/3.1.1: + resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + dependencies: + '@types/unist': 2.0.6 + unist-util-is: 4.1.0 + dev: false + /unist-util-visit-parents/5.1.0: resolution: {integrity: sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==} dependencies: @@ -5438,6 +5468,14 @@ packages: unist-util-is: 5.1.1 dev: false + /unist-util-visit/2.0.3: + resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} + dependencies: + '@types/unist': 2.0.6 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + dev: false + /unist-util-visit/4.1.0: resolution: {integrity: sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==} dependencies: