From 530996755e8c23e9e0c931e9d4459dfdb038d613 Mon Sep 17 00:00:00 2001 From: JounQin Date: Tue, 17 Dec 2024 12:20:01 +0800 Subject: [PATCH] feat: display title in frontmatter as heading fallback (#1556) --- e2e/fixtures/heading-title/doc/guide.md | 3 ++ e2e/fixtures/heading-title/package.json | 16 +++++++++ e2e/fixtures/heading-title/rspress.config.ts | 6 ++++ e2e/fixtures/heading-title/tsconfig.json | 1 + e2e/tests/heading-title.test.ts | 36 +++++++++++++++++++ packages/core/src/node/mdx/loader.ts | 3 ++ packages/core/src/runtime/App.tsx | 1 + .../docs/en/api/config/config-theme.mdx | 17 +++++++++ .../docs/zh/api/config/config-theme.mdx | 18 ++++++++++ packages/shared/src/types/defaultTheme.ts | 5 +++ packages/shared/src/types/index.ts | 1 + packages/theme-default/package.json | 1 + .../src/layout/DocLayout/index.tsx | 24 +++++++++++-- pnpm-lock.yaml | 13 +++++++ 14 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 e2e/fixtures/heading-title/doc/guide.md create mode 100644 e2e/fixtures/heading-title/package.json create mode 100644 e2e/fixtures/heading-title/rspress.config.ts create mode 100644 e2e/fixtures/heading-title/tsconfig.json create mode 100644 e2e/tests/heading-title.test.ts diff --git a/e2e/fixtures/heading-title/doc/guide.md b/e2e/fixtures/heading-title/doc/guide.md new file mode 100644 index 000000000..e2fd037c4 --- /dev/null +++ b/e2e/fixtures/heading-title/doc/guide.md @@ -0,0 +1,3 @@ +--- +title: Heading Title +--- diff --git a/e2e/fixtures/heading-title/package.json b/e2e/fixtures/heading-title/package.json new file mode 100644 index 000000000..a91edaa3e --- /dev/null +++ b/e2e/fixtures/heading-title/package.json @@ -0,0 +1,16 @@ +{ + "name": "@rspress-fixture/rspress-heading-title", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "rspress dev", + "build": "rspress build", + "preview": "rspress preview" + }, + "dependencies": { + "rspress": "workspace:*" + }, + "devDependencies": { + "@types/node": "^18.11.17" + } +} diff --git a/e2e/fixtures/heading-title/rspress.config.ts b/e2e/fixtures/heading-title/rspress.config.ts new file mode 100644 index 000000000..0731c0907 --- /dev/null +++ b/e2e/fixtures/heading-title/rspress.config.ts @@ -0,0 +1,6 @@ +import * as path from 'node:path'; +import { defineConfig } from 'rspress/config'; + +export default defineConfig({ + root: path.join(__dirname, 'doc'), +}); diff --git a/e2e/fixtures/heading-title/tsconfig.json b/e2e/fixtures/heading-title/tsconfig.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/e2e/fixtures/heading-title/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/e2e/tests/heading-title.test.ts b/e2e/tests/heading-title.test.ts new file mode 100644 index 000000000..cb3806500 --- /dev/null +++ b/e2e/tests/heading-title.test.ts @@ -0,0 +1,36 @@ +import { expect, test } from '@playwright/test'; +import path from 'node:path'; +import { getPort, killProcess, runDevCommand } from '../utils/runCommands'; + +const fixtureDir = path.resolve(__dirname, '../fixtures'); + +test.describe('heading-title test', async () => { + let appPort; + let app; + test.beforeAll(async () => { + const appDir = path.join(fixtureDir, 'heading-title'); + appPort = await getPort(); + app = await runDevCommand(appDir, appPort); + }); + + test.afterAll(async () => { + if (app) { + await killProcess(app); + } + }); + + test('Guide page', async ({ page }) => { + await page.goto(`http://localhost:${appPort}/guide`, { + waitUntil: 'networkidle', + }); + const h1 = await page.$('h1'); + const className = await page.evaluate(h1 => h1?.className, h1); + expect(className).toContain('title_3b154'); // hash in css module should stable + const text = await page.evaluate(h1 => h1?.textContent, h1); + expect(text).toContain('Heading Title'); + expect(await page.evaluate(h1 => h1?.id, h1)).toBe('heading-title'); + expect(await page.evaluate(link => link?.hash, await h1?.$('a'))).toBe( + '#heading-title', + ); + }); +}); diff --git a/packages/core/src/node/mdx/loader.ts b/packages/core/src/node/mdx/loader.ts index cbcf158e2..4e8a780c2 100644 --- a/packages/core/src/node/mdx/loader.ts +++ b/packages/core/src/node/mdx/loader.ts @@ -33,6 +33,7 @@ interface LoaderOptions { export interface PageMeta { toc: TocItem[]; title: string; + headingTitle: string; frontmatter?: Record; } @@ -167,6 +168,7 @@ export default async function mdxLoader( pageMeta = { ...compilationMeta, title: frontmatter.title || compilationMeta.title || '', + headingTitle: compilationMeta.title, frontmatter, } as PageMeta; } else { @@ -183,6 +185,7 @@ export default async function mdxLoader( pageMeta = { toc, title: frontmatter.title || title || '', + headingTitle: title, frontmatter, }; // We should check dead links in mdx-rs mode diff --git a/packages/core/src/runtime/App.tsx b/packages/core/src/runtime/App.tsx index 65df7820f..726fffc52 100644 --- a/packages/core/src/runtime/App.tsx +++ b/packages/core/src/runtime/App.tsx @@ -25,6 +25,7 @@ export enum QueryStatus { type PageMeta = { title: string; + headingTitle: string; toc: Header[]; frontmatter: Record; }; diff --git a/packages/document/docs/en/api/config/config-theme.mdx b/packages/document/docs/en/api/config/config-theme.mdx index 2553ebd09..d75784785 100644 --- a/packages/document/docs/en/api/config/config-theme.mdx +++ b/packages/document/docs/en/api/config/config-theme.mdx @@ -687,3 +687,20 @@ export default defineConfig({ }, }); ``` + +## fallbackHeadingTitle + +- Type: `boolean` +- Default: `true` + +Whether to display [`frontmatter.title`](./config-frontmatter#title) as fallback when the heading title is not presented. For example: + +```ts title="rspress.config.ts" +import { defineConfig } from 'rspress/config'; + +export default defineConfig({ + themeConfig: { + fallbackHeadingTitle: false, + }, +}); +``` diff --git a/packages/document/docs/zh/api/config/config-theme.mdx b/packages/document/docs/zh/api/config/config-theme.mdx index 07476dda3..556fb595d 100644 --- a/packages/document/docs/zh/api/config/config-theme.mdx +++ b/packages/document/docs/zh/api/config/config-theme.mdx @@ -673,3 +673,21 @@ export default defineConfig({ }, }); ``` + +## fallbackHeadingTitle + +- Type: `boolean` +- Default: `true` + +是否在文档标题未提供时将 [`frontmatter.title`](./config-frontmatter#title) 作为后备内容。比如: + + +```ts title="rspress.config.ts" +import { defineConfig } from 'rspress/config'; + +export default defineConfig({ + themeConfig: { + fallbackHeadingTitle: false, + }, +}); +``` diff --git a/packages/shared/src/types/defaultTheme.ts b/packages/shared/src/types/defaultTheme.ts index 3f12ab23a..66e26794b 100644 --- a/packages/shared/src/types/defaultTheme.ts +++ b/packages/shared/src/types/defaultTheme.ts @@ -115,6 +115,11 @@ export interface Config { * @default 'auto' */ localeRedirect?: 'auto' | 'never'; + /** + * Whether to show the fallback heading title when the heading title is not presented but `frontmatter.title` exists + * @default true + */ + fallbackHeadingTitle?: boolean; } /** diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 80ebd8bf0..530ef98d0 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -306,6 +306,7 @@ export interface FrontMatterMeta { export interface PageData { siteData: SiteData; page: BaseRuntimePageInfo & { + headingTitle?: string; pagePath: string; lastUpdatedTime?: string; description?: string; diff --git a/packages/theme-default/package.json b/packages/theme-default/package.json index 7638ddfc9..834612b99 100644 --- a/packages/theme-default/package.json +++ b/packages/theme-default/package.json @@ -44,6 +44,7 @@ "htmr": "^1.0.2", "lodash-es": "^4.17.21", "nprogress": "^0.2.0", + "github-slugger": "^2.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet-async": "^1.3.0", diff --git a/packages/theme-default/src/layout/DocLayout/index.tsx b/packages/theme-default/src/layout/DocLayout/index.tsx index b21d573f3..253b564f3 100644 --- a/packages/theme-default/src/layout/DocLayout/index.tsx +++ b/packages/theme-default/src/layout/DocLayout/index.tsx @@ -1,5 +1,6 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { MDXProvider } from '@mdx-js/react'; +import { slug } from 'github-slugger'; import { getCustomMDXComponent, ScrollToTop, Overview } from '@theme'; import { Content, usePageData, NoSSR } from '@rspress/runtime'; import { Aside } from '../../components/Aside'; @@ -9,6 +10,8 @@ import { SideMenu } from '../../components/LocalSideBar'; import { TabDataContext } from '../../logic/TabDataContext'; import styles from './index.module.scss'; import type { UISwitchResult } from '../../logic/useUISwitch'; +import { H1 } from './docComponents/title'; +import { A } from './docComponents/link'; export interface DocLayoutProps { beforeSidebar?: React.ReactNode; @@ -43,7 +46,7 @@ export function DocLayout(props: DocLayoutProps) { components, } = props; const { siteData, page } = usePageData(); - const { toc = [], frontmatter } = page; + const { headingTitle, title, toc = [], frontmatter } = page; const [tabData, setTabData] = useState({}); const headers = toc; const { themeConfig } = siteData; @@ -64,6 +67,22 @@ export function DocLayout(props: DocLayoutProps) { ); + const fallbackTitle = useMemo(() => { + const titleSlug = title && slug(title); + return ( + siteData.themeConfig.fallbackHeadingTitle !== false && + !headingTitle && + titleSlug && ( +

+ {title} + + # + +

+ ) + ); + }, [headingTitle, title]); + return (
{beforeDocContent} + {fallbackTitle} {docContent} {afterDocContent}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dd42b84b..9e3faeeab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -216,6 +216,16 @@ importers: specifier: ^18.11.17 version: 18.11.17 + e2e/fixtures/heading-title: + dependencies: + rspress: + specifier: workspace:* + version: link:../../../packages/cli + devDependencies: + '@types/node': + specifier: ^18.11.17 + version: 18.11.17 + e2e/fixtures/i18n: dependencies: rspress: @@ -1497,6 +1507,9 @@ importers: flexsearch: specifier: 0.7.43 version: 0.7.43 + github-slugger: + specifier: ^2.0.0 + version: 2.0.0 htmr: specifier: ^1.0.2 version: 1.0.2(react@18.3.1)