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 69ecc7928851 [fix](i18n) Preserve search identity and source fallback
(#4534)
69ecc7928851 is described below
commit 69ecc79288512c3e2e665081e52bc44548383cd5
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Tue Oct 27 19:32:01 2026 +0000
[fix](i18n) Preserve search identity and source fallback (#4534)
Switching a Japanese Dev search to English kept `ctx=docs/dev`, which
the source-language search ignored and resolved to an empty root index.
The reverse switch mixed Dev, stable, and community results. Normalize
the logical scope between `ctx=docs&version=docs/dev/` and
`ctx=docs/dev`, preserve the query, extra parameters and hash, and
retain course/releases/community scope in English-source links.
Managed locales do not index unsupported historical versions. Their
documentation search now exposes an explicit English search link for the
same version, and historical search URLs show the source-search choice
instead of querying current translated indexes. Implicit source-language
searches retain their native stable-version default across a language
switch. English and Chinese also index the community context so its
source links remain useful. Active/default version decisions come from
native metadata and registered contexts.
Source-language indexes now exclude pages explicitly marked
non-indexable by the unified manifest. This removes English fallback
bodies from Chinese search and keeps unlisted source pages out of the
index. Chinese no-result pages offer the same version/context in the
English source search.
Validation:
- 397 Node tests: 395 passed, 2 conditional skips; 27 workflow tests
passed.
- Real five-language Rspack fixture: 36 cases passed, including actual
language-menu round trips, 3.x/2.1 source search and explicit
source-only search pages in three managed locales, plugin source links,
and Chinese fallback/source-index boundaries; 40 mobile retry-theme and
96 source-link viewport/theme checks passed.
- Manual content validation retains the existing four report-drift
records; document prose, translation ownership, production versions and
sidebars are unchanged.
Self-review against AGENTS.md:
| Checkpoint | Conclusion |
| --- | --- |
| Goal | Reproduced scope loss and false source-index boundaries are
addressed; final production acceptance continues after merge. |
| Focus | Search configuration, URL/state adapters, and their fixture
dependencies; no prose backfill. |
| Information architecture | Active versions, plugins, English fallback
and translation ownership/denominators are preserved. |
| Paths/navigation | Document routes and sidebars stay stable; legacy
search representations normalize to the same version/context and
preserve query extras/hash. |
| Frontend | Source links are explicit, localized and keyboard
accessible, with semantic theme tokens and viewport/theme coverage. |
| Validation | Pure scope tests plus actual worker, source-index,
language-menu and historical-version browser cases cover the boundaries;
no full production build was run locally. |
| Other quality | English fallbacks/unlisted source pages are excluded
by manifest decisions; no credential or workflow permission expansion. |
---------
Co-authored-by: morningman <[email protected]>
---
docusaurus.config.js | 9 +-
.../i18n/__tests__/fixtures/content-manifest.cjs | 2 +-
scripts/i18n/__tests__/locale-entry.test.cjs | 3 +
scripts/i18n/__tests__/marketing-pages.test.cjs | 1 +
scripts/i18n/__tests__/search-scopes.test.cjs | 49 +++++++
scripts/i18n/ci-plan.cjs | 2 +-
scripts/i18n/search-plugin.cjs | 10 +-
scripts/i18n/verify-search-transport.cjs | 163 ++++++++++++++++++++-
src/hooks/use-search-query.ts | 26 ++--
src/theme/SearchBar/SearchBar.jsx | 27 +++-
src/theme/SearchBar/SearchBar.module.css | 16 ++
src/theme/SearchPage/SearchPage.jsx | 30 ++--
src/utils/searchMessages.ts | 10 +-
src/utils/searchScopes.js | 47 ++++++
src/utils/useLanguageItems.ts | 14 +-
translations/shared/registry.json | 3 +-
16 files changed, 366 insertions(+), 46 deletions(-)
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 5bb5d245c7e7..59a5f76aaed1 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -15,6 +15,9 @@ const {filterContentSitemap} =
require('./scripts/i18n/sitemap.cjs');
const contentTranslationPlugin = require('./scripts/i18n/content-plugin.cjs');
const {translatedContentPages} = require('./scripts/i18n/source-groups.cjs');
const searchPlugin = require('./scripts/i18n/search-plugin.cjs');
+const SOURCE_SEARCH_CONTEXTS = ['docs',
+ {path:'community',label:{en:'Community','zh-CN':'社区'}}, 'course',
+ {path:'releases',label:{en:'Release Notes','zh-CN':'发行说明'}}];
const {enabledLocales, enabledLocaleRecords} = require('./config/i18n');
const {managedLocales} = require('./scripts/i18n/state.cjs');
const DEFAULT_LOCALE = 'en';
@@ -332,6 +335,8 @@ const config = {
],
projectName: 'apache/doris-website', // Usually your repo name.
customFields: {
+ searchContexts: {source:SOURCE_SEARCH_CONTEXTS.map(item=>typeof
item==='string'?item:item.path),
+ managed:searchPlugin.contextsFor(__dirname)},
// Real Slack invite link. `/slack` (src/pages/slack.tsx) records the
// click in Matomo and then forwards visitors here, so every public
// channel keeps linking to https://doris.apache.org/slack and this is
@@ -543,13 +548,13 @@ const config = {
highlightSearchTermsOnTargetPage: true,
// indexPages: true,
indexDocs: true,
- docsRouteBasePath: ['docs', 'course', 'releases',
'zh-CN/docs', 'zh-CN/course', 'zh-CN/releases'],
+ docsRouteBasePath: ['docs', 'community', 'course', 'releases',
'zh-CN/docs', 'zh-CN/community', 'zh-CN/course', 'zh-CN/releases'],
indexBlog: false,
explicitSearchResultPath: true,
searchBarShortcut: true,
searchBarShortcutHint: true,
searchResultLimits: 100,
- searchContextByPaths: ['docs', 'course', { path: 'releases',
label: { en: 'Release Notes', 'zh-CN': '发行说明' } }],
+ searchContextByPaths: SOURCE_SEARCH_CONTEXTS,
useAllContextsWithNoSearchContext: false,
ignoreFiles: [/^docs\/(?:[^/]+\/)?key-features\//],
},
diff --git a/scripts/i18n/__tests__/fixtures/content-manifest.cjs
b/scripts/i18n/__tests__/fixtures/content-manifest.cjs
index b49fef951cac..3b06a33301b0 100644
--- a/scripts/i18n/__tests__/fixtures/content-manifest.cjs
+++ b/scripts/i18n/__tests__/fixtures/content-manifest.cjs
@@ -48,7 +48,7 @@ export default function Page(){return <LayoutNext
title={title}><h1>{title}</h1>
for (const record of states.pages.filter(record => record.locale ===
locale)) record.targetHash = digestFiles(root, record.targetFiles);
states.pages.push({contentId: 'page:blocked', locale, state:
'blocked', sourceFiles: ['src/pages/blocked.tsx'], targetFiles: []});
}
- for (const file of ['src/utils/useLocalizedRoute.ts',
'src/utils/useLanguageItems.ts', 'src/utils/localePreference.ts',
'src/utils/languageMessages.ts',
+ for (const file of ['src/utils/useLocalizedRoute.ts',
'src/utils/useLanguageItems.ts', 'src/utils/searchScopes.js',
'src/utils/localePreference.ts', 'src/utils/languageMessages.ts',
'src/components/home-next/LocalizedLink.tsx',
'src/components/home-next/PageContent.tsx',
'src/components/home-next/PageContent.scss',
'src/components/home-next/PageLanguageSwitcher.tsx',
'src/components/home-next/PageLanguageSwitcher.scss',
'src/components/brand-theme/BrandThemeSwitcher.tsx',
'src/components/brand-theme/BrandThemeSwitcher.scss',
diff --git a/scripts/i18n/__tests__/locale-entry.test.cjs
b/scripts/i18n/__tests__/locale-entry.test.cjs
index cf20051723ab..d01bdadab5ef 100644
--- a/scripts/i18n/__tests__/locale-entry.test.cjs
+++ b/scripts/i18n/__tests__/locale-entry.test.cjs
@@ -174,6 +174,9 @@ test('English-root menu links carry this manual choice even
before hydration and
'./localePreference': {saveLocalePreference: locale =>
saved.push(locale)},
'./localizedRoute': routeModule.exports,
'./languageMessages': {languageMessages: () => ({note: 'English'})},
+ '@docusaurus/plugin-content-docs/client': {useAllDocsData:()=>({})},
+ './localization':
{isTranslatedLocale:locale=>['ja','ko','ru'].includes(locale)},
+ './searchScopes': {convertSearchParams:()=>{throw new Error('Root
links must not rewrite search scope');}},
};
const module = {exports: {}};
new Function('require', 'module', 'exports',
compile('useLanguageItems'))(name => {assert(Object.hasOwn(imports, name),
name); return imports[name];}, module, module.exports);
diff --git a/scripts/i18n/__tests__/marketing-pages.test.cjs
b/scripts/i18n/__tests__/marketing-pages.test.cjs
index 1094ce3db297..db477a29f0e7 100644
--- a/scripts/i18n/__tests__/marketing-pages.test.cjs
+++ b/scripts/i18n/__tests__/marketing-pages.test.cjs
@@ -74,6 +74,7 @@ function modules(locale, pathname, enabled = true,
translatedPages = {ja: pagePa
'@generated/docusaurus.config': config,
'@docusaurus/Translate': {translate: ({message}) => message},
'@docusaurus/useDocusaurusContext': () => ({i18n, siteConfig: config}),
+ '@docusaurus/plugin-content-docs/client': {useAllDocsData:()=>({})},
'@docusaurus/useBaseUrl': source => source,
'@docusaurus/useGlobalData': () => ({'doris-translation-status':
{default: {
alternates: {}, locales: enabled ? ['ja','ko','ru'] : [],
prefixes: [`${prefix}/docs/dev`,`${prefix}/community`],
diff --git a/scripts/i18n/__tests__/search-scopes.test.cjs
b/scripts/i18n/__tests__/search-scopes.test.cjs
new file mode 100644
index 000000000000..a1ac7e903f67
--- /dev/null
+++ b/scripts/i18n/__tests__/search-scopes.test.cjs
@@ -0,0 +1,49 @@
+const
test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),ts=require('typescript');
+const
{outputText}=ts.transpileModule(fs.readFileSync(path.resolve(__dirname,'../../../src/utils/searchScopes.js'),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020}});
+const moduleValue={exports:{}};new
Function('exports',outputText)(moduleValue.exports);
+const
{resolveSearchScope,convertSearchParams,docSearchSourceVersion}=moduleValue.exports;
+const
versions=['dev','4.x','3.x','2.1'].map(name=>({path:'/docs/'+name,isLast:name==='4.x'}));
+const
source={versions,contexts:['docs','community','course','releases'],managed:false};
+const
managed={versions,contexts:['docs/dev','docs/4.x','community','course','releases'],managed:true};
+
+test('Dev and stable document scope survives both source/managed URL
representations',()=>{
+
assert.deepEqual(resolveSearchScope({context:'docs/dev'},source),{context:'docs',version:'docs/dev/',sourceOnlyVersion:null});
+
assert.deepEqual(resolveSearchScope({context:'docs',version:'docs/dev/'},managed),{context:'docs/dev',version:'',sourceOnlyVersion:null});
+
assert.equal(resolveSearchScope({context:'docs'},managed).context,'docs/4.x');
+
assert.deepEqual(resolveSearchScope({context:'docs/4.x'},source),{context:'docs',version:'',sourceOnlyVersion:null});
+});
+test('old managed versions select explicit source search instead of current
translated indexes',()=>{
+ for(const version of ['3.x','2.1']) {
+
assert.deepEqual(resolveSearchScope({context:'docs',version:'docs/'+version+'/'},managed),
+
{context:'',version:'docs/'+version+'/',sourceOnlyVersion:version});
+
assert.equal(resolveSearchScope({context:'docs/'+version},managed).sourceOnlyVersion,version);
+
assert.equal(resolveSearchScope({context:'docs/'+version},source).version,'docs/'+version+'/');
+
assert.equal(docSearchSourceVersion('/ja/docs/'+version+'/intro/',versions.map(v=>({...v,path:'/ja'+v.path})),managed.contexts),version);
+ }
+
assert.equal(docSearchSourceVersion('/ja/docs/dev/intro/',versions.map(v=>({...v,path:'/ja'+v.path})),managed.contexts),null);
+
assert.equal(docSearchSourceVersion('/ja/community/intro/',versions.map(v=>({...v,path:'/ja'+v.path})),managed.contexts),null);
+});
+test('plugin contexts preserve their identity and cannot inherit a docs
version',()=>{
+ for(const context of ['community','course','releases'])for(const options
of [source,managed])
+
assert.deepEqual(resolveSearchScope({context,version:'docs/3.x/'},options),{context,version:'',sourceOnlyVersion:null});
+});
+test('language conversion preserves the query and unrelated parameters through
a round trip',()=>{
+ const original=new URLSearchParams({q:'Doris +
query_vector',ctx:'docs/dev',probe:'/a?b=c'});
+ const
english=convertSearchParams(original,source),japanese=convertSearchParams(english,managed);
+
assert.equal(english.get('ctx'),'docs');assert.equal(english.get('version'),'docs/dev/');
+
assert.equal(japanese.get('ctx'),'docs/dev');assert(!japanese.has('version'));
+ for(const value of
[english,japanese]){assert.equal(value.get('q'),original.get('q'));assert.equal(value.get('probe'),original.get('probe'));}
+ assert.equal(original.get('ctx'),'docs/dev');
+});
+test('an implicit native default stays on the stable version when switching
languages',()=>{
+ const target=convertSearchParams(new
URLSearchParams({q:'Doris'}),managed,source);
+ assert.equal(target.get('ctx'),'docs/4.x');
+ assert.equal(target.get('q'),'Doris');assert(!target.has('version'));
+});
+test('latest version and unsupported status come from native metadata and
registered contexts',()=>{
+ const
futureVersions=[{path:'/site/ru/docs/dev'},{path:'/site/ru/docs/5.x',isLast:true},{path:'/site/ru/docs/4.x'}];
+ const
options={managed:true,versions:futureVersions,contexts:['docs/dev','docs/5.x','community']};
+
assert.equal(resolveSearchScope({context:'docs'},options).context,'docs/5.x');
+
assert.equal(resolveSearchScope({context:'docs/4.x'},options).sourceOnlyVersion,'4.x');
+
assert.equal(resolveSearchScope({context:'https://example.com'},source).context,'docs');
+});
diff --git a/scripts/i18n/ci-plan.cjs b/scripts/i18n/ci-plan.cjs
index 567c645dc2b8..44a1883820d8 100644
--- a/scripts/i18n/ci-plan.cjs
+++ b/scripts/i18n/ci-plan.cjs
@@ -70,7 +70,7 @@ function policy(files, {forceFull = false, forceSample =
false} = {}) {
runSamplingFixture: files.some(file =>
/^scripts\/(build-locales?\.cjs$|i18n\/(ci-sample|ci-blog-sample|build-cache|verify-ci-sampl(?:e|ing-fixture))\.cjs$)/.test(file)),
runSearchFixture: mode === 'full' || files.some(file =>
-
/^(src\/theme\/(SearchBar|SearchPage)\/|src\/utils\/(fetchSearchIndex|searchMessages)\.[jt]s$|scripts\/i18n\/(search-plugin|search-worker-loader|verify-search-transport)\.cjs$)/.test(file)),
+
/^(src\/theme\/(SearchBar|SearchPage)\/|src\/utils\/(fetchSearchIndex|searchMessages|searchScopes|useLanguageItems)\.[jt]sx?$|src\/hooks\/use-search-query\.ts$|docusaurus\.config\.js$|scripts\/i18n\/(search-plugin|search-worker-loader|verify-search-transport)\.cjs$)/.test(file)),
runPublishFixture: checks.check_publish_fixture || mode === 'full',
runTranslationTests: mode === 'full' || files.some(file =>
runtime.test(file) || mechanism.test(file) ||
maintenanceConfig(file) || /^scripts\/(i18n\/|build-locales?\.cjs$)/.test(file)
diff --git a/scripts/i18n/search-plugin.cjs b/scripts/i18n/search-plugin.cjs
index 5de6e047d0d7..226d8e3a1ce9 100644
--- a/scripts/i18n/search-plugin.cjs
+++ b/scripts/i18n/search-plugin.cjs
@@ -162,7 +162,15 @@ function searchPlugin(context, options) {
if (indexFilename(context) !== filename) throw new Error('Search index
filename differs from its browser URL');
return {...plugin, postBuild: managed
? buildData =>
buildLocalizedIndex(buildData,config,filename,locale,contextNames)
- : postBuildFactory(config,filename)};
+ : buildData => {
+ const hidden = new Set((buildData.plugins ??
[]).flatMap(plugin=>plugin.content?.contentIdentity?.entries ?? [])
+
.filter(page=>page.kind==='docs'&&page.siteLocale===locale&&page.indexable===false)
+ .map(page=>normalize(page.route)));
+ // Native source-language indexing keeps its version/context
layout,
+ // but cannot advertise English fallbacks as Chinese translations.
+ return postBuildFactory(config,filename)({...buildData,
+
routesPaths:buildData.routesPaths.filter(route=>!hidden.has(normalize(route)))});
+ }};
}
module.exports = searchPlugin;
module.exports.validateOptions = nativeModule.validateOptions;
diff --git a/scripts/i18n/verify-search-transport.cjs
b/scripts/i18n/verify-search-transport.cjs
index 28402e52d7d4..a1aba802013f 100644
--- a/scripts/i18n/verify-search-transport.cjs
+++ b/scripts/i18n/verify-search-transport.cjs
@@ -3,6 +3,8 @@ const path = require('node:path');
const assert = require('node:assert/strict');
const {spawnSync, execFileSync} = require('node:child_process');
const {createFixture} = require('./__tests__/fixtures/multi-locale.cjs');
+const {hash} = require('./state.cjs');
+const {structure} = require('./structure.cjs');
const {createServer} = require('../publish/serve.cjs');
const root = path.resolve(__dirname, '../..');
const fixture = path.join(root, '.i18n-build/search-transport-fixture');
@@ -17,6 +19,31 @@ async function build() {
write('src/scss/_variables.scss',fs.readFileSync(path.join(root,'src/scss/_variables.scss'),'utf8'));
write('static/images/search-icon.svg',fs.readFileSync(path.join(root,'static/images/search-icon.svg')));
write('src/search-fixture.scss','@use "./scss/variables";\n');
+
fs.cpSync(path.join(root,'src/theme/NavbarItem/LocaleDropdownNavbarItem'),path.join(fixture,'src/theme/NavbarItem/LocaleDropdownNavbarItem'),{recursive:true});
+ write('versions.json','["4.x","3.x","2.1"]');
+ write('docs/source-only.md','---\ntitle: Source only\n---\n\nDoris
SourceOnlyFixtureKeyword.\n');
+ for(const version of ['3.x','2.1']) {
+ const body=`---\ntitle: Historical ${version}\n---\n\n##
Usage\n\nDoris HistoricalFixtureSQL ${version}.\n`;
+ write(`versioned_docs/version-${version}/historical.md`,body);
+ write(`versioned_sidebars/version-${version}-sidebars.json`,'{}');
+
write(`i18n/zh-CN/docusaurus-plugin-content-docs/version-${version}/historical.md`,body.replace('Historical','中文历史'));
+ }
+ for(const plugin of ['course','releases']) {
+ const
sourceRoot=plugin==='course'?'course':'releasenotes',sourcePath=sourceRoot+'/sample.md';
+ const body=`---\ntitle: ${plugin} sample\n---\n\n## Usage\n\nDoris
${plugin}FixtureSQL.\n`;
+ write(sourcePath,body);
+ for(const locale of ['zh-CN','ja','ko','ru']) {
+ const
targetPath=`i18n/${locale}/docusaurus-plugin-content-docs-${plugin}/current/sample.md`;
+ const target=body.replace('Doris
',`${{ja:'日本語',ko:'한국어',ru:'Русский','zh-CN':'中文'}[locale]} Doris `);
+ write(targetPath,target);
+ if(locale==='zh-CN')continue;
+ const
stateFile=path.join(fixture,'translations/state',locale,'docs.json'),state=JSON.parse(fs.readFileSync(stateFile));
+
state.pages.push({pluginId:plugin,version:'current',docId:'sample',locale,sourceLocale:'en',sourcePath,targetPath,
+
sourceHash:hash(body),targetHash:hash(target),sourceTechnicalHash:(await
structure(body,'.md')).technicalHash,
+
sourceCommit:'a'.repeat(40),reviewStatus:'machine',ownership:'machine',translatedAt:'2026-10-28T00:00:00Z',glossaryVersion:'0'});
+ fs.writeFileSync(stateFile,JSON.stringify(state));
+ }
+ }
write('src/theme/Root.tsx',`import React, {useEffect} from 'react';
export default function
Root({children}){useEffect(()=>{document.documentElement.dataset.hasHydrated='true';},[]);return
children;}\n`);
fs.appendFileSync(path.join(fixture,'docusaurus.config.js'),`
@@ -24,12 +51,19 @@ const base = module.exports;
module.exports = () => {const config = base();
config.future = {experimental_faster:true};
config.plugins.push('docusaurus-plugin-sass',require(${JSON.stringify(path.join(root,'config/custom-docusaurus-plugin.js'))}));
+ for(const [id,folder] of
[['course','course'],['releases','releasenotes']])config.plugins.splice(2,0,
+
[require(${JSON.stringify(path.join(root,'scripts/i18n/docs-plugin.cjs'))}),{id,path:folder,routeBasePath:id,sidebarPath:false}]);
+
config.plugins.push(context=>({name:'fixture-search-identity',loadContent:()=>({contentIdentity:{reports:[],entries:
+
context.i18n.locales.map(locale=>({contentId:'utility:search',kind:'utility',renderIntegrated:false,siteLocale:locale,
+
contentLocale:locale,state:'source',fallback:false,indexable:false,route:(locale==='en'?'':'/'+locale)+'/search/',
+
englishRoute:'/search/',englishPath:'src/theme/SearchPage/index.js',sourceHash:'fixture',targetHash:null,updatePolicy:'native-manual-docs'}))}})}));
config.presets[0][1].theme =
{customCss:[require.resolve('./src/search-fixture.scss')]};
config.themes =
[[require(${JSON.stringify(path.join(root,'scripts/i18n/search-plugin.cjs'))}),{
hashed:true,language:['en','zh'],indexDocs:true,indexBlog:false,indexPages:false,
-
docsRouteBasePath:['docs','community'],searchContextByPaths:['docs','community'],
+
docsRouteBasePath:['docs','community','course','releases'],searchContextByPaths:['docs','community','course','releases'],
useAllContextsWithNoSearchContext:false,highlightSearchTermsOnTargetPage:true}]];
- config.themeConfig = {navbar:{title:'Search
fixture',items:[{type:'search',position:'right'}]}};
+
config.customFields.searchContexts={source:['docs','community','course','releases'],managed:require(${JSON.stringify(path.join(root,'scripts/i18n/search-plugin.cjs'))}).contextsFor(__dirname)};
+ config.themeConfig = {navbar:{title:'Search
fixture',items:[{type:'localeDropdown',position:'right'},{type:'search',position:'right'}]}};
return config;};\n`);
execFileSync('git',['add','.'],{cwd:fixture});
execFileSync('git',['-c','user.name=Fixture','-c','[email protected]','commit','-qm','Search
transport fixture'],{cwd:fixture});
@@ -46,37 +80,60 @@ async function verify() {
const localOrigin = `http://127.0.0.1:${server.address().port}`;
const browser = await chromium.launch({headless:true}),api=await
request.newContext();
const
csp=fs.readFileSync(path.join(root,'static/.htaccess'),'utf8').match(/Header
set Content-Security-Policy "([^"]+)"/)[1];
- const results=[],errors=[],contexts=new Set();
+ const results=[],errors=[],contexts=new Set(),closingContexts=new
WeakSet();
async function context(mode={}) {
const ctx=await
browser.newContext({serviceWorkers:'block',viewport:{width:1440,height:900}}),requests=[];
contexts.add(ctx);
await ctx.route('**/*',async route=>{
+ try {
const url=new URL(route.request().url());
-
if(!['doris.apache.org','cdnd.selectdb.com'].includes(url.hostname))return
route.abort();
+
if(!['doris.apache.org','cdnd.selectdb.com'].includes(url.hostname))return
await route.abort();
if(/search-index.*\.json$/.test(url.pathname)) {
requests.push({url:url.href,origin:url.origin});
if(mode.failBoth||mode.failCdn&&url.hostname==='cdnd.selectdb.com')
- return
route.fulfill({status:503,body:'Unavailable',headers:{'access-control-allow-origin':'*'}});
+ return await
route.fulfill({status:503,body:'Unavailable',headers:{'access-control-allow-origin':'*'}});
if(mode.delay)await new
Promise(resolve=>setTimeout(resolve,mode.delay));
}
const response=await api.get(localOrigin+url.pathname+url.search);
- return
route.fulfill({response,headers:{...response.headers(),'content-security-policy':csp,
+ return await
route.fulfill({response,headers:{...response.headers(),'content-security-policy':csp,
'access-control-allow-origin':'*'}});
+ } catch(error) {
+ const
cancelled=closingContexts.has(ctx)||route.request().failure()?.errorText==='net::ERR_ABORTED';
+ if(!cancelled||!/Route is already
handled|Target.*closed|Request context disposed/i.test(error.message))throw
error;
+ }
});
const page=await
ctx.newPage();page.on('pageerror',error=>errors.push(error.message));
return {ctx,page,requests};
}
async function close(ctx) {
- await ctx.unrouteAll({behavior:'wait'});
+ closingContexts.add(ctx);
+ await ctx.unrouteAll({behavior:'ignoreErrors'});
await ctx.close();contexts.delete(ctx);
}
const open=async(page,url)=>{
assert.equal((await
page.goto('https://doris.apache.org'+url,{waitUntil:'domcontentloaded'})).status(),200);
await page.waitForSelector('html[data-has-hydrated="true"]');
+ await page.getByText('Search fixture',{exact:true}).first().waitFor();
};
const
input=page=>page.locator('input.navbar__search-input:not([readonly]):visible').first();
const suggestions=page=>page.locator('[class*="suggestion_"]');
try {
+ for(const locale of ['en','zh-CN']) {
+ const
manifest=JSON.parse(fs.readFileSync(path.join(fixture,'.i18n-build/content-manifests',locale+'.json')));
+ const hidden=new
Set(manifest.pages.filter(page=>page.kind==='docs'&&!page.indexable).map(page=>page.route.replace(/\/$/,'')));
+ assert(hidden.size>0,'Source fixture must contain an
unlisted/fallback boundary');
+ function walk(directory) {
+ for(const entry of
fs.readdirSync(directory,{withFileTypes:true})) {
+ const file=path.join(directory,entry.name);
+ if(entry.isDirectory())walk(file);
+ else if(/^search-index.*\.json$/.test(entry.name)) {
+ for(const section of
JSON.parse(fs.readFileSync(file,'utf8')))for(const doc of section.documents??[])
+
if(doc.u)assert(!hidden.has(doc.u.replace(/\/$/,'')),'Non-indexable source page
entered search: '+doc.u);
+ }
+ }
+ }
+ walk(locale==='en'?site:path.join(site,locale));
+ }
for(const locale of ['en','zh-CN','ja','ko','ru']) {
const {ctx,page,requests}=await context({delay:1000});
const
manifest=JSON.parse(fs.readFileSync(path.join(fixture,'.i18n-build/content-manifests',locale+'.json')));
@@ -100,6 +157,98 @@ async function verify() {
assert(requests.some(r=>r.origin==='https://doris.apache.org'));
results.push({fallbackToOrigin:true,requests});await close(ctx);
}
+ for(const start of [
+
{route:'/ja/search/?ctx=docs%2Fdev&q=Doris&probe=1#retained',target:'en-US',context:'docs',version:'docs/dev/'},
+
{route:'/search/?ctx=docs&version=docs%2Fdev%2F&q=Doris&probe=1#retained',target:'ja',context:'docs/dev',version:''},
+
{route:'/search/?q=Doris&probe=1#retained',target:'ja',context:'docs/4.x',version:'',expectedVersion:'4.x'},
+ ]) {
+ const {ctx,page}=await context();await open(page,start.route);
+ await page.locator('section article').first().waitFor();
+ const
language=page.locator(`a.dropdown__link[lang="${start.target}"]`).first();
+ const target=new URL(await
language.getAttribute('href'),'https://doris.apache.org');
+ assert.equal(target.searchParams.get('ctx'),start.context);
+ assert.equal(target.searchParams.get('version')??'',start.version);
+
assert.equal(target.searchParams.get('q'),'Doris');assert.equal(target.searchParams.get('probe'),'1');assert.equal(target.hash,'#retained');
+ await page.locator('.navbar__item.dropdown').first().hover();await
language.click();
+ await
page.waitForFunction(value=>document.documentElement.lang===value,start.target);
+ await page.waitForSelector('html[data-has-hydrated="true"]');
+ await page.locator('section article').first().waitFor();
+ const links=await page.locator('section article
a').evaluateAll(nodes=>nodes.map(node=>node.getAttribute('href')));
+
assert(links.every(link=>link.includes('/docs/'+(start.expectedVersion??'dev')+'/')),'Language
switch lost version scope');
+ results.push({languageSwitch:start,links});await close(ctx);
+ }
+ for(const locale of ['ja','ko','ru']) for(const version of
['3.x','2.1']) {
+ const {ctx,page,requests}=await context();await
open(page,`/${locale}/docs/${version}/historical/`);
+ const
link=page.locator('[data-source-search="'+version+'"]');await link.waitFor();
+ const target=new URL(await
link.getAttribute('href'),'https://doris.apache.org');
+
assert.equal(target.pathname,'/search/');assert.equal(target.searchParams.get('version'),'docs/'+version+'/');
+ assert.equal(await
page.locator('input.navbar__search-input:visible').count(),0);
+ assert.equal(requests.length,0,'Unsupported version fetched a
current translated index');
+ const sourceLinkThemes=[];
+ for(const width of [1440,390]) {
+ await page.setViewportSize({width,height:900});
+ for(const theme of
['doris','golden','blue','read','yellow-blue','purple','yellow-black','sky']) {
+ await
page.evaluate(value=>{document.documentElement.dataset.brandTheme=value;},theme);
+ const box=await link.boundingBox();
+ assert(box.x>=-1&&box.x+box.width<=width+1,'Source search
link overflows');
+ sourceLinkThemes.push({width,theme,box});
+ }
+ }
+ await
page.keyboard.press(process.platform==='darwin'?'Meta+k':'Control+k');
+ assert(await link.evaluate(e=>e===document.activeElement));
+ await link.click();await
page.waitForFunction(()=>document.documentElement.lang==='en-US');
+ await page.waitForSelector('html[data-has-hydrated="true"]');
+ const query=page.locator('input[name="q"]');await
query.fill('HistoricalFixtureSQL');
+ await page.locator('section article').first().waitFor();
+ const links=await page.locator('section article
a').evaluateAll(nodes=>nodes.map(node=>node.getAttribute('href')));
+ assert(links.every(value=>value.includes('/docs/'+version+'/')));
+
results.push({sourceVersion:{locale,version},sourceLinkThemes,links});await
close(ctx);
+ }
+ for(const plugin of ['community','course','releases']) {
+ const {ctx,page}=await context();await
open(page,`/ja/search/?ctx=${plugin}&q=NoMatchingFixtureValue#retained`);
+ const link=page.locator('.container.margin-vert--lg
a[href*="/search/"]').first();await link.waitFor();
+ const target=new URL(await
link.getAttribute('href'),'https://doris.apache.org');
+
assert.equal(target.searchParams.get('ctx'),plugin);assert.equal(target.hash,'#retained');assert(!target.searchParams.has('version'));
+ await link.click();await
page.waitForFunction(()=>document.documentElement.lang==='en-US');
+ await page.waitForSelector('html[data-has-hydrated="true"]');
+ await page.locator('input[name="q"]').fill('Doris');await
page.locator('section article').first().waitFor();
+ const links=await page.locator('section article
a').evaluateAll(nodes=>nodes.map(node=>node.getAttribute('href')));
+ assert(links.every(value=>value.startsWith('/'+plugin+'/')));
+ results.push({sourcePlugin:plugin,links});await close(ctx);
+ }
+ for(const locale of ['ja','ko','ru']) for(const version of
['3.x','2.1']) {
+ const {ctx,page,requests}=await context();
+ await
open(page,`/${locale}/search/?ctx=docs&version=docs%2F${version}%2F&q=HistoricalFixtureSQL#retained`);
+ const note=page.locator('.container.margin-vert--lg
[role="note"]');await note.waitFor();
+ assert.equal(await
page.locator('#context-selector').inputValue(),'docs/'+version);
+ assert.equal(requests.length,0,'Historical search queried
translated current indexes');
+ const source=note.locator('a'),url=new URL(await
source.getAttribute('href'),'https://doris.apache.org');
+
assert.equal(url.searchParams.get('version'),'docs/'+version+'/');assert.equal(url.hash,'#retained');
+ await source.click();await
page.waitForFunction(()=>document.documentElement.lang==='en-US');
+ await page.waitForSelector('html[data-has-hydrated="true"]');await
page.locator('section article').first().waitFor();
+ const links=await page.locator('section article
a').evaluateAll(nodes=>nodes.map(node=>node.getAttribute('href')));
+ assert(links.every(value=>value.includes('/docs/'+version+'/')));
+ results.push({historicalSearchPage:{locale,version},links});await
close(ctx);
+ }
+ {
+ const {ctx,page}=await context();await
open(page,'/zh-CN/search/?ctx=docs&version=docs%2Fdev%2F&q=SourceOnlyFixtureKeyword');
+ const link=page.locator('.container.margin-vert--lg
a[hreflang="en"]');await link.waitFor();
+ const url=new URL(await
link.getAttribute('href'),'https://doris.apache.org');assert.equal(url.pathname,'/search/');
+ assert.equal(url.searchParams.get('version'),'docs/dev/');await
link.click();
+ await
page.waitForFunction(()=>document.documentElement.lang==='en-US');await
page.waitForSelector('html[data-has-hydrated="true"]');
+ await page.locator('section article').first().waitFor();
+ const links=await page.locator('section article
a').evaluateAll(nodes=>nodes.map(node=>node.getAttribute('href')));
+
assert(links.some(value=>value.startsWith('/docs/dev/source-only/')));results.push({chineseFallbackSource:true,links});await
close(ctx);
+ }
+ {
+ const {ctx,page}=await context();await
open(page,'/ja/search/?ctx=docs&version=docs%2F3.x%2F&q=Doris#retained');
+ await page.locator('.container.margin-vert--lg
[role="note"]').waitFor();
+ await page.locator('#context-selector').selectOption('docs/dev');
+ await page.locator('section article').first().waitFor();
+ assert(!new
URL(page.url()).searchParams.has('version'));assert.equal(new
URL(page.url()).hash,'#retained');
+ const links=await page.locator('section article
a').evaluateAll(nodes=>nodes.map(node=>node.getAttribute('href')));
+
assert(links.every(value=>value.includes('/ja/docs/dev/')));results.push({historicalToSupported:true,links});await
close(ctx);
+ }
const retryLabels={en:'Retry','zh-CN':'重试',ja:'再試行',ko:'다시
시도',ru:'Повторить'};
for(const locale of Object.keys(retryLabels)) for(const fullPage of
[false,true]) {
const mode={failBoth:true},{ctx,page,requests}=await context(mode);
diff --git a/src/hooks/use-search-query.ts b/src/hooks/use-search-query.ts
index ed984ae57fb5..267d82d42395 100644
--- a/src/hooks/use-search-query.ts
+++ b/src/hooks/use-search-query.ts
@@ -7,19 +7,19 @@
import { useHistory, useLocation } from "@docusaurus/router";
import useIsBrowser from "@docusaurus/useIsBrowser";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
-import { searchContextByPaths } from "../utils/proxiedGenerated";
+import { resolveSearchScope } from "../utils/searchScopes";
const SEARCH_PARAM_QUERY = "q";
const SEARCH_PARAM_CONTEXT = "ctx";
const SEARCH_PARAM_VERSION = "version";
-function useSearchQuery() {
+function useSearchQuery(scopeOptions) {
const isBrowser = useIsBrowser();
const history = useHistory();
const location = useLocation();
const { siteConfig: { baseUrl }, } = useDocusaurusContext();
const params = isBrowser ? new URLSearchParams(location.search) : null;
const searchValue = params?.get(SEARCH_PARAM_QUERY) || "";
- const searchContext = params?.get(SEARCH_PARAM_CONTEXT) || "";
- const searchVersion = params?.get(SEARCH_PARAM_VERSION) || "";
+ const scope =
resolveSearchScope({context:params?.get(SEARCH_PARAM_CONTEXT) || '',
+ version:params?.get(SEARCH_PARAM_VERSION) || ''}, scopeOptions);
const getSearchParams = (searchValue) => {
const searchParams = new URLSearchParams(location.search);
if (searchValue) {
@@ -32,25 +32,27 @@ function useSearchQuery() {
};
return {
searchValue,
- searchContext: searchContext &&
- Array.isArray(searchContextByPaths) &&
- searchContextByPaths.some((item) => typeof item === "string"
- ? item === searchContext
- : item.path === searchContext)
- ? searchContext
- : "",
- searchVersion,
+ searchContext:scope.context,
+ searchVersion:scope.version,
+ sourceOnlyVersion:scope.sourceOnlyVersion,
+ queryParams:params??new URLSearchParams(),
+ hash:location.hash,
updateSearchPath: (searchValue) => {
const searchParams = getSearchParams(searchValue);
history.replace({
+ pathname:location.pathname,
search: searchParams.toString(),
+ hash:location.hash,
});
},
updateSearchContext: (value) => {
const searchParams = new URLSearchParams(location.search);
searchParams.set(SEARCH_PARAM_CONTEXT, value);
+ searchParams.delete(SEARCH_PARAM_VERSION);
history.replace({
+ pathname:location.pathname,
search: searchParams.toString(),
+ hash:location.hash,
});
},
generateSearchPageLink: (searchValue) => {
diff --git a/src/theme/SearchBar/SearchBar.jsx
b/src/theme/SearchBar/SearchBar.jsx
index 2f713672ccfa..441b1a488969 100644
--- a/src/theme/SearchBar/SearchBar.jsx
+++ b/src/theme/SearchBar/SearchBar.jsx
@@ -31,6 +31,7 @@ import {fetchContextIndexes, searchAcrossContexts} from
'@site/src/utils/searchC
import {isTranslatedLocale} from '@site/src/utils/localization';
import {searchIndexBaseUrl} from '@site/src/utils/searchVersion';
import {searchMessages} from '@site/src/utils/searchMessages';
+import {docSearchSourceVersion} from '@site/src/utils/searchScopes';
const searchContextsByWorker = searchAcrossContexts(searchByWorker);
@@ -57,6 +58,8 @@ export default function SearchBar({ handleSearchBarToggle }) {
} = useDocusaurusContext();
const [isDocsPage] = useIsDocPage(false);
const sourceVersions = useAllDocsData()[docsPluginIdForPreferredVersion ??
'default']?.versions ?? [];
+ const sourceOnlyVersion = isTranslatedLocale(currentLocale)
+ ?
docSearchSourceVersion(location.pathname,sourceVersions,Array.isArray(searchContextByPaths)?searchContextByPaths:[])
: null;
const versionUrl = isTranslatedLocale(currentLocale) ? baseUrl
: searchIndexBaseUrl(baseUrl, sourceVersions, {pathname:
location.pathname});
@@ -65,6 +68,7 @@ export default function SearchBar({ handleSearchBarToggle }) {
const history = useHistory();
const searchBarRef = useRef(null);
+ const sourceSearchRef = useRef(null);
const indexStateMap = useRef(new Map());
// Should the input be focused after the index is loaded?
const focusAfterIndexLoaded = useRef(false);
@@ -122,11 +126,11 @@ export default function SearchBar({ handleSearchBarToggle
}) {
search.current?.autocomplete.destroy();
search.current = null;
};
- }, [versionUrl, searchContext]);
+ }, [versionUrl, searchContext, sourceOnlyVersion]);
const loadIndex = useCallback(
async (forceLoad = false) => {
- if (hidden || (indexStateMap.current.get(indexKey) && !forceLoad))
{
+ if (sourceOnlyVersion || hidden ||
(indexStateMap.current.get(indexKey) && !forceLoad)) {
// Do not load the index (again) if its already loaded or in
the process of being loaded.
return;
}
@@ -149,7 +153,7 @@ export default function SearchBar({ handleSearchBarToggle
}) {
setLoadError(true);
return;
}
- if (myToken !== loadToken.current) {
+ if (myToken !== loadToken.current || !searchBarRef.current) {
// Superseded by a newer loadIndex; don't mount a second
autocomplete on the same input.
return;
}
@@ -309,7 +313,7 @@ export default function SearchBar({ handleSearchBarToggle
}) {
input.focus();
}
},
- [hidden, indexKey, searchContext, versionUrl, baseUrl, history],
+ [hidden, indexKey, searchContext, versionUrl, baseUrl, history,
sourceOnlyVersion],
);
useEffect(() => {
if (!Mark) {
@@ -384,6 +388,7 @@ export default function SearchBar({ handleSearchBarToggle
}) {
const handleShortcut = event => {
if ((isMac ? event.metaKey : event.ctrlKey) && (event.key === 'k'
|| event.key === 'K')) {
event.preventDefault();
+ if(sourceOnlyVersion)
{sourceSearchRef.current?.focus();return;}
searchBarRef.current?.focus();
onInputFocus();
}
@@ -392,7 +397,7 @@ export default function SearchBar({ handleSearchBarToggle
}) {
return () => {
document.removeEventListener('keydown', handleShortcut);
};
- }, [isMac, onInputFocus]);
+ }, [isMac, onInputFocus, sourceOnlyVersion]);
useEffect(() => {
if (inputValue) {
const inputDoms =
document.getElementsByClassName('navbar__search-input');
@@ -426,6 +431,18 @@ export default function SearchBar({ handleSearchBarToggle
}) {
search.current?.autocomplete.setVal('');
}, [location.pathname, location.search, location.hash, history]);
+ if(sourceOnlyVersion) {
+ const
sourceBase=baseUrl.endsWith('/'+currentLocale+'/')?baseUrl.slice(0,-(currentLocale.length+1)):baseUrl;
+ const params=new
URLSearchParams({ctx:'docs',version:'docs/'+sourceOnlyVersion+'/'});
+ if(inputValue)params.set('q',inputValue);
+ return <div
className={clsx('navbar__search',styles.searchBarContainer)}>
+ <a ref={sourceSearchRef} className={styles.searchSourceLink}
href={`${sourceBase}search/?${params}`} hrefLang="en"
+ title={searchMessages(currentLocale).sourceOnly}
data-source-search={sourceOnlyVersion}>
+ {translate({id:'theme.SearchPage.searchSource',message:'Search
the source documents'})} ({sourceOnlyVersion})
+ </a>
+ </div>;
+ }
+
return (
<div
className={clsx('navbar__search', styles.searchBarContainer, {
diff --git a/src/theme/SearchBar/SearchBar.module.css
b/src/theme/SearchBar/SearchBar.module.css
index e9c6a418aa85..f73862884f9b 100644
--- a/src/theme/SearchBar/SearchBar.module.css
+++ b/src/theme/SearchBar/SearchBar.module.css
@@ -45,6 +45,22 @@
white-space: normal;
}
+.searchSourceLink {
+ display: inline-flex;
+ align-items: center;
+ box-sizing: border-box;
+ max-width: 100%;
+ min-height: 2.5rem;
+ padding: 0.25rem 0.5rem;
+ border: 1px solid var(--brand-border-soft);
+ border-radius: 6px;
+ background: var(--brand-surface-note);
+ color: var(--brand-ink);
+ font-size: 0.875rem;
+ line-height: 1.2;
+ white-space: normal;
+}
+
.searchBar .mobileDropdownMenu {
left: 0 !important;
}
diff --git a/src/theme/SearchPage/SearchPage.jsx
b/src/theme/SearchPage/SearchPage.jsx
index 0530fe0cadaa..ddd2e487965b 100644
--- a/src/theme/SearchPage/SearchPage.jsx
+++ b/src/theme/SearchPage/SearchPage.jsx
@@ -25,6 +25,7 @@ import {fetchContextIndexes, searchAcrossContexts} from
"../../utils/searchConte
import {isTranslatedLocale} from "../../utils/localization";
import {searchIndexBaseUrl} from "../../utils/searchVersion";
import {searchMessages} from "../../utils/searchMessages";
+import {convertSearchParams} from "../../utils/searchScopes";
const searchContextsByWorker = searchAcrossContexts(searchByWorker);
export default function SearchPage() {
return (<Layout>
@@ -32,16 +33,17 @@ export default function SearchPage() {
</Layout>);
}
function SearchPageContent() {
- const { siteConfig: { baseUrl }, i18n: { currentLocale }, } =
useDocusaurusContext();
+ const { siteConfig: { baseUrl,customFields }, i18n: {
currentLocale,defaultLocale }, } = useDocusaurusContext();
const isBrowser = useIsBrowser();
const { selectMessage } = usePluralForm();
- const { searchValue, searchContext, searchVersion, updateSearchPath,
updateSearchContext, } = useSearchQuery();
+ const sourceVersions = useAllDocsData()[docsPluginIdForPreferredVersion ??
'default']?.versions ?? [];
+ const { searchValue, searchContext, searchVersion, sourceOnlyVersion,
updateSearchPath, updateSearchContext, queryParams, hash } = useSearchQuery({
+
contexts:Array.isArray(searchContextByPaths)?searchContextByPaths:[],versions:sourceVersions,managed:isTranslatedLocale(currentLocale)});
const [searchQuery, setSearchQuery] = useState(searchValue);
const [searchResults, setSearchResults] = useState();
const [searchWorkerReady, setSearchWorkerReady] = useState(false);
const [loadError, setLoadError] = useState(false);
const [retry, setRetry] = useState(0);
- const sourceVersions = useAllDocsData()[docsPluginIdForPreferredVersion ??
'default']?.versions ?? [];
const versionUrl = isTranslatedLocale(currentLocale) ? baseUrl
: searchIndexBaseUrl(baseUrl, sourceVersions, {versionPath:
searchVersion});
const pageTitle = useMemo(() => searchQuery
@@ -71,12 +73,14 @@ function SearchPageContent() {
}
}, [searchValue]);
// The source index lives at the site root, so drop this locale's prefix.
- const sourceBaseUrl = isTranslatedLocale(currentLocale)
+ const sourceBaseUrl = currentLocale!==defaultLocale
? baseUrl.replace(new
RegExp(`${currentLocale.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/$`), '')
: baseUrl;
- const englishSearch = `${sourceBaseUrl}search/?${new URLSearchParams({q:
searchQuery,
- ...(searchContext?.startsWith('docs/') ? {ctx: 'docs'} : {}),
- ...(searchContext === 'docs/dev' ? {version: 'docs/dev/'} :
{})}).toString()}`;
+ const sourceParams=new URLSearchParams(queryParams);
+ if(searchQuery)sourceParams.set('q',searchQuery);else
sourceParams.delete('q');
+ const sourceContexts=customFields?.searchContexts?.source ??
['docs','community','course','releases'];
+ const englishSearch =
`${sourceBaseUrl}search/?${convertSearchParams(sourceParams,
+
{contexts:sourceContexts,versions:sourceVersions,managed:false}).toString()}${hash}`;
useEffect(() => {
let active = true;
setLoadError(false);
@@ -84,6 +88,7 @@ function SearchPageContent() {
setSearchResults(undefined);
async function doFetchIndexes() {
try {
+ if(sourceOnlyVersion)
{if(active)setSearchWorkerReady(true);return;}
if (!Array.isArray(searchContextByPaths) ||
searchContext ||
useAllContextsWithNoSearchContext) {
@@ -104,7 +109,7 @@ function SearchPageContent() {
doFetchIndexes();
}
return () => {active = false;};
- }, [isBrowser, searchContext, versionUrl, searchQuery, retry]);
+ }, [isBrowser, searchContext, versionUrl, searchQuery, retry,
sourceOnlyVersion]);
return (<React.Fragment>
<Head>
{/*
@@ -127,7 +132,8 @@ function SearchPageContent() {
<input type="search" name="q" className={styles.searchQueryInput}
aria-label={translate({id: 'theme.SearchBar.label', message: 'Search'})}
onChange={handleSearchInputChange} value={searchQuery} autoComplete="off"
autoFocus/>
</div>
{Array.isArray(searchContextByPaths) ? (<div className={clsx("col",
"col--3", "padding-left--none", styles.searchContextColumn)}>
- <select name="search-context"
className={styles.searchContextInput} aria-label={translate({id:
'site.search.context', message: 'Search context'})} id="context-selector"
value={searchContext} onChange={(e) => updateSearchContext(e.target.value)}>
+ <select name="search-context"
className={styles.searchContextInput} aria-label={translate({id:
'site.search.context', message: 'Search context'})} id="context-selector"
value={sourceOnlyVersion?'docs/'+sourceOnlyVersion:searchContext} onChange={(e)
=> updateSearchContext(e.target.value)}>
+ {sourceOnlyVersion&&<option value={'docs/'+sourceOnlyVersion}
disabled>{sourceOnlyVersion} ·
{searchMessages(currentLocale).sourceOnly}</option>}
{useAllContextsWithNoSearchContext && (<option value="">
{translate({
id: "theme.SearchPage.searchContext.everywhere",
@@ -144,6 +150,10 @@ function SearchPageContent() {
</div>) : null}
</div>
+ {sourceOnlyVersion&&<p
role="note">{searchMessages(currentLocale).sourceOnly}{' '}
+ <a href={englishSearch}
hrefLang="en">{translate({id:'theme.SearchPage.searchSource',message:'Search
the source documents'})}</a>
+ </p>}
+
{!searchWorkerReady && !loadError && searchQuery && (<div>
<LoadingRing />
</div>)}
@@ -167,7 +177,7 @@ function SearchPageContent() {
message: "No documents were found",
description: "The paragraph for empty search result",
})}
- {isTranslatedLocale(currentLocale) && <> <a
href={englishSearch}>{translate({id: 'theme.SearchPage.searchSource', message:
'Search the source documents'})}</a></>}
+ {currentLocale!==defaultLocale && <> <a href={englishSearch}
hrefLang="en">{translate({id: 'theme.SearchPage.searchSource', message: 'Search
the source documents'})}</a></>}
</p>) : (<p>
⚠️ The search index is only available when you run docusaurus
build!
diff --git a/src/utils/searchMessages.ts b/src/utils/searchMessages.ts
index 97ac699b1b47..0ad0f72be7c1 100644
--- a/src/utils/searchMessages.ts
+++ b/src/utils/searchMessages.ts
@@ -1,8 +1,8 @@
const messages = {
- en: {error: 'Search could not load.', retry: 'Retry'},
- 'zh-CN': {error: '搜索加载失败。', retry: '重试'},
- ja: {error: '検索を読み込めませんでした。', retry: '再試行'},
- ko: {error: '검색을 불러오지 못했습니다.', retry: '다시 시도'},
- ru: {error: 'Не удалось загрузить поиск.', retry: 'Повторить'},
+ en: {error: 'Search could not load.', retry: 'Retry', sourceOnly:'Search
for this version is available in English.'},
+ 'zh-CN': {error: '搜索加载失败。', retry: '重试', sourceOnly:'此版本可在英文文档中搜索。'},
+ ja: {error: '検索を読み込めませんでした。', retry: '再試行',
sourceOnly:'このバージョンは英語のドキュメントで検索できます。'},
+ ko: {error: '검색을 불러오지 못했습니다.', retry: '다시 시도', sourceOnly:'이 버전은 영어 문서에서
검색할 수 있습니다.'},
+ ru: {error: 'Не удалось загрузить поиск.', retry: 'Повторить',
sourceOnly:'Поиск по этой версии доступен в английской документации.'},
};
export function searchMessages(locale: string) {return messages[locale] ??
messages.en;}
diff --git a/src/utils/searchScopes.js b/src/utils/searchScopes.js
new file mode 100644
index 000000000000..e23d2eb25e04
--- /dev/null
+++ b/src/utils/searchScopes.js
@@ -0,0 +1,47 @@
+const pathOf = item => typeof item === 'string' ? item : item.path;
+export const versionId = version =>
version?.path?.split('/').filter(Boolean).pop();
+export const defaultSearchVersion = versions =>
versionId(versions.find(version => version.isLast) ?? versions[0]) ?? 'dev';
+
+// Source-language indexes use ctx=docs plus a version path. Managed languages
+// use ctx=docs/<version>. Translate that logical scope, not just the URL
prefix.
+export function resolveSearchScope({context = '', version = ''}, {contexts =
[], versions = [], managed = false} = {}) {
+ const paths = contexts.map(pathOf), fallback = managed ? '' : paths[0] ??
'';
+ if (context && context !== 'docs' && !context.startsWith('docs/') &&
paths.includes(context))
+ return {context, version:'', sourceOnlyVersion:null};
+ const requested = context.startsWith('docs/') ? context.slice(5)
+ : version.match(/^\/?docs\/([^/]+)\/?$/)?.[1];
+ const known = versions.map(versionId);
+ if (context === 'docs' || context.startsWith('docs/') || requested) {
+ const selected = known.includes(requested) ? requested :
defaultSearchVersion(versions);
+ if (managed) {
+ const target = 'docs/' + selected;
+ return paths.includes(target) ?
{context:target,version:'',sourceOnlyVersion:null}
+ :
{context:'',version:'docs/'+selected+'/',sourceOnlyVersion:selected};
+ }
+ return {context:paths.includes('docs')?'docs':fallback,
+ version:selected === defaultSearchVersion(versions) ? '' :
'docs/'+selected+'/',sourceOnlyVersion:null};
+ }
+ return
{context:paths.includes(context)?context:fallback,version:'',sourceOnlyVersion:null};
+}
+
+export function convertSearchParams(params, options, sourceOptions) {
+ const result = new URLSearchParams(params);
+ if(sourceOptions) {
+ const
current=resolveSearchScope({context:result.get('ctx')??'',version:result.get('version')??''},sourceOptions);
+ if(current.sourceOnlyVersion)result.set('ctx','docs');
+ else if(current.context)result.set('ctx',current.context);else
result.delete('ctx');
+ if(current.version)result.set('version',current.version);else
result.delete('version');
+ }
+ const scope =
resolveSearchScope({context:result.get('ctx')??'',version:result.get('version')??''},options);
+ const context = scope.sourceOnlyVersion ? 'docs' : scope.context;
+ if (context) result.set('ctx',context); else result.delete('ctx');
+ if (scope.version) result.set('version',scope.version); else
result.delete('version');
+ return result;
+}
+
+export function docSearchSourceVersion(pathname, versions, contexts) {
+ const version = versions.filter(value => pathname === value.path ||
pathname.startsWith(value.path.replace(/\/$/,'')+'/'))
+ .sort((a,b)=>b.path.length-a.path.length)[0];
+ const id = versionId(version);
+ return id && !contexts.map(pathOf).includes('docs/'+id) ? id : null;
+}
diff --git a/src/utils/useLanguageItems.ts b/src/utils/useLanguageItems.ts
index e5fef775230e..b9fe1035d369 100644
--- a/src/utils/useLanguageItems.ts
+++ b/src/utils/useLanguageItems.ts
@@ -6,6 +6,9 @@ import {useLocalizedRoute} from './useLocalizedRoute';
import {saveLocalePreference} from './localePreference';
import {appendRouteSuffix, rootBaseUrl} from './localizedRoute';
import {languageMessages} from './languageMessages';
+import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
+import {isTranslatedLocale} from './localization';
+import {convertSearchParams} from './searchScopes';
export function useLanguageItems(queryString = '') {
const {i18n: {locales, localeConfigs, currentLocale, defaultLocale},
siteConfig} = useDocusaurusContext();
@@ -13,13 +16,22 @@ export function useLanguageItems(queryString = '') {
const browser = useIsBrowser();
const page = useContentRouteData();
const resolve = useLocalizedRoute();
+ const versions = useAllDocsData().default?.versions ?? [];
return locales.map(locale => {
const route = resolve(pathname, {locale, switchLocale: true});
const root = rootBaseUrl(siteConfig.baseUrl, currentLocale,
defaultLocale);
// An English root has no locale prefix. Keep this explicit user action
// authoritative even when storage is unavailable in this browser.
const englishRoot = locale === defaultLocale &&
route.href.split(/[?#]/)[0].replace(/\/$/, '') === root.replace(/\/$/, '');
- let href = appendRouteSuffix(route.href, browser ? search : '',
browser ? hash : '', queryString);
+ const relative = pathname.startsWith(root) ?
pathname.slice(root.length).replace(new
RegExp(`^(?:${locales.filter(value=>value!==defaultLocale).join('|')})/`),'') :
'';
+ const scopes = siteConfig.customFields?.searchContexts as
{source?:string[];managed?:string[]} | undefined;
+ const contexts = isTranslatedLocale(locale) ? scopes?.managed :
scopes?.source;
+ const sourceContexts = isTranslatedLocale(currentLocale) ?
scopes?.managed : scopes?.source;
+ const suffix = browser && relative.replace(/\/$/,'') === 'search' &&
contexts
+ ? '?' + convertSearchParams(new
URLSearchParams(search),{contexts,versions,managed:isTranslatedLocale(locale)},
+
sourceContexts?{contexts:sourceContexts,versions,managed:isTranslatedLocale(currentLocale)}:undefined).toString()
+ : browser ? search : '';
+ let href = appendRouteSuffix(route.href, suffix, browser ? hash : '',
queryString);
if (englishRoot) href = appendRouteSuffix(href, '', '',
`lang=${defaultLocale}`);
return {locale, label: localeConfigs[locale].label, htmlLang:
localeConfigs[locale].htmlLang,
href,
diff --git a/translations/shared/registry.json
b/translations/shared/registry.json
index 753300e52ef7..42969316ce52 100644
--- a/translations/shared/registry.json
+++ b/translations/shared/registry.json
@@ -67,6 +67,7 @@
"src/utils/searchInput.js",
"src/utils/searchMessages.ts",
"src/utils/searchNormalization.js",
+ "src/utils/searchScopes.js",
"src/utils/searchVersion.js",
"src/utils/tokenize.ts",
"src/utils/translation.ts",
@@ -174,7 +175,7 @@
"theme.SearchPage.noResultsText": "No documents were found",
"theme.SearchPage.searchSource": "Search the source documents"
},
- "sourceHash":
"9032b3ab8106811aad12c49fddb2b59f83e80c7f8892f2e532db634f446e1595",
+ "sourceHash":
"3de2fa30873950f25e9d3d9a4d333904c39dd0881ea9b4cf1dda98070e87d20d",
"entryFiles": [
"src/components/home-next/NavbarNext.tsx",
"src/components/home-next/SlackCommunityNudge.tsx",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]