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]

Reply via email to