diff --git a/e2e/tests/custom-headers.test.ts b/e2e/tests/custom-headers.test.ts index 7ca3a4f096..df0b7a1652 100644 --- a/e2e/tests/custom-headers.test.ts +++ b/e2e/tests/custom-headers.test.ts @@ -90,7 +90,10 @@ test.describe('custom headers', async () => { const htmlContent = await page.content(); expect(htmlContent).toContain( - '', + '', + ); + expect(htmlContent).toContain( + '', ); }); }); diff --git a/e2e/tests/title-suffix.test.ts b/e2e/tests/title-suffix.test.ts index 3008f09d89..059b826c03 100644 --- a/e2e/tests/title-suffix.test.ts +++ b/e2e/tests/title-suffix.test.ts @@ -10,12 +10,12 @@ test('title suffix', async () => { expect( ( await readFile(path.join(appDir, 'doc_build/index.html'), 'utf-8') - ).includes('Default Title - Index Suffix'), + ).includes('Default Title - Index Suffix'), ).toBeTruthy(); expect( (await readFile(path.join(appDir, 'doc_build/foo.html'), 'utf-8')).includes( - 'Foo | Foo Suffix', + 'Foo | Foo Suffix', ), ).toBeTruthy(); }); diff --git a/packages/core/package.json b/packages/core/package.json index 013744a81a..2829ed7591 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -47,7 +47,6 @@ "reset": "rimraf ./**/node_modules" }, "dependencies": { - "@dr.pogodin/react-helmet": "2.0.4", "@mdx-js/loader": "^3.1.0", "@mdx-js/mdx": "^3.1.0", "@mdx-js/react": "^3.1.0", @@ -61,6 +60,7 @@ "@rspress/runtime": "workspace:*", "@rspress/shared": "workspace:*", "@rspress/theme-default": "workspace:*", + "@unhead/react": "^2.0.0", "enhanced-resolve": "5.18.1", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", diff --git a/packages/core/src/node/constants.ts b/packages/core/src/node/constants.ts index 2d3c67fe91..e5adc035e2 100644 --- a/packages/core/src/node/constants.ts +++ b/packages/core/src/node/constants.ts @@ -57,8 +57,6 @@ export const OUTPUT_DIR = 'doc_build'; export const APP_HTML_MARKER = ''; export const HEAD_MARKER = ''; export const META_GENERATOR = ''; -export const HTML_START_TAG = ' Promise<{ appHtml: string; pageData: PageData }>; routes: Route[]; } @@ -84,12 +87,12 @@ export async function renderPages( return !route.routePath.includes(':'); }) .map(async route => { - const helmetContext = new HelmetData({}); + const head = createHead(); const { routePath } = route; let appHtml = ''; if (render) { try { - ({ appHtml } = await render(routePath, helmetContext.context)); + ({ appHtml } = await render(routePath, head)); } catch (e) { logger.error( `Page "${picocolors.yellow(routePath)}" SSG rendering failed.`, @@ -99,8 +102,7 @@ export async function renderPages( } } - const { helmet } = helmetContext.context; - let html = htmlTemplate + const replacedHtmlTemplate = htmlTemplate // During ssr, we already have the title in react-helmet .replace(/(.*?)<\/title>/gi, '') // Don't use `string` as second param @@ -115,27 +117,10 @@ export async function renderPages( HEAD_MARKER, [ await renderConfigHead(config, route), - helmet.title.toString(), - helmet.meta.toString(), - helmet.link.toString(), - helmet.style.toString(), - helmet.script.toString(), await renderFrontmatterHead(route), ].join(''), ); - if (helmet.htmlAttributes) { - html = html.replace( - HTML_START_TAG, - `${HTML_START_TAG} ${helmet.htmlAttributes?.toString()}`, - ); - } - - if (helmet.bodyAttributes) { - html = html.replace( - BODY_START_TAG, - `${BODY_START_TAG} ${helmet.bodyAttributes?.toString()}`, - ); - } + const html = await transformHtmlTemplate(head, replacedHtmlTemplate); const normalizeHtmlFilePath = (path: string) => { const normalizedBase = `${normalizeSlash(config?.base || '/')}/`; diff --git a/packages/core/src/runtime/App.tsx b/packages/core/src/runtime/App.tsx index e725c9e53c..567483736c 100644 --- a/packages/core/src/runtime/App.tsx +++ b/packages/core/src/runtime/App.tsx @@ -1,4 +1,3 @@ -import { HelmetProvider } from '@dr.pogodin/react-helmet'; import { DataContext, useLocation } from '@rspress/runtime'; import { Layout } from '@theme'; import React, { useContext, useLayoutEffect } from 'react'; @@ -11,7 +10,7 @@ enum QueryStatus { Hide = '0', } -export function App({ helmetContext }: { helmetContext?: object }) { +export function App() { const { setData: setPageData, data } = useContext(DataContext); const { pathname, search } = useLocation(); useLayoutEffect(() => { @@ -41,7 +40,7 @@ export function App({ helmetContext }: { helmetContext?: object }) { query.get(GLOBAL_COMPONENTS_KEY) === QueryStatus.Hide; return ( - <HelmetProvider context={helmetContext}> + <> <Layout /> { // Global UI @@ -64,6 +63,6 @@ export function App({ helmetContext }: { helmetContext?: object }) { }); }) } - </HelmetProvider> + </> ); } diff --git a/packages/core/src/runtime/ClientApp.tsx b/packages/core/src/runtime/ClientApp.tsx index f749d3a452..f657356786 100644 --- a/packages/core/src/runtime/ClientApp.tsx +++ b/packages/core/src/runtime/ClientApp.tsx @@ -1,9 +1,12 @@ import { BrowserRouter, DataContext, ThemeContext } from '@rspress/runtime'; import type { PageData } from '@rspress/shared'; import { useThemeState } from '@theme'; +import { UnheadProvider, createHead } from '@unhead/react/client'; import { useMemo, useState } from 'react'; import { App } from './App'; +const head = createHead(); + // eslint-disable-next-line import/no-commonjs export function ClientApp({ @@ -20,7 +23,9 @@ export function ClientApp({ value={useMemo(() => ({ data, setData }), [data, setData])} > <BrowserRouter> - <App /> + <UnheadProvider head={head}> + <App /> + </UnheadProvider> </BrowserRouter> </DataContext.Provider> </ThemeContext.Provider> diff --git a/packages/core/src/runtime/ssrServerEntry.tsx b/packages/core/src/runtime/ssrServerEntry.tsx index 71b85b5b75..853702a2d1 100644 --- a/packages/core/src/runtime/ssrServerEntry.tsx +++ b/packages/core/src/runtime/ssrServerEntry.tsx @@ -1,7 +1,9 @@ import { DataContext, ThemeContext } from '@rspress/runtime'; import { StaticRouter } from '@rspress/runtime/server'; import type { PageData } from '@rspress/shared'; +import { type Unhead, UnheadProvider } from '@unhead/react/server'; import { renderToString } from 'react-dom/server'; + import { App } from './App'; import { initPageData } from './initPageData'; @@ -9,7 +11,7 @@ const DEFAULT_THEME = 'light'; export async function render( pagePath: string, - helmetContext: object, + head: Unhead, ): Promise<{ appHtml: string; pageData: PageData }> { const initialPageData = await initPageData(pagePath); @@ -17,7 +19,9 @@ export async function render( <ThemeContext.Provider value={{ theme: DEFAULT_THEME }}> <DataContext.Provider value={{ data: initialPageData }}> <StaticRouter location={pagePath}> - <App helmetContext={helmetContext} /> + <UnheadProvider value={head}> + <App /> + </UnheadProvider> </StaticRouter> </DataContext.Provider> </ThemeContext.Provider>, diff --git a/packages/plugin-rss/static/global-components/FeedsAnnotations.tsx b/packages/plugin-rss/static/global-components/FeedsAnnotations.tsx index 02c71e091c..4c0976c565 100644 --- a/packages/plugin-rss/static/global-components/FeedsAnnotations.tsx +++ b/packages/plugin-rss/static/global-components/FeedsAnnotations.tsx @@ -1,14 +1,14 @@ /// <reference path="../../index.d.ts" /> import type { LinkHTMLAttributes } from 'react'; -import { Helmet, usePageData } from 'rspress/runtime'; +import { Head, usePageData } from 'rspress/runtime'; export default function FeedsAnnotations() { const { page } = usePageData(); const feeds = page.feeds || []; return ( - <Helmet> + <Head> {feeds.map(({ language, url, mime }) => { const props: LinkHTMLAttributes<HTMLLinkElement> = { rel: 'alternate', @@ -21,6 +21,6 @@ export default function FeedsAnnotations() { // biome-ignore lint/correctness/useJsxKeyInIterable: no key props return <link {...props} />; })} - </Helmet> + </Head> ); } diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 5141562cd5..1bd847df2c 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -41,8 +41,8 @@ "reset": "rimraf ./**/node_modules" }, "dependencies": { - "@dr.pogodin/react-helmet": "2.0.4", "@rspress/shared": "workspace:*", + "@unhead/react": "^2.0.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^6.29.0" diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 432722541f..3fffa2c3b3 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -37,5 +37,5 @@ export { pathnameToRouteService, normalizeRoutePath, } from './route'; -export { Helmet } from '@dr.pogodin/react-helmet'; +export { Head } from '@unhead/react'; export { NoSSR } from './NoSSR'; diff --git a/packages/theme-default/package.json b/packages/theme-default/package.json index 64b0bce3f8..d58d010174 100644 --- a/packages/theme-default/package.json +++ b/packages/theme-default/package.json @@ -44,10 +44,10 @@ "reset": "rimraf ./**/node_modules" }, "dependencies": { - "@dr.pogodin/react-helmet": "2.0.4", "@mdx-js/react": "2.3.0", "@rspress/runtime": "workspace:*", "@rspress/shared": "workspace:*", + "@unhead/react": "^2.0.0", "body-scroll-lock": "4.0.0-beta.0", "copy-to-clipboard": "^3.3.3", "flexsearch": "0.7.43", diff --git a/packages/theme-default/src/layout/Layout/index.tsx b/packages/theme-default/src/layout/Layout/index.tsx index 858956e3e3..ade29f2488 100644 --- a/packages/theme-default/src/layout/Layout/index.tsx +++ b/packages/theme-default/src/layout/Layout/index.tsx @@ -1,12 +1,13 @@ import 'nprogress/nprogress.css'; import '../../styles'; -import { Helmet } from '@dr.pogodin/react-helmet'; import { Content, usePageData } from '@rspress/runtime'; import { HomeLayout as DefaultHomeLayout, NotFoundLayout as DefaultNotFoundLayout, Nav, } from '@theme'; +import { useHead } from '@unhead/react'; +import { Head } from '@unhead/react'; import type React from 'react'; import type { NavProps } from '../../components/Nav'; import { useSetup } from '../../logic/sideEffects'; @@ -155,16 +156,18 @@ export function Layout(props: LayoutProps) { } }; + useHead({ + htmlAttrs: { + lang: currentLang || 'en', + }, + }); + return ( <> - <Helmet - htmlAttributes={{ - lang: currentLang || 'en', - }} - > + <Head> {title ? <title>{title} : null} {description ? : null} - + {top} {pageType !== 'blank' && uiSwitch.showNavbar && ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d539d97e29..6e507d1e1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -719,9 +719,6 @@ importers: packages/core: dependencies: - '@dr.pogodin/react-helmet': - specifier: 2.0.4 - version: 2.0.4(react@19.1.0) '@mdx-js/loader': specifier: ^3.1.0 version: 3.1.0(acorn@8.14.0)(webpack@5.99.3) @@ -761,6 +758,9 @@ importers: '@rspress/theme-default': specifier: workspace:* version: link:../theme-default + '@unhead/react': + specifier: ^2.0.0 + version: 2.0.8(react@19.1.0) enhanced-resolve: specifier: 5.18.1 version: 5.18.1 @@ -1613,12 +1613,12 @@ importers: packages/runtime: dependencies: - '@dr.pogodin/react-helmet': - specifier: 2.0.4 - version: 2.0.4(react@19.1.0) '@rspress/shared': specifier: workspace:* version: link:../shared + '@unhead/react': + specifier: ^2.0.0 + version: 2.0.8(react@19.1.0) react: specifier: ^19.1.0 version: 19.1.0 @@ -1702,9 +1702,6 @@ importers: packages/theme-default: dependencies: - '@dr.pogodin/react-helmet': - specifier: 2.0.4 - version: 2.0.4(react@19.1.0) '@mdx-js/react': specifier: 2.3.0 version: 2.3.0(react@19.1.0) @@ -1714,6 +1711,9 @@ importers: '@rspress/shared': specifier: workspace:* version: link:../shared + '@unhead/react': + specifier: ^2.0.0 + version: 2.0.8(react@19.1.0) body-scroll-lock: specifier: 4.0.0-beta.0 version: 4.0.0-beta.0 @@ -2532,11 +2532,6 @@ packages: search-insights: optional: true - '@dr.pogodin/react-helmet@2.0.4': - resolution: {integrity: sha512-NXSgzBKiyvHF4UvR40fKRB0gTIlezfnyvmTqJKZy5Gbtv23SXMuneZbtovvG/sKxbOYPVn1lZl211bTKhd5g4w==} - peerDependencies: - react: '19' - '@emnapi/core@1.3.0': resolution: {integrity: sha512-9hRqVlhwqBqCoToZ3hFcNVqL+uyHV06Y47ax4UB8L6XgVRqYz7MFnfessojo6+5TK89pKwJnpophwjTMOeKI9Q==} @@ -3519,6 +3514,11 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@unhead/react@2.0.8': + resolution: {integrity: sha512-H/DmGG2Nz2OU3ASEZzLOIlwzQl027yOl0YhnlLEu3y6pvV/myLtgogcb68hXyHAtmpMAfnxhivUxCaiuFW7C6w==} + peerDependencies: + react: '>=18' + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -4582,10 +4582,6 @@ packages: resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} engines: {node: '>=8'} - get-stdin@9.0.0: - resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} - engines: {node: '>=12'} - get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -4762,6 +4758,9 @@ packages: highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} @@ -4856,9 +4855,6 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} - invariant@2.2.4: - resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - is-absolute-url@4.0.1: resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5153,10 +5149,6 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} @@ -5986,9 +5978,6 @@ packages: peerDependencies: react: ^19.1.0 - react-fast-compare@3.2.2: - resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} @@ -6442,9 +6431,6 @@ packages: resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==} engines: {node: '>=11.0'} - shallowequal@1.1.0: - resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -6828,6 +6814,9 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + unhead@2.0.8: + resolution: {integrity: sha512-63WR+y08RZE7ChiFdgNY64haAkhCtUS5/HM7xo4Q83NA63txWbEh2WGmrKbArdQmSct+XlqbFN8ZL1yWpQEHEA==} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -8122,13 +8111,6 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@dr.pogodin/react-helmet@2.0.4(react@19.1.0)': - dependencies: - invariant: 2.2.4 - react: 19.1.0 - react-fast-compare: 3.2.2 - shallowequal: 1.1.0 - '@emnapi/core@1.3.0': dependencies: '@emnapi/wasi-threads': 1.0.1 @@ -9192,6 +9174,11 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@unhead/react@2.0.8(react@19.1.0)': + dependencies: + react: 19.1.0 + unhead: 2.0.8 + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -10381,8 +10368,6 @@ snapshots: get-port@5.1.1: {} - get-stdin@9.0.0: {} - get-stream@8.0.1: {} git-hooks-list@4.1.1: {} @@ -10687,6 +10672,8 @@ snapshots: highlightjs-vue@1.0.0: {} + hookable@5.5.3: {} + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 @@ -10769,10 +10756,6 @@ snapshots: inline-style-parser@0.2.4: {} - invariant@2.2.4: - dependencies: - loose-envify: 1.4.0 - is-absolute-url@4.0.1: {} is-absolute@1.0.0: @@ -11054,10 +11037,6 @@ snapshots: longest-streak@3.1.0: {} - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - loupe@3.1.2: {} lower-case@2.0.2: @@ -12349,8 +12328,6 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 - react-fast-compare@3.2.2: {} - react-is@18.2.0: {} react-lazy-with-preload@2.2.1: {} @@ -12888,8 +12865,6 @@ snapshots: is-plain-object: 2.0.4 is-primitive: 3.0.1 - shallowequal@1.1.0: {} - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -13336,6 +13311,10 @@ snapshots: undici-types@6.20.0: optional: true + unhead@2.0.8: + dependencies: + hookable: 5.5.3 + unicorn-magic@0.1.0: {} unified@10.1.2: diff --git a/scripts/dictionary.txt b/scripts/dictionary.txt index 7bb3b03f75..de7426c06b 100644 --- a/scripts/dictionary.txt +++ b/scripts/dictionary.txt @@ -145,6 +145,7 @@ transpiling treeshaking tsbuildinfo tsdoc +Unhead unocss unpatch unplugin