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 (
-
+ <>
{
// Global UI
@@ -64,6 +63,6 @@ export function App({ helmetContext }: { helmetContext?: object }) {
});
})
}
-
+ >
);
}
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])}
>
-
+
+
+
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(
-
+
+
+
,
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 @@
///
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 (
-
+
{feeds.map(({ language, url, mime }) => {
const props: LinkHTMLAttributes = {
rel: 'alternate',
@@ -21,6 +21,6 @@ export default function FeedsAnnotations() {
// biome-ignore lint/correctness/useJsxKeyInIterable: no key props
return ;
})}
-
+
);
}
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 (
<>
-
+
{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