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 c156ec9e83e [UI] Add section-scoped docs navigation (#4147)
c156ec9e83e is described below
commit c156ec9e83ecc16f0b9c5e35f9f3396bdbc9a797
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Wed Sep 16 22:47:14 2026 +0800
[UI] Add section-scoped docs navigation (#4147)
## Summary
- promote top-level documentation sections into a lightweight horizontal
navigation bar for dev and 4.x
- scope the left sidebar to the active documentation section
- move Search and language controls into the docs navigation bar
- rename the new `Use Doris` section to `Guides` in dev and 4.x
- promote `SQL References` from `Operate & Maintain` to its own
top-level section in dev and 4.x
- remove the repeated current-domain heading and version picker from the
new navigation layout
- preserve the legacy full-sidebar navigation and utility controls for
3.x and 2.1
- add responsive mobile section selection and keep community, release,
and course navigation unchanged
## Validation
- targeted TypeScript check passed for all touched React/TypeScript
files
- JSON parsing passed for the updated sidebar and Chinese translation
files
- Docusaurus development compilation passed with all active docs
versions enabled in English and Chinese
- Docusaurus production build passed for the current docs version
- manually verified section switching and sidebar scoping for dev and
4.x, including `SQL References`
- verified the version picker is absent from the desktop and mobile
section navigation while Search and language controls remain available
- verified the 3.x and 2.1 English and Chinese routes return 200 and
retain their seven-section legacy sidebar
- verified the legacy 3.x and 2.1 navigation at 997px, 1024px, 1080px,
1280px, and 1600px without overlap or horizontal overflow
- verified the five-section dev/4.x navigation from 997px through mobile
without overlap or horizontal overflow
- verified the 390px mobile layout and drawer without horizontal
overflow or runtime errors
- `git diff --check` passed
## Versions
- [x] dev
- [x] 4.x
- [x] 3.x
- [x] 2.1 or older (legacy navigation preserved)
## Languages
- [x] Chinese
- [x] English
The navigation components are shared by both locales. The Chinese
translation keys for the renamed section were updated alongside the
English sidebar labels; documentation page content is unchanged.
## 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]>
---
i18n/zh-CN/code.json | 17 +-
.../docusaurus-plugin-content-docs/current.json | 4 +-
.../version-4.x.json | 4 +-
sidebars.ts | 11 +-
.../docs-domain-nav/DocsDomainNav.module.scss | 347 +++++++++++++++++++++
src/components/docs-domain-nav/DocsDomainNav.tsx | 76 +++++
src/components/home-next/NavbarNext.tsx | 32 +-
src/styles.d.ts | 4 +
src/theme/BlogFooter/index.tsx | 10 +-
src/theme/BlogListPage/blog-search.logic.d.ts | 21 ++
src/theme/BlogListPage/blog-search.logic.js | 59 ++++
src/theme/BlogListPage/blog-search.logic.test.js | 56 ++++
src/theme/BlogListPage/index.tsx | 193 ++++++++----
src/theme/BlogListPage/styles.module.scss | 108 +++++++
.../DocItem/Layout/MobileSidebarDrawer.module.css | 12 +
src/theme/DocItem/Layout/MobileSidebarDrawer.tsx | 141 +++++----
src/theme/DocItem/Layout/styles.module.css | 11 +-
src/theme/DocRoot/Layout/index.tsx | 23 +-
src/theme/DocRoot/Layout/styles.module.css | 34 ++
src/theme/DocSidebar/Desktop/index.tsx | 22 +-
src/theme/DocSidebar/Desktop/styles.module.css | 4 +
src/theme/DocSidebarItem/Category/index.tsx | 123 ++++----
src/theme/DocSidebarItem/Category/style.scss | 26 --
.../NavbarItem/LocaleDropdownNavbarItem/index.tsx | 3 +-
.../LocaleDropdownNavbarItem/styles.scss | 5 +
src/theme/TOC/styles.module.css | 4 +-
src/utils/docs-sidebar-scope.ts | 60 ++++
versioned_sidebars/version-4.x-sidebars.json | 19 +-
28 files changed, 1163 insertions(+), 266 deletions(-)
diff --git a/i18n/zh-CN/code.json b/i18n/zh-CN/code.json
index ca68b95015e..7431bd6ae6b 100644
--- a/i18n/zh-CN/code.json
+++ b/i18n/zh-CN/code.json
@@ -7,5 +7,20 @@
},
"theme.docs.versions.unreleasedText": {
"message": "尚未发布"
+ },
+ "blog.search.label": {
+ "message": "搜索博客"
+ },
+ "blog.search.placeholder": {
+ "message": "按标题、摘要、标签或作者搜索"
+ },
+ "blog.search.clear": {
+ "message": "清除"
+ },
+ "blog.search.resultCount": {
+ "message": "找到 {count} 篇博客"
+ },
+ "blog.search.noResults": {
+ "message": "当前搜索词和分类下没有匹配的博客。"
}
-}
\ No newline at end of file
+}
diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/current.json
b/i18n/zh-CN/docusaurus-plugin-content-docs/current.json
index 824ca792d26..3bd03320aeb 100644
--- a/i18n/zh-CN/docusaurus-plugin-content-docs/current.json
+++ b/i18n/zh-CN/docusaurus-plugin-content-docs/current.json
@@ -19,9 +19,9 @@
"message": "管理指南",
"description": "The label for category Management in sidebar docs"
},
- "sidebar.docs.category.Reference": {
+ "sidebar.docs.category.SQL References": {
"message": "SQL 手册",
- "description": "The label for category Reference in sidebar docs"
+ "description": "The label for category SQL References in sidebar docs"
},
"sidebar.docs.category.Lakehouse Best Practices": {
"message": "湖仓一体最佳实践",
diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/version-4.x.json
b/i18n/zh-CN/docusaurus-plugin-content-docs/version-4.x.json
index c5628961bcf..96a00ff1c68 100644
--- a/i18n/zh-CN/docusaurus-plugin-content-docs/version-4.x.json
+++ b/i18n/zh-CN/docusaurus-plugin-content-docs/version-4.x.json
@@ -19,9 +19,9 @@
"message": "管理指南",
"description": "The label for category Management in sidebar docs"
},
- "sidebar.docs.category.Reference": {
+ "sidebar.docs.category.SQL References": {
"message": "SQL 手册",
- "description": "The label for category Reference in sidebar docs"
+ "description": "The label for category SQL References in sidebar docs"
},
"sidebar.docs.category.Lakehouse Best Practices": {
"message": "湖仓一体最佳实践",
diff --git a/sidebars.ts b/sidebars.ts
index 857c1dd7b24..0f6bf552d1b 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -210,7 +210,7 @@ const sidebars: SidebarsConfig = {
},
{
type: 'category',
- label: 'Use Doris',
+ label: 'Guides',
collapsible: false,
collapsed: false,
items: [
@@ -1221,11 +1221,14 @@ const sidebars: SidebarsConfig = {
],
},
],
+ },
+ ],
},
{
type: 'category',
- label: 'Reference',
- collapsed: true,
+ label: 'SQL References',
+ collapsible: false,
+ collapsed: false,
items: [
{
type: 'category',
@@ -2784,8 +2787,6 @@ const sidebars: SidebarsConfig = {
},
],
},
- ],
- },
],
};
diff --git a/src/components/docs-domain-nav/DocsDomainNav.module.scss
b/src/components/docs-domain-nav/DocsDomainNav.module.scss
new file mode 100644
index 00000000000..8979bdf1442
--- /dev/null
+++ b/src/components/docs-domain-nav/DocsDomainNav.module.scss
@@ -0,0 +1,347 @@
+.root {
+ position: sticky;
+ top: calc(var(--home-next-banner-height, 0px) +
var(--docs-global-navbar-height, 64px));
+ z-index: 250;
+ width: 100%;
+ min-height: var(--docs-domain-nav-height, 64px);
+ background: var(--global-colors-white);
+ border-bottom: 1px solid var(--brand-border-soft);
+}
+
+.inner {
+ box-sizing: border-box;
+ width: 100%;
+ min-height: var(--docs-domain-nav-height, 64px);
+ margin: 0 auto;
+ padding: 0 28px;
+ display: flex;
+ align-items: stretch;
+ gap: 12px;
+}
+
+.domainList {
+ display: flex;
+ align-items: stretch;
+ min-width: 0;
+}
+
+.domainLink {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ padding: 0 18px;
+ color: var(--brand-ink-soft);
+ font-size: 15px;
+ font-weight: 500;
+ text-decoration: none;
+ white-space: nowrap;
+ transition: color 0.15s ease, background-color 0.15s ease;
+
+ &:hover {
+ color: var(--brand-primary-deep);
+ background: var(--brand-surface-faint);
+ text-decoration: none;
+ }
+
+ &[aria-current='page'] {
+ color: var(--brand-primary-deep);
+ background: transparent;
+ font-weight: 650;
+ }
+
+ &[aria-current='page']::after {
+ content: '';
+ position: absolute;
+ right: 18px;
+ bottom: 0;
+ left: 18px;
+ height: 3px;
+ border-radius: 3px 3px 0 0;
+ background: var(--brand-primary);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--brand-primary);
+ outline-offset: -4px;
+ }
+}
+
+.utilities {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ margin-left: auto;
+ padding-left: 18px;
+ border-left: 1px solid var(--brand-border-soft);
+ flex: 0 0 auto;
+}
+
+.search {
+ width: clamp(210px, 20vw, 300px);
+ margin-right: 8px;
+}
+
+.search :global(.navbar__search) {
+ width: 100%;
+ margin-left: 0;
+}
+
+.search :global(.navbar__search-input) {
+ width: 100% !important;
+ height: 38px;
+ padding-left: 2.55rem;
+ border: 1px solid var(--brand-border-soft);
+ border-radius: 6px;
+ background-color: var(--brand-surface-faint);
+ color: var(--brand-ink);
+}
+
+.search :global(.navbar__search-input)::placeholder {
+ color: var(--global-colors-text-secondary);
+}
+
+.search :global(.navbar__search-input):hover,
+.search :global(.navbar__search-input):focus {
+ border-color: var(--brand-primary);
+ background-color: var(--global-colors-white);
+ outline: none;
+}
+
+.search :global([class*='dropdownMenu']) {
+ left: auto !important;
+ right: 0 !important;
+}
+
+.search :global(kbd) {
+ color: var(--global-colors-text-secondary);
+ border-color: var(--brand-border-soft);
+ background: var(--global-colors-white);
+}
+
+.locale {
+ display: flex;
+ align-items: center;
+}
+
+.locale :global(.navbar__item) {
+ display: flex !important;
+ padding: 0;
+}
+
+.locale :global(.navbar__link) {
+ box-sizing: border-box;
+ min-height: 38px;
+ margin: 0;
+ padding: 0 10px;
+ gap: 6px;
+ border: 0;
+ border-radius: 4px;
+ background: transparent;
+ color: var(--brand-ink-soft) !important;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ font-weight: 550 !important;
+ text-decoration: none;
+}
+
+.locale :global(.navbar__link):hover {
+ background: var(--brand-surface-soft);
+ color: var(--brand-primary-deep) !important;
+}
+
+.locale :global(.navbar__link)::after {
+ opacity: 0.55;
+}
+
+.locale :global(.dropdown__menu) {
+ right: 0;
+ left: auto;
+ min-width: 9rem;
+}
+
+.mobileDomainPicker {
+ display: none;
+}
+
+@media (max-width: 1280px) {
+ .inner {
+ padding-inline: 20px;
+ }
+
+ .domainLink {
+ padding-inline: 13px;
+ }
+
+ .search {
+ width: clamp(180px, 18vw, 250px);
+ }
+}
+
+@media (max-width: 1080px) {
+ .domainLink {
+ padding-inline: 10px;
+ font-size: 13px;
+ }
+
+ .search {
+ width: 180px;
+ }
+
+ .locale :global(.locale-current-label) {
+ display: none;
+ }
+}
+
+@media (max-width: 996px) {
+ .root,
+ .inner {
+ min-height: var(--docs-domain-nav-height, 56px);
+ }
+
+ .inner {
+ align-items: center;
+ padding-inline: 12px;
+ gap: 6px;
+ }
+
+ .domainList {
+ display: none;
+ }
+
+ .mobileDomainPicker {
+ position: relative;
+ display: block;
+ flex: 0 1 150px;
+ min-width: 112px;
+
+ summary {
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ min-height: 38px;
+ padding: 0 11px;
+ list-style: none;
+ border: 1px solid var(--brand-border-soft);
+ border-radius: 6px;
+ color: var(--brand-primary-deep);
+ font-size: 13px;
+ font-weight: 650;
+ cursor: pointer;
+ white-space: nowrap;
+
+ &::-webkit-details-marker {
+ display: none;
+ }
+
+ svg {
+ width: 14px;
+ height: 14px;
+ flex: none;
+ transition: transform 0.15s ease;
+ }
+ }
+
+ &[open] summary svg {
+ transform: rotate(180deg);
+ }
+ }
+
+ .mobileDomainMenu {
+ position: absolute;
+ top: calc(100% + 6px);
+ left: 0;
+ z-index: 5;
+ width: min(260px, calc(100vw - 24px));
+ padding: 6px;
+ border: 1px solid var(--brand-border-soft);
+ border-radius: 8px;
+ background: var(--brand-paper);
+ box-shadow: 0 12px 30px rgb(var(--brand-shadow-rgb) / 20%);
+ }
+
+ .mobileDomainLink {
+ display: block;
+ padding: 9px 11px;
+ border-radius: 4px;
+ color: var(--brand-ink);
+ font-size: 14px;
+ font-weight: 550;
+ text-decoration: none;
+
+ &:hover {
+ background: var(--brand-surface-soft);
+ color: var(--brand-primary-deep);
+ text-decoration: none;
+ }
+
+ &[aria-current='page'] {
+ background: var(--brand-surface-note);
+ color: var(--brand-primary-deep);
+ font-weight: 700;
+ }
+ }
+
+ .utilities {
+ min-width: 0;
+ gap: 4px;
+ padding-left: 6px;
+ }
+
+ .search {
+ width: clamp(76px, 22vw, 132px);
+ }
+
+ .search :global(.navbar__search-input) {
+ height: 38px;
+ min-width: 0;
+ padding-right: 0.5rem;
+ }
+
+ .search :global([class*='searchHintContainer']) {
+ display: none !important;
+ }
+
+ .locale :global(.locale-current-label) {
+ display: none;
+ }
+
+ .locale :global(.navbar__link) {
+ display: inline-flex !important;
+ min-width: 38px;
+ min-height: 38px;
+ justify-content: center;
+ padding-inline: 9px;
+ }
+}
+
+@media (max-width: 420px) {
+ .mobileDomainPicker {
+ flex-basis: 128px;
+ }
+
+ .mobileDomainPicker summary {
+ overflow: hidden;
+ }
+
+ .mobileDomainPicker summary span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .search {
+ width: 40px;
+ margin-right: 0;
+ }
+
+ .search :global(.navbar__search-input) {
+ padding-right: 0;
+ color: transparent;
+ caret-color: var(--brand-primary-deep);
+ }
+
+ .search :global(.navbar__search-input)::placeholder {
+ color: transparent;
+ }
+}
diff --git a/src/components/docs-domain-nav/DocsDomainNav.tsx
b/src/components/docs-domain-nav/DocsDomainNav.tsx
new file mode 100644
index 00000000000..f86a2aae791
--- /dev/null
+++ b/src/components/docs-domain-nav/DocsDomainNav.tsx
@@ -0,0 +1,76 @@
+import React, { type JSX } from 'react';
+import Link from '@docusaurus/Link';
+import SearchBar from '@theme/SearchBar';
+import LocaleDropdownNavbarItem from
'@theme/NavbarItem/LocaleDropdownNavbarItem';
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+import type { DocsDomain } from '@site/src/utils/docs-sidebar-scope';
+
+import styles from './DocsDomainNav.module.scss';
+
+interface DocsDomainNavProps {
+ domains: DocsDomain[];
+}
+
+function DomainLinks({ domains, mobile = false }: { domains: DocsDomain[];
mobile?: boolean }): JSX.Element {
+ return (
+ <>
+ {domains.map(domain => (
+ <Link
+ key={domain.label}
+ to={domain.href}
+ className={mobile ? styles.mobileDomainLink :
styles.domainLink}
+ aria-current={domain.isActive ? 'page' : undefined}
+ onClick={event => {
+ if (!mobile) return;
+
event.currentTarget.closest('details')?.removeAttribute('open');
+ }}
+ >
+ {domain.label}
+ </Link>
+ ))}
+ </>
+ );
+}
+
+export default function DocsDomainNav({ domains }: DocsDomainNavProps):
JSX.Element | null {
+ const {
+ i18n: { currentLocale },
+ } = useDocusaurusContext();
+ const activeDomain = domains.find(domain => domain.isActive) ?? domains[0];
+ const isZH = currentLocale === 'zh-CN';
+
+ if (!activeDomain) {
+ return null;
+ }
+
+ return (
+ <nav className={styles.root} aria-label={isZH ? '文档分类导航' :
'Documentation sections'}>
+ <div className={styles.inner}>
+ <div className={styles.domainList}>
+ <DomainLinks domains={domains} />
+ </div>
+
+ <details className={styles.mobileDomainPicker}>
+ <summary>
+ <span>{activeDomain.label}</span>
+ <svg viewBox="0 0 16 16" aria-hidden="true">
+ <path d="m4 6 4 4 4-4" fill="none"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"
strokeLinejoin="round" />
+ </svg>
+ </summary>
+ <div className={styles.mobileDomainMenu}>
+ <DomainLinks domains={domains} mobile />
+ </div>
+ </details>
+
+ <div className={styles.utilities}>
+ <div className={styles.search}>
+ <SearchBar />
+ </div>
+ <div className={styles.locale}>
+ <LocaleDropdownNavbarItem mobile={false} {...({} as
any)} />
+ </div>
+ </div>
+ </div>
+ </nav>
+ );
+}
diff --git a/src/components/home-next/NavbarNext.tsx
b/src/components/home-next/NavbarNext.tsx
index 95160d5a32e..adb90e659b2 100644
--- a/src/components/home-next/NavbarNext.tsx
+++ b/src/components/home-next/NavbarNext.tsx
@@ -1,4 +1,4 @@
-import React, { JSX, useState, useEffect } from 'react';
+import React, { JSX, useState, useEffect, useRef } from 'react';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { getLocalePrefix } from '@site/src/utils/locale';
@@ -65,7 +65,6 @@ function buildNavItems(
items: [
{ label: 'Release Notes', href: releasesHref },
{ label: 'Blogs', href: '/blog' },
- { label: 'News and Events', href: '/events' },
{ label: 'Course', href: '/course' },
{ label: 'Profile Analysis', href: '/profile-analysis' },
],
@@ -123,6 +122,7 @@ export function NavbarNext(): JSX.Element {
i18n: { currentLocale, defaultLocale },
} = useDocusaurusContext();
const [mobileOpen, setMobileOpen] = useState(false);
+ const navbarRef = useRef<HTMLElement>(null);
const localePrefix = getLocalePrefix(currentLocale, defaultLocale);
const devDocsHref =
`${localePrefix}/docs/dev/getting-started/what-is-apache-doris`;
const stableDocsHref =
`${localePrefix}/docs/4.x/getting-started/what-is-apache-doris`;
@@ -157,8 +157,34 @@ export function NavbarNext(): JSX.Element {
return () => window.removeEventListener('keydown', onKeyDown);
}, [mobileOpen]);
+ useEffect(() => {
+ const navbar = navbarRef.current;
+ if (!navbar) return undefined;
+
+ const root = document.documentElement;
+ const updateNavbarHeight = () => {
+ root.style.setProperty('--navbar-next-height',
`${navbar.getBoundingClientRect().height}px`);
+ };
+ updateNavbarHeight();
+
+ const resizeObserver = typeof ResizeObserver !== 'undefined'
+ ? new ResizeObserver(updateNavbarHeight)
+ : undefined;
+ resizeObserver?.observe(navbar);
+ window.addEventListener('resize', updateNavbarHeight);
+
+ return () => {
+ resizeObserver?.disconnect();
+ window.removeEventListener('resize', updateNavbarHeight);
+ root.style.removeProperty('--navbar-next-height');
+ };
+ }, []);
+
return (
- <nav className={`navbar navbar--fixed-top navbar-next${mobileOpen ? '
navbar-next--mobile-open' : ''}`}>
+ <nav
+ ref={navbarRef}
+ className={`navbar navbar--fixed-top navbar-next${mobileOpen ? '
navbar-next--mobile-open' : ''}`}
+ >
<div className="navbar-next__inner">
<Link to={homeHref} className="navbar-next__logo"
aria-label="Apache Doris">
<img
diff --git a/src/styles.d.ts b/src/styles.d.ts
new file mode 100644
index 00000000000..3de784947ee
--- /dev/null
+++ b/src/styles.d.ts
@@ -0,0 +1,4 @@
+declare module '*.module.scss' {
+ const classes: { readonly [key: string]: string };
+ export default classes;
+}
diff --git a/src/theme/BlogFooter/index.tsx b/src/theme/BlogFooter/index.tsx
index 63ccca677c2..95748d1bcbd 100644
--- a/src/theme/BlogFooter/index.tsx
+++ b/src/theme/BlogFooter/index.tsx
@@ -21,12 +21,10 @@ export default function BlogListFooter({
<Pagination
responsive
onChange={page => {
- history.push(
- `${location.pathname}?currentPage=${page ? page :
''}¤tCategory=${encodeURIComponent(
- currentCategory ? currentCategory : '',
- )}#blog`,
- location.state,
- );
+ const params = new URLSearchParams(location.search);
+ params.set('currentPage', String(page || 1));
+ params.set('currentCategory', currentCategory || 'All');
+
history.push(`${location.pathname}?${params.toString()}#blog`, location.state);
}}
defaultPageSize={9}
current={currentPage}
diff --git a/src/theme/BlogListPage/blog-search.logic.d.ts
b/src/theme/BlogListPage/blog-search.logic.d.ts
new file mode 100644
index 00000000000..eaec9ad435a
--- /dev/null
+++ b/src/theme/BlogListPage/blog-search.logic.d.ts
@@ -0,0 +1,21 @@
+export interface BlogSearchItem {
+ frontMatter?: {
+ title?: string;
+ summary?: string;
+ description?: string;
+ author?: string | { name?: string };
+ authors?: Array<string | { name?: string }>;
+ tags?: Array<string | { label?: string; name?: string }>;
+ keywords?: string | Array<string | { label?: string; name?: string }>;
+ };
+ metadata?: {
+ title?: string;
+ authors?: Array<string | { name?: string }>;
+ tags?: Array<string | { label?: string; name?: string }>;
+ };
+}
+
+export function normalizeSearchValue(value: unknown): string;
+export function getBlogSearchText(blog: BlogSearchItem): string;
+export function matchesBlogSearch(blog: BlogSearchItem, query: string):
boolean;
+export function filterBlogs<T extends BlogSearchItem>(blogs: T[], query:
string): T[];
diff --git a/src/theme/BlogListPage/blog-search.logic.js
b/src/theme/BlogListPage/blog-search.logic.js
new file mode 100644
index 00000000000..ccb875443d1
--- /dev/null
+++ b/src/theme/BlogListPage/blog-search.logic.js
@@ -0,0 +1,59 @@
+function normalizeSearchValue(value) {
+ return String(value ?? '')
+ .normalize('NFKC')
+ .toLocaleLowerCase();
+}
+
+function getNamedValues(values) {
+ if (values == null) {
+ return [];
+ }
+
+ const items = Array.isArray(values) ? values : [values];
+ return items.map(value =>
+ typeof value === 'string' || typeof value === 'number' ? value :
value?.label ?? value?.name ?? '',
+ );
+}
+
+function getBlogSearchText(BlogPostContent) {
+ const frontMatter = BlogPostContent?.frontMatter ?? {};
+ const metadata = BlogPostContent?.metadata ?? {};
+
+ return normalizeSearchValue(
+ [
+ metadata.title,
+ frontMatter.title,
+ frontMatter.summary,
+ frontMatter.description,
+ ...getNamedValues(frontMatter.author),
+ ...getNamedValues(frontMatter.authors),
+ ...getNamedValues(metadata.authors),
+ ...getNamedValues(frontMatter.tags),
+ ...getNamedValues(metadata.tags),
+ ...getNamedValues(frontMatter.keywords),
+ ]
+ .filter(Boolean)
+ .join(' '),
+ );
+}
+
+function matchesBlogSearch(BlogPostContent, query) {
+ const terms =
normalizeSearchValue(query).trim().split(/\s+/).filter(Boolean);
+ if (terms.length === 0) {
+ return true;
+ }
+
+ const searchText = getBlogSearchText(BlogPostContent);
+ return terms.every(term => searchText.includes(term));
+}
+
+function filterBlogs(blogs, query) {
+ return blogs.filter(blog => matchesBlogSearch(blog, query));
+}
+
+module.exports = {
+ filterBlogs,
+ getBlogSearchText,
+ matchesBlogSearch,
+ normalizeSearchValue,
+};
diff --git a/src/theme/BlogListPage/blog-search.logic.test.js
b/src/theme/BlogListPage/blog-search.logic.test.js
new file mode 100644
index 00000000000..94132c017f8
--- /dev/null
+++ b/src/theme/BlogListPage/blog-search.logic.test.js
@@ -0,0 +1,56 @@
+const assert = require('node:assert/strict');
+const test = require('node:test');
+
+const { filterBlogs, matchesBlogSearch } = require('./blog-search.logic');
+
+const blogs = [
+ {
+ frontMatter: {
+ title: 'Real-Time Analytics with Apache Doris',
+ summary: 'Build fast dashboards for customer-facing workloads.',
+ author: 'Apache Doris Team',
+ tags: ['Best Practice'],
+ },
+ metadata: {
+ title: 'Real-Time Analytics with Apache Doris',
+ permalink: '/blog/real-time-analytics',
+ },
+ },
+ {
+ frontMatter: {
+ title: 'Lakehouse Integration',
+ description: 'Query Iceberg tables directly from Doris.',
+ authors: [{ name: 'Jane Doe' }],
+ tags: [{ label: 'Tech Sharing' }],
+ keywords: 'Open table format',
+ },
+ metadata: {
+ title: 'Lakehouse Integration',
+ permalink: '/blog/lakehouse-integration',
+ },
+ },
+];
+
+test('matches title, summary, description, author, and tag fields
case-insensitively', () => {
+ assert.equal(matchesBlogSearch(blogs[0], 'REAL-TIME'), true);
+ assert.equal(matchesBlogSearch(blogs[0], 'dashboards'), true);
+ assert.equal(matchesBlogSearch(blogs[0], 'doris team'), true);
+ assert.equal(matchesBlogSearch(blogs[0], 'best practice'), true);
+ assert.equal(matchesBlogSearch(blogs[1], 'iceberg'), true);
+ assert.equal(matchesBlogSearch(blogs[1], 'jane doe'), true);
+ assert.equal(matchesBlogSearch(blogs[1], 'tech sharing'), true);
+ assert.equal(matchesBlogSearch(blogs[1], 'open table'), true);
+});
+
+test('requires every whitespace-separated search term to match', () => {
+ assert.deepEqual(filterBlogs(blogs, 'doris dashboards'), [blogs[0]]);
+ assert.deepEqual(filterBlogs(blogs, 'dashboards iceberg'), []);
+});
+
+test('treats blank searches as an unfiltered list', () => {
+ assert.deepEqual(filterBlogs(blogs, ' '), blogs);
+});
+
+test('normalizes full-width characters before matching', () => {
+ assert.equal(matchesBlogSearch(blogs[0], 'Doris'), true);
+});
diff --git a/src/theme/BlogListPage/index.tsx b/src/theme/BlogListPage/index.tsx
index 9eea6ffe7b8..f2a451cac65 100644
--- a/src/theme/BlogListPage/index.tsx
+++ b/src/theme/BlogListPage/index.tsx
@@ -1,34 +1,28 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import clsx from 'clsx';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { PageMetadata, HtmlClassNameProvider, ThemeClassNames } from
'@docusaurus/theme-common';
+import { translate } from '@docusaurus/Translate';
import BlogLayout from '@theme/BlogLayout';
import BlogListItem from '../BlogListItem';
-import useIsBrowser from '@docusaurus/useIsBrowser';
import BlogListFooter from '../BlogFooter';
-import BlogListPaginator from '@theme/BlogListPaginator';
-import SearchMetadata from '@theme/SearchMetadata';
import HeadBlogs from '@site/src/components/blogs/components/head-blogs';
import PageHeader from '@site/src/components/PageHeader';
import type { Props } from '@theme/BlogListPage';
-import BlogPostItems from '@theme/BlogPostItems';
import { useHistory, useLocation } from '@docusaurus/router';
+import { filterBlogs } from './blog-search.logic';
+import styles from './styles.module.scss';
// import BlogListPageStructuredData from '@theme/BlogListPage/StructuredData';
const allText = 'All';
+const PAGE_SIZE = 9;
const HIDDEN_BLOG_TABS = new Set(['Release Notes', 'Top News']);
const FIXED_BLOG_TABS = ['Glossary'];
-function getBlogCategories(props) {
- const { siteConfig } = useDocusaurusContext();
- const allText = 'All';
- const { items } = props;
+function getBlogCategories(items) {
const allCategory = { label: allText, values: [] };
const categories = [allCategory];
- useEffect(() => {
- sessionStorage.setItem('tag', allText);
- }, []);
items.forEach(({ content: BlogPostContent }) => {
const { frontMatter } = BlogPostContent;
const tags = frontMatter.tags || [];
@@ -93,95 +87,158 @@ function BlogListPageMetadata(props) {
);
}
function BlogListPageContent(props) {
- const { metadata, items, sidebar } = props;
- const isBrowser = useIsBrowser();
+ const { items, sidebar } = props;
const [blogs, setBlogs] = useState([]);
- const blogCategories = getBlogCategories(props);
+ const blogCategories = useMemo(() => getBlogCategories(items), [items]);
const ALL_BLOG = blogCategories.find(item => item.label ===
allText).values;
-
- const { siteConfig } = useDocusaurusContext();
- const isCN = siteConfig.baseUrl.indexOf('zh-CN') > -1;
- const [active, setActive] = useState(() => {
- const tag = isBrowser ? sessionStorage.getItem('tag') : allText;
- return tag || allText;
- });
- const [pageSize, setPageSize] = useState<number>(9);
- let [pageNumber, setPageNumber] = useState<number>(1);
+
+ const [active, setActive] = useState(allText);
+ const [searchQuery, setSearchQuery] = useState('');
const [currentBlogs, setCurrentBlogs] = useState([]);
- const [currentPage, setCurrentPage] = useState<number>(0);
+ const [currentPage, setCurrentPage] = useState<number>(1);
const location = useLocation();
const history = useHistory();
const changeCategory = category => {
- history.push(
- `${location.pathname}?currentPage=1¤tCategory=${category ?
category : ''}#blog`,
- location.state,
- );
+ const params = new URLSearchParams(location.search);
+ params.set('currentPage', '1');
+ params.set('currentCategory', category || allText);
+ history.push(`${location.pathname}?${params.toString()}#blog`,
location.state);
};
- useEffect(() => {
- let currentPageNumber = 1;
- let currentCategoryName = allText;
- if (location.search && location.search.split('?').length > 0) {
- const params = location.search.split('?')[1].split('&');
- if (params) {
- params.map(e => {
- const [key, value] = e.split('=');
- if (key === 'currentPage') {
- if (value) currentPageNumber = +value;
- } else {
- if (value) currentCategoryName = decodeURI(value);
- }
- });
- }
+
+ const changeSearchQuery = query => {
+ setSearchQuery(query);
+ const params = new URLSearchParams(location.search);
+ params.set('currentPage', '1');
+ if (query) {
+ params.set('q', query);
+ } else {
+ params.delete('q');
}
+ history.replace(`${location.pathname}?${params.toString()}#blog`,
location.state);
+ };
- setActive(currentCategoryName);
- setCurrentPage(currentPageNumber);
+ useEffect(() => {
+ const params = new URLSearchParams(location.search);
+ const requestedPage = Number(params.get('currentPage'));
+ const currentPageNumber = Number.isInteger(requestedPage) &&
requestedPage > 0 ? requestedPage : 1;
+ const currentCategoryName = params.get('currentCategory') || allText;
+ const currentSearchQuery = params.get('q') || '';
let currentCategory = blogCategories.find(item => item.label ===
currentCategoryName);
if (!currentCategory) {
currentCategory = blogCategories.find(item => item.label ===
allText);
}
- setBlogs(currentCategory.values);
- setCurrentBlogs(currentCategory.values.slice((currentPageNumber - 1) *
pageSize, currentPageNumber * pageSize));
- }, [location.search]);
+ const filteredBlogs = filterBlogs(currentCategory.values,
currentSearchQuery);
+ const lastPage = Math.max(1, Math.ceil(filteredBlogs.length /
PAGE_SIZE));
+ const normalizedPage = Math.min(currentPageNumber, lastPage);
- useEffect(() => {
- changeCategory(active);
- isBrowser && sessionStorage.setItem('tag', active);
- }, [active]);
+ setActive(currentCategory.label);
+ setSearchQuery(currentSearchQuery);
+ setCurrentPage(normalizedPage);
+ setBlogs(filteredBlogs);
+ setCurrentBlogs(filteredBlogs.slice((normalizedPage - 1) * PAGE_SIZE,
normalizedPage * PAGE_SIZE));
+ }, [blogCategories, location.search]);
+
+ const searchLabel = translate({
+ id: 'blog.search.label',
+ message: 'Search blogs',
+ description: 'Accessible label for the search field on the blog list
page',
+ });
return (
<BlogLayout sidebar={sidebar} pageType="blogList"
className="lg:max-w-7xl">
<PageHeader title="Blog" className="bg-white" {...props} />
<HeadBlogs blogs={ALL_BLOG} />
<div id="blog" className="flex flex-col lg:max-w-7xl scroll-mt-24">
- <ul className="scrollbar-none w-[100%] mt-6 custom-scrollbar
m-auto flex gap-3 overflow-auto text-[#4C576C] lg:mt-[5.5rem]
lg:justify-center lg:gap-6">
+ <section className={styles.searchSection}
aria-label={searchLabel}>
+ <label className={styles.visuallyHidden}
htmlFor="blog-search-input">
+ {searchLabel}
+ </label>
+ <div className={styles.searchControl}>
+ <svg
+ className={styles.searchIcon}
+ aria-hidden="true"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ >
+ <circle cx="11" cy="11" r="7" />
+ <path d="m20 20-4-4" />
+ </svg>
+ <input
+ id="blog-search-input"
+ className={styles.searchInput}
+ type="search"
+ value={searchQuery}
+ onChange={event =>
changeSearchQuery(event.target.value)}
+ placeholder={translate({
+ id: 'blog.search.placeholder',
+ message: 'Search by title, summary, tag, or
author',
+ })}
+ autoComplete="off"
+ />
+ {searchQuery && (
+ <button
+ className={styles.clearButton}
+ type="button"
+ onClick={() => changeSearchQuery('')}
+ >
+ {translate({ id: 'blog.search.clear', message:
'Clear' })}
+ </button>
+ )}
+ </div>
+ {searchQuery.trim() && (
+ <p className={styles.resultCount} aria-live="polite">
+ {translate(
+ {
+ id: 'blog.search.resultCount',
+ message: '{count} blog posts found',
+ },
+ { count: blogs.length },
+ )}
+ </p>
+ )}
+ </section>
+ <ul className="scrollbar-none w-[100%] mt-6 custom-scrollbar
m-auto flex gap-3 overflow-auto text-[#4C576C] lg:justify-center lg:gap-6">
{blogCategories.map((item: any, index) => (
- <li className=" py-px" key={index} onClick={() =>
changeCategory(item.label)}>
- <span
+ <li className="py-px" key={index}>
+ <button
+ type="button"
+ onClick={() => changeCategory(item.label)}
+ aria-pressed={active === item.label}
className={`block cursor-pointer
whitespace-nowrap rounded-[2.5rem] px-4 py-2 text-sm
shadow-[0px_1px_4px_0px_rgba(0,89,68,0.10)] hover:bg-primary hover:text-white
lg:px-6 lg:py-3 lg:text-base ${
active === item.label && 'bg-primary
text-white'
}`}
>
{item.label}
- </span>
+ </button>
</li>
))}
</ul>
<ul className="mt-6 grid gap-6 lg:mt-10 lg:grid-cols-3 m-auto">
- {currentBlogs.map((BlogPostContent, i) => (
- <BlogListItem
- key={BlogPostContent.metadata.permalink + i}
- frontMatter={BlogPostContent.frontMatter}
- assets={BlogPostContent.assets}
- metadata={BlogPostContent.metadata}
- truncated={BlogPostContent.metadata.truncated}
- >
- <BlogPostContent />
- </BlogListItem>
- ))}
+ {currentBlogs.length > 0 ? (
+ currentBlogs.map((BlogPostContent, i) => (
+ <BlogListItem
+ key={BlogPostContent.metadata.permalink + i}
+ frontMatter={BlogPostContent.frontMatter}
+ assets={BlogPostContent.assets}
+ metadata={BlogPostContent.metadata}
+ truncated={BlogPostContent.metadata.truncated}
+ >
+ <BlogPostContent />
+ </BlogListItem>
+ ))
+ ) : (
+ <li className={styles.emptyState}>
+ {translate({
+ id: 'blog.search.noResults',
+ message: 'No blog posts match this search and
category.',
+ })}
+ </li>
+ )}
</ul>
<BlogListFooter total={blogs.length} currentPage={currentPage}
currentCategory={active} />
</div>
diff --git a/src/theme/BlogListPage/styles.module.scss
b/src/theme/BlogListPage/styles.module.scss
new file mode 100644
index 00000000000..939257902f6
--- /dev/null
+++ b/src/theme/BlogListPage/styles.module.scss
@@ -0,0 +1,108 @@
+.searchSection {
+ align-self: center;
+ margin-top: 5.5rem;
+ max-width: 42rem;
+ width: 100%;
+}
+
+.searchControl {
+ align-items: center;
+ background: var(--global-colors-white);
+ border: 1px solid var(--global-colors-split);
+ border-radius: var(--global-border-radius);
+ color: var(--global-colors-text-secondary);
+ display: flex;
+ min-height: 3rem;
+ position: relative;
+ transition: border-color 160ms ease, box-shadow 160ms ease;
+}
+
+.searchControl:focus-within {
+ border-color: var(--ifm-color-primary);
+ box-shadow: 0 0 0 3px rgb(var(--brand-primary-rgb) / 16%);
+}
+
+.searchIcon {
+ flex: 0 0 auto;
+ height: 1.25rem;
+ margin-left: 1rem;
+ width: 1.25rem;
+}
+
+.searchInput {
+ background: transparent;
+ border: 0;
+ color: var(--global-colors-text-primary);
+ flex: 1;
+ font-size: var(--global-font-size-medium);
+ min-width: 0;
+ outline: 0;
+ padding: 0.75rem 1rem;
+}
+
+.searchInput::placeholder {
+ color: var(--global-colors-text-secondary);
+}
+
+.searchInput::-webkit-search-cancel-button,
+.searchInput::-webkit-search-decoration {
+ appearance: none;
+}
+
+.clearButton {
+ background: transparent;
+ border: 0;
+ color: var(--ifm-color-primary);
+ cursor: pointer;
+ flex: 0 0 auto;
+ font: inherit;
+ font-weight: 500;
+ margin-right: 0.5rem;
+ padding: 0.5rem;
+}
+
+.clearButton:hover {
+ text-decoration: underline;
+}
+
+.clearButton:focus-visible {
+ border-radius: 0.25rem;
+ outline: 2px solid var(--ifm-color-primary);
+ outline-offset: 1px;
+}
+
+.resultCount {
+ color: var(--global-colors-text-secondary);
+ font-size: var(--global-font-size-small);
+ margin: 0.75rem 0 0;
+ text-align: center;
+}
+
+.emptyState {
+ background: var(--brand-surface-faint);
+ border: 1px solid var(--brand-border-soft);
+ border-radius: var(--global-border-radius);
+ color: var(--global-colors-text-secondary);
+ grid-column: 1 / -1;
+ padding: 3rem 1.5rem;
+ text-align: center;
+}
+
+.visuallyHidden {
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+
+@media (max-width: 996px) {
+ .searchSection {
+ margin-top: 2rem;
+ }
+}
diff --git a/src/theme/DocItem/Layout/MobileSidebarDrawer.module.css
b/src/theme/DocItem/Layout/MobileSidebarDrawer.module.css
index 2efe532886e..a4ad84af1d5 100644
--- a/src/theme/DocItem/Layout/MobileSidebarDrawer.module.css
+++ b/src/theme/DocItem/Layout/MobileSidebarDrawer.module.css
@@ -46,6 +46,18 @@
color: var(--ifm-color-primary);
}
+.toolbarMenuBtn {
+ width: auto;
+ padding: 0 0.75rem;
+ gap: 0.5rem;
+}
+
+.toolbarMenuLabel {
+ font-size: 0.8125rem;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
.toolbarIcon {
width: 1rem;
height: 1rem;
diff --git a/src/theme/DocItem/Layout/MobileSidebarDrawer.tsx
b/src/theme/DocItem/Layout/MobileSidebarDrawer.tsx
index c2939132adf..ffbc2acaf86 100644
--- a/src/theme/DocItem/Layout/MobileSidebarDrawer.tsx
+++ b/src/theme/DocItem/Layout/MobileSidebarDrawer.tsx
@@ -1,24 +1,28 @@
-import React, { useEffect, useRef, useState, useCallback } from 'react';
+import React, { type JSX, useEffect, useRef, useState, useCallback } from
'react';
import { createPortal } from 'react-dom';
import clsx from 'clsx';
import { useLocation } from '@docusaurus/router';
-import { useDocsSidebar } from '@docusaurus/plugin-content-docs/client';
+import { useActivePlugin, useActiveVersion, useDocsSidebar } from
'@docusaurus/plugin-content-docs/client';
import { ThemeClassNames } from '@docusaurus/theme-common';
import { useAlternatePageUtils } from '@docusaurus/theme-common/internal';
import DocSidebarItems from '@theme/DocSidebarItems';
import SearchBar from '@theme/SearchBar';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+import { getDocsSidebarScope, supportsDocsDomainNavigation } from
'@site/src/utils/docs-sidebar-scope';
import styles from './MobileSidebarDrawer.module.css';
export default function MobileSidebarDrawer(): JSX.Element | null {
const sidebar = useDocsSidebar();
+ const activePlugin = useActivePlugin();
+ const activeVersion = useActiveVersion(activePlugin?.pluginId);
const { pathname, search, hash } = useLocation();
const {
i18n: { currentLocale, locales, localeConfigs },
} = useDocusaurusContext();
const alternatePageUtils = useAlternatePageUtils();
const isZH = currentLocale === 'zh-CN';
+ const usesDomainNavigation =
supportsDocsDomainNavigation(activePlugin?.pluginId, activeVersion?.name);
const [open, setOpen] = useState(false);
const [localeOpen, setLocaleOpen] = useState(false);
const localeContainerRef = useRef<HTMLDivElement>(null);
@@ -59,6 +63,12 @@ export default function MobileSidebarDrawer(): JSX.Element |
null {
if (!sidebar) return null;
+ const sidebarScope = getDocsSidebarScope(sidebar.items, pathname);
+ const scopedSidebarItems = usesDomainNavigation ? sidebarScope.scopedItems
: sidebar.items;
+ const drawerLabel = usesDomainNavigation && sidebarScope.activeDomain
+ ? sidebarScope.activeDomain.label
+ : (isZH ? '目录' : 'Menu');
+
const drawer = (
<div className={clsx(open && styles.open)} aria-hidden={!open}>
<div
@@ -71,7 +81,7 @@ export default function MobileSidebarDrawer(): JSX.Element |
null {
aria-label={isZH ? '文档目录' : 'Docs sidebar'}
>
<div className={styles.header}>
- <span>{isZH ? '目录' : 'Menu'}</span>
+ <span>{drawerLabel}</span>
<button
type="button"
className={styles.close}
@@ -94,7 +104,7 @@ export default function MobileSidebarDrawer(): JSX.Element |
null {
>
<ul className={clsx(ThemeClassNames.docs.docSidebarMenu,
'menu__list', styles.menu)}>
<DocSidebarItems
- items={sidebar.items}
+ items={scopedSidebarItems}
activePath={pathname}
onItemClick={(item) => {
if (item.type === 'link') {
@@ -115,66 +125,70 @@ export default function MobileSidebarDrawer():
JSX.Element | null {
return (
<>
<div className={styles.toolbar}>
- <div className={styles.toolbarSearch}>
- <SearchBar />
- </div>
- <div
- ref={localeContainerRef}
- className={clsx(styles.toolbarLocale, localeOpen &&
styles.toolbarLocaleOpen)}
- >
- <button
- type="button"
- className={styles.toolbarIconBtn}
- onClick={() => setLocaleOpen(o => !o)}
- aria-label={isZH ? '切换语言' : 'Switch language'}
- aria-haspopup="true"
- aria-expanded={localeOpen}
- >
- <svg
- className={styles.toolbarIcon}
- xmlns="http://www.w3.org/2000/svg"
- width="16"
- height="16"
- viewBox="0 0 16 16"
- fill="none"
- aria-hidden="true"
+ {!usesDomainNavigation && (
+ <>
+ <div className={styles.toolbarSearch}>
+ <SearchBar />
+ </div>
+ <div
+ ref={localeContainerRef}
+ className={clsx(styles.toolbarLocale, localeOpen
&& styles.toolbarLocaleOpen)}
>
- <circle cx="8" cy="8" r="6.5"
stroke="currentColor" strokeWidth="1.25" />
- <path
- d="M1.5 8h13M8 1.5c1.75 1.78 2.7 4.03 2.7
6.5s-.95 4.72-2.7 6.5C6.25 12.72 5.3 10.47 5.3 8S6.25 3.28 8 1.5Z"
- stroke="currentColor"
- strokeWidth="1.25"
- />
- </svg>
- </button>
- <ul className={styles.localeMenu} role="menu">
- {locales.map(locale => {
- const baseTo = alternatePageUtils.createUrl({
- locale,
- fullyQualified: false,
- });
- const to = `${baseTo}${search}${hash}`;
- return (
- <li key={locale} role="none">
- <a
- role="menuitem"
- href={to}
- lang={localeConfigs[locale]?.htmlLang}
- className={clsx(
- styles.localeMenuItem,
- locale === currentLocale &&
styles.localeMenuItemActive,
- )}
- >
- {localeConfigs[locale]?.label ??
locale}
- </a>
- </li>
- );
- })}
- </ul>
- </div>
+ <button
+ type="button"
+ className={styles.toolbarIconBtn}
+ onClick={() => setLocaleOpen(o => !o)}
+ aria-label={isZH ? '切换语言' : 'Switch language'}
+ aria-haspopup="true"
+ aria-expanded={localeOpen}
+ >
+ <svg
+ className={styles.toolbarIcon}
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ height="16"
+ viewBox="0 0 16 16"
+ fill="none"
+ aria-hidden="true"
+ >
+ <circle cx="8" cy="8" r="6.5"
stroke="currentColor" strokeWidth="1.25" />
+ <path
+ d="M1.5 8h13M8 1.5c1.75 1.78 2.7 4.03
2.7 6.5s-.95 4.72-2.7 6.5C6.25 12.72 5.3 10.47 5.3 8S6.25 3.28 8 1.5Z"
+ stroke="currentColor"
+ strokeWidth="1.25"
+ />
+ </svg>
+ </button>
+ <ul className={styles.localeMenu} role="menu">
+ {locales.map(locale => {
+ const baseTo =
alternatePageUtils.createUrl({
+ locale,
+ fullyQualified: false,
+ });
+ const to = `${baseTo}${search}${hash}`;
+ return (
+ <li key={locale} role="none">
+ <a
+ role="menuitem"
+ href={to}
+
lang={localeConfigs[locale]?.htmlLang}
+ className={clsx(
+ styles.localeMenuItem,
+ locale === currentLocale
&& styles.localeMenuItemActive,
+ )}
+ >
+ {localeConfigs[locale]?.label
?? locale}
+ </a>
+ </li>
+ );
+ })}
+ </ul>
+ </div>
+ </>
+ )}
<button
type="button"
- className={styles.toolbarIconBtn}
+ className={clsx(styles.toolbarIconBtn,
usesDomainNavigation && styles.toolbarMenuBtn)}
onClick={() => setOpen(true)}
aria-label={isZH ? '打开文档目录' : 'Open docs sidebar'}
aria-expanded={open}
@@ -182,6 +196,11 @@ export default function MobileSidebarDrawer(): JSX.Element
| null {
<svg className={styles.toolbarIcon} viewBox="0 0 16 16"
fill="none" aria-hidden="true">
<path d="M2 4h12M2 8h12M2 12h12" stroke="currentColor"
strokeWidth="1.5" strokeLinecap="round" />
</svg>
+ {usesDomainNavigation && (
+ <span className={styles.toolbarMenuLabel}>
+ {isZH ? '浏览当前分类' : 'Browse this section'}
+ </span>
+ )}
</button>
</div>
diff --git a/src/theme/DocItem/Layout/styles.module.css
b/src/theme/DocItem/Layout/styles.module.css
index 49a67db70ae..ddb3734caa8 100644
--- a/src/theme/DocItem/Layout/styles.module.css
+++ b/src/theme/DocItem/Layout/styles.module.css
@@ -36,7 +36,10 @@
@media (max-width: 996px) {
.mobileStickyHeader {
position: sticky;
- top: 64px;
+ top: var(
+ --docs-navigation-offset,
+ calc(var(--home-next-banner-height, 0px) + var(--navbar-next-height,
64px))
+ );
z-index: 200;
background-color: #fff;
margin-bottom: 0.5rem;
@@ -50,9 +53,3 @@
margin-bottom: 0;
}
}
-
-@media (max-width: 768px) {
- .mobileStickyHeader {
- top: 160px;
- }
-}
diff --git a/src/theme/DocRoot/Layout/index.tsx
b/src/theme/DocRoot/Layout/index.tsx
index aaf1bc2ccc3..ac094094d99 100644
--- a/src/theme/DocRoot/Layout/index.tsx
+++ b/src/theme/DocRoot/Layout/index.tsx
@@ -1,8 +1,12 @@
-import React, { useState } from 'react';
-import { useActivePlugin, useDocsSidebar } from
'@docusaurus/plugin-content-docs/client';
+import React, { type JSX, useMemo, useState } from 'react';
+import clsx from 'clsx';
+import { useLocation } from '@docusaurus/router';
+import { useActivePlugin, useActiveVersion, useDocsSidebar } from
'@docusaurus/plugin-content-docs/client';
import BackToTopButton from '@theme/BackToTopButton';
import DocRootLayoutSidebar from '@theme/DocRoot/Layout/Sidebar';
import DocRootLayoutMain from '@theme/DocRoot/Layout/Main';
+import DocsDomainNav from '@site/src/components/docs-domain-nav/DocsDomainNav';
+import { getDocsSidebarScope, supportsDocsDomainNavigation } from
'@site/src/utils/docs-sidebar-scope';
import type { Props } from '@theme/DocRoot/Layout';
import styles from './styles.module.css';
@@ -10,8 +14,16 @@ import styles from './styles.module.css';
export default function DocRootLayout({ children }: Props): JSX.Element {
const sidebar = useDocsSidebar();
const activePlugin = useActivePlugin();
+ const activeVersion = useActiveVersion(activePlugin?.pluginId);
+ const { pathname } = useLocation();
const [hiddenSidebarContainer, setHiddenSidebarContainer] =
useState(false);
const isCourse = activePlugin?.pluginId === 'course';
+ const usesDomainNavigation =
supportsDocsDomainNavigation(activePlugin?.pluginId, activeVersion?.name);
+ const sidebarScope = useMemo(
+ () => sidebar ? getDocsSidebarScope(sidebar.items, pathname) :
undefined,
+ [pathname, sidebar],
+ );
+ const showDomainNav = Boolean(usesDomainNavigation && sidebarScope &&
sidebarScope.domains.length > 1);
if (isCourse) {
return (
@@ -23,12 +35,15 @@ export default function DocRootLayout({ children }: Props):
JSX.Element {
}
return (
- <div className={styles.docsWrapper}>
+ <div className={clsx(styles.docsWrapper, showDomainNav &&
styles.docsWithDomainNav)}>
<BackToTopButton />
+ {showDomainNav && sidebarScope && (
+ <DocsDomainNav domains={sidebarScope.domains} />
+ )}
<div className={styles.docRoot}>
{sidebar && (
<DocRootLayoutSidebar
- sidebar={sidebar.items}
+ sidebar={showDomainNav && sidebarScope ?
sidebarScope.scopedItems : sidebar.items}
hiddenSidebarContainer={hiddenSidebarContainer}
setHiddenSidebarContainer={setHiddenSidebarContainer}
/>
diff --git a/src/theme/DocRoot/Layout/styles.module.css
b/src/theme/DocRoot/Layout/styles.module.css
index 8482d14bbc9..84143bb5447 100644
--- a/src/theme/DocRoot/Layout/styles.module.css
+++ b/src/theme/DocRoot/Layout/styles.module.css
@@ -10,6 +10,40 @@
min-width: 0;
}
+.docsWrapper {
+ flex-direction: column;
+}
+
+.docsWithDomainNav {
+ --docs-global-navbar-height: var(--navbar-next-height, 64px);
+ --docs-domain-nav-height: 60px;
+ --docs-navigation-offset: calc(
+ var(--home-next-banner-height, 0px) +
+ var(--docs-global-navbar-height) +
+ var(--docs-domain-nav-height)
+ );
+ --doc-sidebar-width: 18rem !important;
+}
+
+@media (min-width: 997px) {
+ .docsWithDomainNav :global(.theme-doc-sidebar-container) {
+ margin-top: 0;
+ }
+
+ .docsWithDomainNav :global(.theme-doc-sidebar-container) > div {
+ top: var(--docs-navigation-offset);
+ height: auto;
+ max-height: calc(100vh - var(--docs-navigation-offset));
+ }
+}
+
+@media (max-width: 996px) {
+ .docsWithDomainNav {
+ --docs-domain-nav-height: 56px;
+ }
+}
+
+
.courseMain {
width: 100%;
min-width: 0;
diff --git a/src/theme/DocSidebar/Desktop/index.tsx
b/src/theme/DocSidebar/Desktop/index.tsx
index 315004a57b4..e3c2a3f2923 100644
--- a/src/theme/DocSidebar/Desktop/index.tsx
+++ b/src/theme/DocSidebar/Desktop/index.tsx
@@ -1,16 +1,21 @@
import React from 'react';
import clsx from 'clsx';
import {useThemeConfig} from '@docusaurus/theme-common';
+import {useActivePlugin, useActiveVersion} from
'@docusaurus/plugin-content-docs/client';
import Logo from '@theme/Logo';
import SearchBar from '@theme/SearchBar';
import LocaleDropdownNavbarItem from
'@theme/NavbarItem/LocaleDropdownNavbarItem';
import CollapseButton from '@theme/DocSidebar/Desktop/CollapseButton';
import Content from '@theme/DocSidebar/Desktop/Content';
import type {Props} from '@theme/DocSidebar/Desktop';
+import {supportsDocsDomainNavigation} from
'@site/src/utils/docs-sidebar-scope';
import styles from './styles.module.css';
function DocSidebarDesktop({path, sidebar, onCollapse, isHidden}: Props) {
+ const activePlugin = useActivePlugin();
+ const activeVersion = useActiveVersion(activePlugin?.pluginId);
+ const usesDomainNavigation =
supportsDocsDomainNavigation(activePlugin?.pluginId, activeVersion?.name);
const {
navbar: {hideOnScroll},
docs: {
@@ -22,18 +27,21 @@ function DocSidebarDesktop({path, sidebar, onCollapse,
isHidden}: Props) {
<div
className={clsx(
styles.sidebar,
+ usesDomainNavigation && styles.sidebarWithDomainNavigation,
hideOnScroll && styles.sidebarWithHideableNavbar,
isHidden && styles.sidebarHidden,
)}>
{hideOnScroll && <Logo tabIndex={-1} className={styles.sidebarLogo} />}
- <div className={styles.sidebarHeader}>
- <div className={styles.sidebarSearch}>
- <SearchBar />
+ {!usesDomainNavigation && (
+ <div className={styles.sidebarHeader}>
+ <div className={styles.sidebarSearch}>
+ <SearchBar />
+ </div>
+ <div className={styles.sidebarLocale}>
+ <LocaleDropdownNavbarItem mobile={false} {...({} as any)} />
+ </div>
</div>
- <div className={styles.sidebarLocale}>
- <LocaleDropdownNavbarItem mobile={false} {...({} as any)} />
- </div>
- </div>
+ )}
<Content path={path} sidebar={sidebar} />
{hideable && <CollapseButton onClick={onCollapse} />}
</div>
diff --git a/src/theme/DocSidebar/Desktop/styles.module.css
b/src/theme/DocSidebar/Desktop/styles.module.css
index 506fbe9f65e..39a268248ef 100644
--- a/src/theme/DocSidebar/Desktop/styles.module.css
+++ b/src/theme/DocSidebar/Desktop/styles.module.css
@@ -11,6 +11,10 @@
padding-top: 0;
}
+ .sidebarWithDomainNavigation {
+ padding-top: 0;
+ }
+
.sidebarHidden {
opacity: 0;
visibility: hidden;
diff --git a/src/theme/DocSidebarItem/Category/index.tsx
b/src/theme/DocSidebarItem/Category/index.tsx
index e0dcd6a93d4..00008244b8e 100644
--- a/src/theme/DocSidebarItem/Category/index.tsx
+++ b/src/theme/DocSidebarItem/Category/index.tsx
@@ -1,10 +1,12 @@
-import React, { type ComponentProps, useEffect, useMemo } from 'react';
+import React, { type ComponentProps, type JSX, useEffect, useMemo } from
'react';
import clsx from 'clsx';
import { ThemeClassNames, useThemeConfig, usePrevious, Collapsible,
useCollapsible } from '@docusaurus/theme-common';
import { isSamePath } from '@docusaurus/theme-common/internal';
import {
isActiveSidebarItem,
findFirstSidebarItemLink,
+ useActivePlugin,
+ useActiveVersion,
useDocSidebarItemsExpandedState,
} from '@docusaurus/plugin-content-docs/client';
import Link from '@docusaurus/Link';
@@ -12,7 +14,7 @@ import { translate } from '@docusaurus/Translate';
import useIsBrowser from '@docusaurus/useIsBrowser';
import DocSidebarItems from '@theme/DocSidebarItems';
import type { Props } from '@theme/DocSidebarItem/Category';
-import useIsDocPage from '../../../hooks/use-is-doc';
+import { supportsDocsDomainNavigation } from
'@site/src/utils/docs-sidebar-scope';
import './style.scss';
@@ -110,7 +112,10 @@ export default function DocSidebarItemCategory({
length,
...props
}: DocSidebarItemCategoryProps): JSX.Element {
- const [isDocsPage] = useIsDocPage(false);
+ const activePlugin = useActivePlugin();
+ const activeVersion = useActiveVersion(activePlugin?.pluginId);
+ const isMainDocs = activePlugin?.pluginId === 'default';
+ const usesDomainNavigation =
supportsDocsDomainNavigation(activePlugin?.pluginId, activeVersion?.name);
const { items, label, collapsible, className, href } = item;
const {
docs: {
@@ -158,72 +163,66 @@ export default function DocSidebarItemCategory({
className,
)}
>
- <div
- className={clsx('menu__list-item-collapsible', {
- 'menu__list-item-collapsible--active': isCurrentPage,
- })}
- >
- {level === 1 && isDocsPage ? (
- <p className={clsx('title_level_1')}>
- <span className="title_level_1__label">{label}</span>
- </p>
- ) : (
- <>
- <Link
- className={clsx('menu__link', {
- 'menu__link--sublist': collapsible,
- 'menu__link--sublist-caret': !href &&
collapsible,
- 'menu__link--active': isActive,
- })}
- onClick={
- collapsible
- ? e => {
- onItemClick?.(item);
- if (href) {
- if (isCurrentPage) {
- // When already on this doc,
toggle expand/collapse on text click
- e.preventDefault();
- updateCollapsed();
- } else {
- // When navigating to this
doc, just ensure it's expanded
- updateCollapsed(false);
- }
- } else {
- // No href: behave like a pure
toggle button
+ {!(level === 1 && usesDomainNavigation) && (
+ <div
+ className={clsx('menu__list-item-collapsible', {
+ 'menu__list-item-collapsible--active': isCurrentPage,
+ })}
+ >
+ <Link
+ className={clsx('menu__link', {
+ 'menu__link--sublist': collapsible,
+ 'menu__link--sublist-caret': !href && collapsible,
+ 'menu__link--active': isActive,
+ })}
+ onClick={
+ collapsible
+ ? e => {
+ onItemClick?.(item);
+ if (href) {
+ if (isCurrentPage) {
+ // When already on this doc,
toggle expand/collapse on text click
e.preventDefault();
updateCollapsed();
+ } else {
+ // When navigating to this doc,
just ensure it's expanded
+ updateCollapsed(false);
}
+ } else {
+ // No href: behave like a pure
toggle button
+ e.preventDefault();
+ updateCollapsed();
}
- : () => {
- onItemClick?.(item);
- }
- }
- aria-current={isCurrentPage ? 'page' : undefined}
- role={collapsible && !href ? 'button' : undefined}
- aria-expanded={collapsible && !href ? !collapsed :
undefined}
- href={collapsible ? hrefWithSSRFallback ?? '#' :
hrefWithSSRFallback}
- {...props}
- >
- {label}
- </Link>
- {href && collapsible && (
- <CollapseButton
- collapsed={collapsed}
- categoryLabel={label}
- onClick={e => {
- e.preventDefault();
- updateCollapsed();
- }}
- />
- )}
- </>
- )}
- </div>
+ }
+ : () => {
+ onItemClick?.(item);
+ }
+ }
+ aria-current={isCurrentPage ? 'page' : undefined}
+ role={collapsible && !href ? 'button' : undefined}
+ aria-expanded={collapsible && !href ? !collapsed :
undefined}
+ href={collapsible ? hrefWithSSRFallback ?? '#' :
hrefWithSSRFallback}
+ {...props}
+ >
+ {label}
+ </Link>
+ {href && collapsible && (
+ <CollapseButton
+ collapsed={collapsed}
+ categoryLabel={label}
+ onClick={e => {
+ e.preventDefault();
+ updateCollapsed();
+ }}
+ />
+ )}
+ </div>
+ )}
<Collapsible
lazy
as="ul"
- className={`menu__list ${level === 1 ? 'menu__list_level_2' :
''} ${level === 1 && !isDocsPage ? 'community_level_2' : ''} `}
+ className={`menu__list ${level === 1 ? 'menu__list_level_2' :
''} ${level === 1 && !isMainDocs ? 'community_level_2' : ''} `}
collapsed={collapsed}
>
<DocSidebarItems
@@ -234,7 +233,7 @@ export default function DocSidebarItemCategory({
level={level + 1}
/>
</Collapsible>
- {level === 1 && index !== length - 1 && isDocsPage ? <div
className="divider"></div> : null}
+ {level === 1 && index !== length - 1 && isMainDocs ? <div
className="divider"></div> : null}
</li>
);
}
diff --git a/src/theme/DocSidebarItem/Category/style.scss
b/src/theme/DocSidebarItem/Category/style.scss
index 506b95e6c1f..47be05d1e6e 100644
--- a/src/theme/DocSidebarItem/Category/style.scss
+++ b/src/theme/DocSidebarItem/Category/style.scss
@@ -49,32 +49,6 @@
border-left: 1px solid #dfe5f0;
}
- .title_level_1 {
- color: #0D0D12;
- font-size: 15px;
- font-style: normal;
- font-weight: 600;
- line-height: 20px;
- letter-spacing: 0.4px;
- display: flex;
- padding: 20px 10px 12px 10px;
- align-items: baseline;
- gap: 8px;
- align-self: stretch;
- margin: 0;
- }
-
- .title_level_1__label {
- font-weight: 600;
- color: #0D0D12;
- }
-
- @media (max-width: 996px) {
- .title_level_1{
- font-size: 1.125rem !important;
- }
- }
-
.menu__list_level_2 {
padding-left: 0 !important;
margin-top: 0 !important;
diff --git a/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx
b/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx
index 4bd8304d8ad..a0ba559aee7 100644
--- a/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx
+++ b/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { useAlternatePageUtils } from '@docusaurus/theme-common/internal';
import { translate } from '@docusaurus/Translate';
@@ -103,6 +103,7 @@ export default function LocaleDropdownNavbarItem({
strokeWidth="1.25"
/>
</svg>
+ <span
className="locale-current-label">{localeConfigs[currentLocale]!.label}</span>
</>
}
items={items}
diff --git a/src/theme/NavbarItem/LocaleDropdownNavbarItem/styles.scss
b/src/theme/NavbarItem/LocaleDropdownNavbarItem/styles.scss
index 6eb26e6878d..892a3cfaada 100644
--- a/src/theme/NavbarItem/LocaleDropdownNavbarItem/styles.scss
+++ b/src/theme/NavbarItem/LocaleDropdownNavbarItem/styles.scss
@@ -1,6 +1,11 @@
.icon-language {
vertical-align: text-bottom;
}
+
+.locale-current-label {
+ display: inline-block;
+ white-space: nowrap;
+}
.dropdown__menu {
padding: 6px 0;
.dropdown__link {
diff --git a/src/theme/TOC/styles.module.css b/src/theme/TOC/styles.module.css
index 4b5d9f462e0..0a9a6114784 100644
--- a/src/theme/TOC/styles.module.css
+++ b/src/theme/TOC/styles.module.css
@@ -1,8 +1,8 @@
.tableOfContents {
- max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem));
+ max-height: calc(100vh - (var(--docs-navigation-offset,
var(--ifm-navbar-height)) + 2rem));
overflow-y: auto;
position: sticky;
- top: calc(var(--ifm-navbar-height) + 1rem);
+ top: calc(var(--docs-navigation-offset, var(--ifm-navbar-height)) + 1rem);
}
@media (max-width: 996px) {
diff --git a/src/utils/docs-sidebar-scope.ts b/src/utils/docs-sidebar-scope.ts
new file mode 100644
index 00000000000..a9e1054d4a3
--- /dev/null
+++ b/src/utils/docs-sidebar-scope.ts
@@ -0,0 +1,60 @@
+import {
+ findFirstSidebarItemLink,
+ isActiveSidebarItem,
+} from '@docusaurus/plugin-content-docs/client';
+import type { PropSidebar, PropSidebarItem } from
'@docusaurus/plugin-content-docs';
+
+export interface DocsDomain {
+ href: string;
+ isActive: boolean;
+ item: PropSidebarItem;
+ label: string;
+}
+
+export interface DocsSidebarScope {
+ activeDomain?: DocsDomain;
+ domains: DocsDomain[];
+ scopedItems: PropSidebar;
+}
+
+export function supportsDocsDomainNavigation(
+ pluginId: string | undefined,
+ versionName: string | undefined,
+): boolean {
+ return pluginId === 'default' && (versionName === 'current' || versionName
=== '4.x');
+}
+
+function getNavigationLabel(item: PropSidebarItem): string | undefined {
+ if (item.type === 'category' || item.type === 'link') {
+ return item.label;
+ }
+ return undefined;
+}
+
+/**
+ * Treat the first sidebar level as documentation domains. The active domain
+ * feeds both the horizontal navigation and the local sidebar, so the two
+ * navigation surfaces cannot drift apart as sidebars.ts evolves.
+ */
+export function getDocsSidebarScope(sidebarItems: PropSidebar, activePath:
string): DocsSidebarScope {
+ const domains = sidebarItems.flatMap(item => {
+ const label = getNavigationLabel(item);
+ const href = findFirstSidebarItemLink(item);
+ if (!label || !href) {
+ return [];
+ }
+ return [{
+ href,
+ isActive: isActiveSidebarItem(item, activePath),
+ item,
+ label,
+ }];
+ });
+ const activeDomain = domains.find(domain => domain.isActive);
+
+ return {
+ activeDomain,
+ domains,
+ scopedItems: activeDomain ? [activeDomain.item] : sidebarItems,
+ };
+}
diff --git a/versioned_sidebars/version-4.x-sidebars.json
b/versioned_sidebars/version-4.x-sidebars.json
index fbf3dde1ead..c56ea937f5c 100644
--- a/versioned_sidebars/version-4.x-sidebars.json
+++ b/versioned_sidebars/version-4.x-sidebars.json
@@ -227,7 +227,7 @@
},
{
"type": "category",
- "label": "Use Doris",
+ "label": "Guides",
"collapsible": false,
"collapsed": false,
"items": [
@@ -1357,12 +1357,15 @@
]
}
]
- },
- {
- "type": "category",
- "label": "Reference",
- "collapsed": true,
- "items": [
+ }
+ ]
+ },
+ {
+ "type": "category",
+ "label": "SQL References",
+ "collapsible": false,
+ "collapsed": false,
+ "items": [
{
"type": "category",
"label": "Basic Elements",
@@ -2898,7 +2901,5 @@
}
]
}
- ]
- }
]
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]