This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git
The following commit(s) were added to refs/heads/master by this push:
new 7426acd4834 [fix](i18n) Fix community entries and navigation hydration
(#4423)
7426acd4834 is described below
commit 7426acd4834f573b9c06e1161d7fa97ecb2a345f
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Wed Oct 21 08:40:33 2026 +0100
[fix](i18n) Fix community entries and navigation hydration (#4423)
Opening `/ru/community/` currently displays an Apache directory listing
even though the community documents are published. Language switches on
a freshly loaded document also drop its query and fragment because their
initial client attributes differ from SSR. These issues affect the
shared locale navigation.
Redirect each enabled locale's community root to its existing “Join the
Community” page. Render language and version switch suffixes after
hydration so their first client attributes match SSR, including the
single-version link.
Extend navigation verification to require a community entry in both
normal and optimized artifacts, including Dev-only checks. The existing
regression fixture now rejects missing entries and redirects into the
wrong document group. Navigation components and verifier changes select
the full five-language optimized CI build so the browser checks run on
the published artifact.
Validation: the full i18n, publication, and config/SSR suite passed 245
of 246 tests, with one existing Apache-environment test skipped. A
fixture using the real site configuration, Docusaurus redirect writer,
and publication optimizer passed for en, zh-CN, ja, ko, and ru,
including query preservation and index.html aliases. A browser fixture
with the actual navigation components and Docusaurus browser provider
reproduced the hydration failure before the change and retained all five
language links and both version links after it, with no hydration
errors. Public production checks reproduced both issues; the repairs
still need CI and deployment verification.
## Versions
- [x] dev
- [x] 4.x
- [x] 3.x
- [x] 2.1 or older (not covered by version/language sync gate)
Shared community routing and validation; no versioned document content
changed.
## Languages
- [x] Chinese
- [x] English
- Japanese, Korean, and Russian are covered by the same enabled-locale
configuration.
## Docs Checklist
- [x] Checked by AI
- [x] Test Cases Built
- [x] Updated required version and language counterparts, or explained
why not
- [x] If only one language changed, confirmed whether source/translation
counterparts need sync
---------
Co-authored-by: morningman <[email protected]>
---
docusaurus.config.js | 3 ++
scripts/i18n/__tests__/doc-navigation.test.cjs | 9 ++++
scripts/i18n/__tests__/navigation-suffix.test.cjs | 63 ++++++++++++++++++++++
scripts/i18n/__tests__/t04.test.cjs | 5 +-
scripts/i18n/build-scope.cjs | 4 +-
scripts/i18n/verify-doc-navigation.cjs | 15 +++---
scripts/i18n/verify-search.cjs | 7 +++
.../NavbarItem/DocsVersionDropdownNavbarItem.tsx | 8 +--
.../NavbarItem/LocaleDropdownNavbarItem/index.tsx | 5 +-
9 files changed, 107 insertions(+), 12 deletions(-)
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 8316730d3fe..199be307c16 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -75,6 +75,9 @@ function buildRedirectIndex() {
for (const locale of ENABLED_LOCALES) {
const prefix = locale === 'en' ? '' : `/${locale}`;
add(`${prefix}/docs/4.x/getting-started/what-is-apache-doris`,
`${prefix}/docs`);
+ // Community has no root document; use its first sidebar page as the
+ // entry instead of exposing the deployment server's directory listing.
+ add(`${prefix}/community/join-community`, `${prefix}/community`);
}
// Decommissioned standalone pages whose content moved into the
diff --git a/scripts/i18n/__tests__/doc-navigation.test.cjs
b/scripts/i18n/__tests__/doc-navigation.test.cjs
index 08a7b2acf8b..0e5e4da028e 100644
--- a/scripts/i18n/__tests__/doc-navigation.test.cjs
+++ b/scripts/i18n/__tests__/doc-navigation.test.cjs
@@ -38,7 +38,16 @@ test('navigation samples a native sidebar member when a
current standalone doc s
write(`site${doc.permalink}index.html`, `<article
class="theme-doc-markdown" lang="ru">Документ</article>${navigation}`);
}
write('site/ru/docs/index.html',
createRedirect({toUrl:pages[2].permalink}));
+ write('site/ru/community/index.html',
createRedirect({toUrl:pages[3].permalink}));
+ write('site/.publish/redirects.json', JSON.stringify({redirects:[]}));
const
result=verify({site:path.join(root,'site'),locale:'ru',snapshot:path.join(root,'snapshot'),manifest:path.join(root,'manifest.json')});
assert.equal(result.routes,4);
assert.equal(result.checks.find(check=>check.group ===
'DEV').sample,pages[1].permalink);
+ const
options={site:path.join(root,'site'),locale:'ru',snapshot:path.join(root,'snapshot'),manifest:path.join(root,'manifest.json'),devOnly:true};
+ fs.unlinkSync(path.join(root,'site/ru/community/index.html'));
+ assert.throws(()=>verify(options), /Missing compacted entry:
\/ru\/community\//);
+ write('site/.publish/redirects.json',
JSON.stringify({redirects:[['/ru/community/',pages[3].permalink,true,301]]}));
+ assert(verify(options).checks.some(check=>check.entry === '/ru/community/'
&& check.target === pages[3].permalink));
+ write('site/.publish/redirects.json',
JSON.stringify({redirects:[['/ru/community/',pages[1].permalink,true,301]]}));
+ assert.throws(()=>verify(options), /not a registered community\/current
document/);
});
diff --git a/scripts/i18n/__tests__/navigation-suffix.test.cjs
b/scripts/i18n/__tests__/navigation-suffix.test.cjs
new file mode 100644
index 00000000000..621b37e705d
--- /dev/null
+++ b/scripts/i18n/__tests__/navigation-suffix.test.cjs
@@ -0,0 +1,63 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const test = require('node:test');
+const ts = require('typescript');
+const React = require('react');
+const {renderToString} = require('react-dom/server');
+const cheerio = require('cheerio');
+
+function fixture(kind, singleVersion = false) {
+ const locales = ['en', 'zh-CN', 'ja', 'ko', 'ru'];
+ const route = locale => `${locale === 'en' ? '' : '/' +
locale}/docs/dev/topic/`;
+ const i18n = {currentLocale: 'ru', locales,
+ localeConfigs: Object.fromEntries(locales.map(locale => [locale,
{label: locale, htmlLang: locale}]))};
+ const versions = (singleVersion ? ['current'] : ['current',
'4.x']).map(name => ({name, label: name,
+ mainDocId: 'topic', docs: [{id: 'topic', path: `/ru/docs/${name ===
'current' ? 'dev' : name}/topic/`}]}));
+ const active = {activeVersion: versions[0],
+ alternateDocVersions: Object.fromEntries(versions.map(version =>
[version.name, version.docs[0]]))};
+ let phase = 'server';
+ const anchor = ({to, label}) => React.createElement('a', {href:
to.replace(/^pathname:\/\//, '')}, label);
+ const imports = {
+ react: React,
+ '@docusaurus/router': {useLocation: () => ({pathname: route('ru'),
+ search: phase === 'server' ? '' : '?from=search', hash: phase ===
'server' ? '' : '#usage'})},
+ '@docusaurus/useIsBrowser': () => phase === 'hydrated',
+ '@docusaurus/useDocusaurusContext': () => ({i18n}),
+ '@docusaurus/Translate': {translate: ({message}) => message},
+ '@docusaurus/theme-common/internal': {useAlternatePageUtils: () =>
({createUrl: ({locale}) => route(locale)})},
+ '@site/src/utils/translation': {useTranslationRoutes: () => ({
+ group: Object.fromEntries(locales.map(locale => [locale,
route(locale)])), translatedLocales: ['ja', 'ko', 'ru']})},
+ '@docusaurus/plugin-content-docs/client': {useVersions: () =>
versions, useActiveDocContext: () => active,
+ useDocsVersionCandidates: () => versions, useDocsPreferredVersion:
() => ({savePreferredVersionName() {}})},
+ '@theme/NavbarItem/DropdownNavbarItem': ({items}) =>
React.createElement('nav', null,
+ items.map((item, key) => React.createElement(anchor, {...item,
key}))),
+ '@theme/NavbarItem/DefaultNavbarItem': anchor,
+ };
+ const file = kind === 'locale' ? 'LocaleDropdownNavbarItem/index.tsx' :
'DocsVersionDropdownNavbarItem.tsx';
+ const source = fs.readFileSync(path.resolve(__dirname,
'../../../src/theme/NavbarItem', file), 'utf8');
+ const compiled = ts.transpileModule(source, {compilerOptions: {module:
ts.ModuleKind.CommonJS,
+ target: ts.ScriptTarget.ES2020, jsx: ts.JsxEmit.React,
esModuleInterop: true}}).outputText;
+ const module = {exports: {}};
+ new Function('require', 'module', 'exports', compiled)(name => {
+ if (name.endsWith('.scss')) return {};
+ assert(Object.hasOwn(imports, name), `Unexpected import ${name}`);
+ return imports[name];
+ }, module, module.exports);
+ return next => {
+ phase = next;
+ return renderToString(React.createElement(module.exports.default));
+ };
+}
+
+for (const [kind, singleVersion] of [['locale', false], ['version', false],
['version', true]]) {
+ test(`${kind} links match SSR before hydration and retain query/hash
afterwards (single version: ${singleVersion})`, () => {
+ const render = fixture(kind, singleVersion);
+ const server = render('server');
+ assert.equal(render('initial-client'), server, 'Initial client
attributes must match server HTML');
+ const links = html => cheerio.load(html)('a').map((_, node) =>
node.attribs.href).get();
+ const initial = links(server), hydrated = links(render('hydrated'));
+ assert(initial.length > 0);
+ assert.deepEqual(hydrated, initial.map(href => href +
'?from=search#usage'));
+ });
+}
diff --git a/scripts/i18n/__tests__/t04.test.cjs
b/scripts/i18n/__tests__/t04.test.cjs
index de207f906d3..d7bf666e480 100644
--- a/scripts/i18n/__tests__/t04.test.cjs
+++ b/scripts/i18n/__tests__/t04.test.cjs
@@ -45,7 +45,10 @@ test('PR build scope separates content locales, shared
inputs, and retired versi
test('publication changes exercise the complete optimized artifact on CI', ()
=> {
for (const file of
['scripts/publish/optimize.cjs','scripts/build-locales.cjs',
- 'config/ssrTemplate.js','static/js/brand-theme-bootstrap.js']) {
+ 'config/ssrTemplate.js','static/js/brand-theme-bootstrap.js',
+ 'src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx',
+ 'src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx',
+
'scripts/i18n/verify-doc-navigation.cjs','scripts/i18n/verify-search.cjs']) {
const scope = buildScope([file],versions,['ja']);
assert.equal(scope.optimize_publish,true,file);
assert.equal(scope.docs_versions,'',file);
diff --git a/scripts/i18n/build-scope.cjs b/scripts/i18n/build-scope.cjs
index 729a88a0d93..c1b70a00da6 100644
--- a/scripts/i18n/build-scope.cjs
+++ b/scripts/i18n/build-scope.cjs
@@ -66,7 +66,9 @@ function buildScope(files, versions, managed = []) {
const searchChanged = files.some(file =>
/^scripts\/i18n\/search-plugin\.cjs$/.test(file)
|| /^src\/theme\/(SearchBar|SearchPage)\/.+\.[jt]sx?$/.test(file)
||
/^src\/utils\/(searchVersion|searchContexts|searchInput|searchNormalization|tokenize|smartQueries|smartTerms)\.[jt]s$/.test(file));
- const optimizePublish = searchChanged || files.some(file =>
/^(scripts\/publish\/|config\/(custom-docusaurus-plugin|ssrTemplate)\.js$|scripts\/build-locales\.cjs$|static\/js\/(brand-theme|course-rail)-bootstrap\.js$|\.github\/workflows\/(cron|manual)-deploy-website\.yml$)/.test(file));
+ const navigationChanged = files.some(file =>
/^src\/theme\/NavbarItem\/(LocaleDropdownNavbarItem\/index|DocsVersionDropdownNavbarItem)\.tsx$/.test(file)
+ || /^scripts\/i18n\/verify-(doc-navigation|search)\.cjs$/.test(file));
+ const optimizePublish = searchChanged || navigationChanged ||
files.some(file =>
/^(scripts\/publish\/|config\/(custom-docusaurus-plugin|ssrTemplate)\.js$|scripts\/build-locales\.cjs$|static\/js\/(brand-theme|course-rail)-bootstrap\.js$|\.github\/workflows\/(cron|manual)-deploy-website\.yml$)/.test(file));
// Publication changes need the same complete artifact as deployment, on
// the CI runner. Keep local checks scoped to fixtures and Dev versions.
if (optimizePublish) {tier = 'full'; shared();}
diff --git a/scripts/i18n/verify-doc-navigation.cjs
b/scripts/i18n/verify-doc-navigation.cjs
index c52850a9a55..72a39e39809 100644
--- a/scripts/i18n/verify-doc-navigation.cjs
+++ b/scripts/i18n/verify-doc-navigation.cjs
@@ -69,8 +69,11 @@ function verify({site, locale = 'ja', snapshot =
inventoryBaseline(root, locale)
checks.push({group: group.id, routes: pages.length, sample:
representative.permalink,
contentLocale: representative.contentLocale, sidebarLinks:
links.length, domains: [...new Set(domains)]});
}
- if (!devOnly) {
- const entry = `/${locale}/docs/`;
+ const entries = [];
+ if (!devOnly) entries.push({entry: `/${locale}/docs/`, pluginId:
'default', version: '4.x'});
+ if (scope.groups.some(group => group.pluginId === 'community'))
+ entries.push({entry: `/${locale}/community/`, pluginId: 'community',
version: 'current'});
+ for (const {entry, pluginId, version} of entries) {
const origin = 'https://doris.apache.org';
let target;
if (fs.existsSync(filename(entry))) {
@@ -80,13 +83,13 @@ function verify({site, locale = 'ja', snapshot =
inventoryBaseline(root, locale)
} else {
const redirects = JSON.parse(fs.readFileSync(path.join(site,
'.publish/redirects.json'), 'utf8')).redirects;
const redirect = redirects.find(([from]) => from.replace(/\/$/,
'') === entry.replace(/\/$/, ''));
- assert(redirect, `Missing compacted stable entry: ${entry}`);
+ assert(redirect, `Missing compacted entry: ${entry}`);
const url = new URL(redirect[1], origin);
- assert.equal(url.origin, origin, 'Stable entry must remain on this
site');
+ assert.equal(url.origin, origin, 'Document entry must remain on
this site');
target = url.pathname;
}
- assert(data.pages.some(page => page.pluginId === 'default' &&
page.version === '4.x' && page.permalink === target),
- `Stable entry is not a registered 4.x document: ${target}`);
+ assert(data.pages.some(page => page.pluginId === pluginId &&
page.version === version && page.permalink === target),
+ `Entry ${entry} is not a registered ${pluginId}/${version}
document: ${target}`);
read(target);
checks.push({entry, target});
}
diff --git a/scripts/i18n/verify-search.cjs b/scripts/i18n/verify-search.cjs
index 8c84be3b1f0..80dc8c46272 100644
--- a/scripts/i18n/verify-search.cjs
+++ b/scripts/i18n/verify-search.cjs
@@ -146,6 +146,13 @@ async function verify({site, locale = 'ja', devOnly =
false, sourceLocales = [],
assert.equal(source.pathname, current.englishPermalink);
assert.equal(source.search, '?i18nProbe=1');
assert.equal(source.hash, '#core-highlights');
+ // Hydration must update language-switch attributes as well as the
+ // source link. SSR cannot know the request's query or fragment.
+ const englishSwitch =
navigation.locator('a.dropdown__link[lang="en-US"]').first();
+ await navigation.waitForFunction(expected =>
+
document.querySelector('a.dropdown__link[lang="en-US"]')?.getAttribute('href')
=== expected,
+ current.englishPermalink + source.search + source.hash);
+ assert.equal(await englishSwitch.getAttribute('href'),
current.englishPermalink + source.search + source.hash);
const linked = await
navigation.locator('a[href]').evaluateAll(nodes => nodes.map(node => new
URL(node.href).pathname));
const fallback = data.pages.find(page => page.pluginId ===
'default' && page.version === 'current'
&& !page.draft && !page.useTarget && linked.some(link =>
route(link) === route(page.permalink)));
diff --git a/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx
b/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx
index 73236bc541f..1d6c2db36cb 100644
--- a/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx
+++ b/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx
@@ -8,6 +8,7 @@ import {
import { translate } from '@docusaurus/Translate';
import { useLocation } from '@docusaurus/router';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+import useIsBrowser from '@docusaurus/useIsBrowser';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import type { Props } from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
@@ -33,6 +34,7 @@ export default function DocsVersionDropdownNavbarItem({
...props
}: Props): JSX.Element {
const { search, hash } = useLocation();
+ const suffix = useIsBrowser() ? search + hash : '';
const {
i18n: { currentLocale },
} = useDocusaurusContext();
@@ -46,7 +48,7 @@ export default function DocsVersionDropdownNavbarItem({
return {
label: version.label,
// preserve ?search#hash suffix on version switches
- to: `${versionDoc.path}${search}${hash}`,
+ to: `${versionDoc.path}${suffix}`,
isActive: () => version === activeDocContext.activeVersion,
onClick: () => savePreferredVersionName(version.name),
};
@@ -57,7 +59,7 @@ export default function DocsVersionDropdownNavbarItem({
return {
label: version.label,
// preserve ?search#hash suffix on version switches
- to: `${targetDoc.path}${search}${hash}`,
+ to: `${targetDoc.path}${suffix}`,
isActive: () => version === activeDocContext.activeVersion,
onClick: () => savePreferredVersionName(version.name),
};
@@ -76,7 +78,7 @@ export default function DocsVersionDropdownNavbarItem({
})
: dropdownVersion.label;
const dropdownTo =
- mobile && items.length > 1 ? undefined :
getVersionTargetDoc(dropdownVersion, activeDocContext).path;
+ mobile && items.length > 1 ? undefined :
`${getVersionTargetDoc(dropdownVersion, activeDocContext).path}${suffix}`;
// We don't want to render a version dropdown with 0 or 1 item. If we build
// the site with a single docs version (onlyIncludeVersions: ['1.0.0']),
diff --git a/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx
b/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx
index e467918d0f0..edf4768aa6b 100644
--- a/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx
+++ b/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx
@@ -3,6 +3,7 @@ import useDocusaurusContext from
'@docusaurus/useDocusaurusContext';
import { useAlternatePageUtils } from '@docusaurus/theme-common/internal';
import { translate } from '@docusaurus/Translate';
import { useLocation } from '@docusaurus/router';
+import useIsBrowser from '@docusaurus/useIsBrowser';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import type { LinkLikeNavbarItemProps } from '@theme/NavbarItem';
import type { Props } from '@theme/NavbarItem/LocaleDropdownNavbarItem';
@@ -22,6 +23,8 @@ export default function LocaleDropdownNavbarItem({
const alternatePageUtils = useAlternatePageUtils();
const {group, translatedLocales} = useTranslationRoutes();
const { search, hash } = useLocation();
+ // Match SSR on the first client render, then update suffixes after
hydration.
+ const suffix = useIsBrowser() ? search + hash : '';
// Locales listed here render in the dropdown with a "Coming Soon" badge
and
// are not clickable yet. Empty while no locale is in that state.
@@ -37,7 +40,7 @@ export default function LocaleDropdownNavbarItem({
const isTranslatedSwitch = translatedLocales.includes(locale) ||
translatedLocales.includes(currentLocale);
const baseTo = `pathname://${isTranslatedSwitch && group?.[locale] ?
group[locale] : nativeTo}`;
// preserve ?search#hash suffix on locale switches
- const to = `${baseTo}${search}${hash}${queryString}`;
+ const to = `${baseTo}${suffix}${queryString}`;
const isComingSoon = COMING_SOON_LOCALES.has(locale);
const baseLabel = localeConfigs[locale]!.label;
const label = isComingSoon ? (
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]