This is an automated email from the ASF dual-hosted git repository.

erickguan pushed a commit to branch website-polish
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit a65f91679cbbcf2cb6a25aac86680a099a63f135
Author: Erick Guan <[email protected]>
AuthorDate: Sun Jul 12 22:04:58 2026 +0800

    Polish docs and services layout and styles
---
 website/src/components/landing/CodeTabs.jsx        |   2 +-
 website/src/components/landing/sections.jsx        |   6 +-
 website/src/components/landing/styles.module.css   |   6 +-
 website/src/components/services/ServicePage.jsx    | 180 +++++++++++++++------
 website/src/components/services/styles.module.css  | 154 ++++++++++++++----
 website/src/components/ui/Breadcrumb.jsx           |  51 ++++++
 website/src/components/ui/CodeWindow.jsx           |   6 +-
 website/src/components/ui/Tabs.jsx                 |   4 +-
 .../ui/breadcrumb.module.css}                      |  23 +--
 website/src/components/ui/code-window.module.css   |   7 +-
 website/src/css/footer.css                         |   7 +-
 website/src/css/global.css                         |   9 ++
 website/src/theme/DocBreadcrumbs/index.jsx         | 124 ++++++++++++++
 13 files changed, 468 insertions(+), 111 deletions(-)

diff --git a/website/src/components/landing/CodeTabs.jsx 
b/website/src/components/landing/CodeTabs.jsx
index e43a0a6ed..395ce3c9a 100644
--- a/website/src/components/landing/CodeTabs.jsx
+++ b/website/src/components/landing/CodeTabs.jsx
@@ -162,7 +162,7 @@ export default function CodeTabs({ samples, title, equalize 
= false }) {
         getId={(sample) => sample.id}
         getLabel={(sample) => sample.label}
         className={styles.codeTabs}
-        ariaLabel="Choose a language"
+        aria-label="Choose a language"
         controlsId={`${baseId}-panel`}
         id={baseId}
       />
diff --git a/website/src/components/landing/sections.jsx 
b/website/src/components/landing/sections.jsx
index c68fd502e..af5cf91df 100644
--- a/website/src/components/landing/sections.jsx
+++ b/website/src/components/landing/sections.jsx
@@ -108,7 +108,7 @@ export function UsedBy() {
         </div>
         <ul className={styles.logoWall}>
           {usedBy.map((u) => (
-            <li key={u.name}>
+            <li className={styles.logoWallItem} key={u.name}>
               <Link className={styles.logoItem} to={u.href} title={u.name}>
                 <img
                   className={styles.logoMark}
@@ -122,7 +122,7 @@ export function UsedBy() {
               </Link>
             </li>
           ))}
-          <li>
+          <li className={styles.logoWallItem}>
             <Link className={styles.actionLogo} to={USERS_LIST_URL}>
               + Add your logo
             </Link>
@@ -221,6 +221,7 @@ export function Services() {
                     key={s.name}
                   >
                     <img
+                      className={styles.serviceChipIcon}
                       src={withBaseUrl(s.icon)}
                       alt=""
                       width="15"
@@ -264,6 +265,7 @@ export function Bindings() {
           {bindings.map((b) => (
             <Link className={styles.bindingCard} to={b.doc} key={b.name}>
               <img
+                className={styles.bindingIcon}
                 src={withBaseUrl(b.icon)}
                 alt=""
                 width="30"
diff --git a/website/src/components/landing/styles.module.css 
b/website/src/components/landing/styles.module.css
index f45dc5bbf..f0c8954a5 100644
--- a/website/src/components/landing/styles.module.css
+++ b/website/src/components/landing/styles.module.css
@@ -186,7 +186,7 @@
   grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
   gap: var(--odl-space-3);
 }
-.logoWall > li {
+.logoWallItem {
   display: flex;
 }
 .logoItem {
@@ -347,7 +347,7 @@
   color: var(--odl-fg);
   text-decoration: none;
 }
-.serviceChip img {
+.serviceChipIcon {
   width: 15px;
   height: 15px;
   object-fit: contain;
@@ -386,7 +386,7 @@
   background: var(--odl-surface-2);
   text-decoration: none;
 }
-.bindingCard img {
+.bindingIcon {
   width: 30px;
   height: 30px;
   object-fit: contain;
diff --git a/website/src/components/services/ServicePage.jsx 
b/website/src/components/services/ServicePage.jsx
index a95fbb464..35480d4ea 100644
--- a/website/src/components/services/ServicePage.jsx
+++ b/website/src/components/services/ServicePage.jsx
@@ -20,11 +20,22 @@
 import React, { useEffect, useState } from "react";
 import Layout from "@theme/Layout";
 import CodeBlock from "@theme/CodeBlock";
-import Link from "@docusaurus/Link";
 import { useLocation } from "@docusaurus/router";
+import Breadcrumb from "../ui/Breadcrumb";
 import Tabs from "../ui/Tabs";
 import styles from "./styles.module.css";
 
+const LANGUAGE_ALIASES = {
+  "c++": "cpp",
+  cplusplus: "cpp",
+  cpp: "cpp",
+  cxx: "cpp",
+};
+
+function cx(...classNames) {
+  return classNames.filter(Boolean).join(" ");
+}
+
 // Renders inline `code` spans inside an otherwise plain doc comment so the
 // reference table stays readable without pulling in a full markdown renderer.
 function renderComment(text) {
@@ -44,45 +55,70 @@ function ConfigTable({ rows }) {
   return (
     <div className={styles.tableWrap}>
       <table className={styles.table}>
-        <thead>
-          <tr>
-            <th>Key</th>
-            <th>Type</th>
-            <th>Required</th>
-            <th>Description</th>
+        <thead className={styles.tableHead}>
+          <tr className={styles.tableHeaderRow}>
+            <th className={styles.tableHeaderCell} scope="col">
+              Key
+            </th>
+            <th className={styles.tableHeaderCell} scope="col">
+              Type
+            </th>
+            <th className={styles.tableHeaderCell} scope="col">
+              Required
+            </th>
+            <th className={styles.tableHeaderCell} scope="col">
+              Description
+            </th>
           </tr>
         </thead>
-        <tbody>
-          {rows.map((c) => (
-            <tr key={c.name} className={c.deprecated ? styles.deprecatedRow : 
""}>
-              <td>
-                <code>{c.name}</code>
-                {c.deprecated && <span 
className={styles.badge}>deprecated</span>}
-                {c.default != null && (
-                  <span className={styles.chip}>
-                    default <code>{c.default}</code>
-                  </span>
-                )}
-                {c.default == null && c.example != null && (
-                  <span className={styles.chip}>
-                    e.g. <code>{c.example}</code>
-                  </span>
-                )}
-              </td>
-              <td data-label="Type">{c.type}</td>
-              <td data-label="Required">{c.required ? "yes" : "no"}</td>
-              <td className={styles.desc} data-label="Description">
-                {renderComment(c.comments)}
-                {c.deprecated && c.deprecated.note && (
-                  <div className={styles.deprecatedNote}>
-                    Deprecated
-                    {c.deprecated.since ? ` since ${c.deprecated.since}` : 
""}:{" "}
-                    {renderComment(c.deprecated.note)}
-                  </div>
-                )}
-              </td>
-            </tr>
-          ))}
+        <tbody className={styles.tableBody}>
+          {rows.map((c) => {
+            const rowClassName = cx(
+              styles.tableRow,
+              c.deprecated ? styles.deprecatedRow : ""
+            );
+
+            return (
+              <tr key={c.name} className={rowClassName}>
+                <td className={cx(styles.tableCell, styles.tableKeyCell)}>
+                  <code className={styles.configKey}>{c.name}</code>
+                  {c.deprecated && (
+                    <span className={styles.badge}>deprecated</span>
+                  )}
+                  {c.default != null && (
+                    <span className={styles.chip}>
+                      default <code 
className={styles.chipCode}>{c.default}</code>
+                    </span>
+                  )}
+                  {c.default == null && c.example != null && (
+                    <span className={styles.chip}>
+                      e.g. <code className={styles.chipCode}>{c.example}</code>
+                    </span>
+                  )}
+                </td>
+                <td className={styles.tableCell} data-label="Type">
+                  {c.type}
+                </td>
+                <td className={styles.tableCell} data-label="Required">
+                  {c.required ? "yes" : "no"}
+                </td>
+                <td
+                  className={cx(styles.tableCell, styles.desc)}
+                  data-label="Description"
+                >
+                  {renderComment(c.comments)}
+                  {c.deprecated && c.deprecated.note && (
+                    <div className={styles.deprecatedNote}>
+                      {`Deprecated${
+                        c.deprecated.since ? ` since ${c.deprecated.since}` : 
""
+                      }: `}
+                      {renderComment(c.deprecated.note)}
+                    </div>
+                  )}
+                </td>
+              </tr>
+            );
+          })}
         </tbody>
       </table>
     </div>
@@ -99,6 +135,52 @@ function bindingFromPath(pathname, scheme, available) {
   return tail && available.includes(tail) ? tail : undefined;
 }
 
+function normalizeLanguage(value) {
+  const normalized = value.trim().toLowerCase();
+  return LANGUAGE_ALIASES[normalized] || normalized;
+}
+
+function safeDecodeURIComponent(value) {
+  try {
+    return decodeURIComponent(value);
+  } catch {
+    return undefined;
+  }
+}
+
+// Keep raw `+` characters intact so links like `?language=c++` select C++.
+// URLSearchParams decodes `+` as a space because it follows form encoding.
+function languageFromSearch(search) {
+  const query = search.startsWith("?") ? search.slice(1) : search;
+  for (const part of query.split("&")) {
+    const separator = part.indexOf("=");
+    const rawKey = separator === -1 ? part : part.slice(0, separator);
+    const rawValue = separator === -1 ? "" : part.slice(separator + 1);
+    const key = safeDecodeURIComponent(rawKey);
+    if (key !== "language") {
+      continue;
+    }
+
+    return safeDecodeURIComponent(rawValue);
+  }
+
+  return undefined;
+}
+
+function bindingFromSearch(search, examples) {
+  const language = languageFromSearch(search);
+  if (!language) {
+    return undefined;
+  }
+
+  const normalized = normalizeLanguage(language);
+  return examples.find(
+    (example) =>
+      normalizeLanguage(example.binding) === normalized ||
+      normalizeLanguage(example.language) === normalized
+  )?.binding;
+}
+
 export default function ServicePage({ data }) {
   const { bindings, service } = data;
   const location = useLocation();
@@ -106,14 +188,15 @@ export default function ServicePage({ data }) {
   const labelOf = Object.fromEntries(bindings.map((b) => [b.id, b.label]));
   const available = service.examples.map((e) => e.binding);
   const initial =
+    bindingFromSearch(location.search, service.examples) ||
     bindingFromPath(location.pathname, service.scheme, available) ||
     available[0];
 
   const [active, setActive] = useState(initial);
   useEffect(() => {
     setActive(initial);
-    // Re-sync when navigating between deep-link pages on the client.
-  }, [location.pathname]);
+    // Re-sync when navigating between deep-link pages or URL-selected tabs.
+  }, [location.pathname, location.search]);
 
   const example = service.examples.find((e) => e.binding === active);
 
@@ -123,11 +206,12 @@ export default function ServicePage({ data }) {
   return (
     <Layout title={title} description={description}>
       <div className={styles.page}>
-        <nav className={styles.breadcrumb} aria-label="Breadcrumb">
-          <Link to="/services">Services</Link>
-          <span aria-hidden="true"> / </span>
-          <span>{service.scheme}</span>
-        </nav>
+        <Breadcrumb
+          aria-label="Breadcrumb"
+          rootLabel="Services"
+          rootHref="/services"
+          items={[{ label: service.scheme }]}
+        />
 
         <header className={styles.header}>
           <h1 className={styles.title}>{service.scheme}</h1>
@@ -146,7 +230,7 @@ export default function ServicePage({ data }) {
               onChange={(e) => setActive(e.binding)}
               getId={(e) => e.binding}
               getLabel={(e) => labelOf[e.binding] || e.binding}
-              ariaLabel="Choose a binding"
+              aria-label="Choose a binding"
               controlsId="service-example-panel"
               id="service-example-tabs"
             />
@@ -162,7 +246,9 @@ export default function ServicePage({ data }) {
             </div>
 
             <details className={styles.details}>
-              <summary>All configuration options (copy &amp; trim)</summary>
+              <summary className={styles.detailsSummary}>
+                All configuration options (copy &amp; trim)
+              </summary>
               <CodeBlock language={example.language} title="Full reference">
                 {example.full}
               </CodeBlock>
diff --git a/website/src/components/services/styles.module.css 
b/website/src/components/services/styles.module.css
index 89c974922..e0334c8c8 100644
--- a/website/src/components/services/styles.module.css
+++ b/website/src/components/services/styles.module.css
@@ -24,12 +24,6 @@
   overflow-wrap: anywhere;
 }
 
-.breadcrumb {
-  font-size: 0.9rem;
-  color: var(--ifm-color-emphasis-600);
-  margin-bottom: 1rem;
-}
-
 .header {
   margin-bottom: 2rem;
 }
@@ -113,7 +107,7 @@
   margin-top: 0.5rem;
 }
 
-.details > summary {
+.detailsSummary {
   cursor: pointer;
   font-weight: 500;
   padding: 0.5rem 0;
@@ -126,23 +120,39 @@
   color: var(--ifm-color-emphasis-600);
 }
 
+.page :global(.theme-code-block),
+.page :global([class*="codeBlockContainer"]) {
+  max-width: 100%;
+}
+
 .group {
-  border: 1px solid var(--ifm-color-emphasis-200);
-  border-radius: var(--ifm-global-radius);
+  border: 1px solid var(--odl-code-border);
+  border-radius: var(--odl-radius-md);
   margin-bottom: 0.75rem;
   overflow: hidden;
+  background: var(--ifm-background-color);
+  background-clip: padding-box;
 }
 
 .groupSummary {
   cursor: pointer;
-  font-weight: 600;
+  font-size: 0.95rem;
+  font-weight: 700;
   padding: 0.7rem 1rem;
-  background: var(--ifm-color-emphasis-100);
+  background: var(--odl-code-bg);
+  color: var(--ifm-heading-color);
+  border-radius: calc(var(--odl-radius-md) - 1px);
   display: flex;
   align-items: center;
   gap: 0.5rem;
 }
 
+.group[open] > .groupSummary {
+  border-bottom: 1px solid var(--odl-code-border);
+  border-radius: calc(var(--odl-radius-md) - 1px)
+    calc(var(--odl-radius-md) - 1px) 0 0;
+}
+
 .groupCount {
   font-size: 0.75rem;
   font-weight: 500;
@@ -164,7 +174,7 @@
   white-space: nowrap;
 }
 
-.chip code {
+.chipCode {
   font-size: 0.72rem;
   background: transparent;
   border: none;
@@ -172,40 +182,87 @@
 }
 
 .tableWrap {
+  margin: 0;
   overflow-x: auto;
+  overflow-y: hidden;
   max-width: 100%;
   -webkit-overflow-scrolling: touch;
+  background: var(--ifm-background-color);
 }
 
 .table {
+  --service-table-border: var(--odl-code-border);
+  --service-table-border-strong: var(--odl-border-strong);
+  --service-table-stripe: var(--odl-code-bg);
+
   width: 100%;
   min-width: 720px;
+  margin: 0;
   display: table;
-  border-collapse: collapse;
+  border: 0;
+  border-collapse: separate;
+  border-spacing: 0;
+  background: transparent;
+}
+
+.tableHeaderRow {
+  border-top: 0;
+  background: transparent;
 }
 
-.table th,
-.table td {
+.tableRow {
+  border-top: 0;
+  background: transparent;
+}
+
+.tableHeaderCell,
+.tableCell {
   text-align: left;
   vertical-align: top;
   padding: 0.6rem 0.75rem;
-  border-bottom: 1px solid var(--ifm-color-emphasis-200);
+  border: 0;
+  background: transparent;
+  overflow-wrap: normal;
+  word-break: normal;
 }
 
-.table th {
+.tableHeaderCell {
   font-size: 0.8rem;
   text-transform: uppercase;
   letter-spacing: 0.03em;
   color: var(--ifm-color-emphasis-600);
+  border-bottom: 1px solid var(--service-table-border);
+}
+
+.tableRow:not(:last-child) .tableCell {
+  border-bottom: 1px solid var(--service-table-border);
+}
+
+.tableHeaderCell:not(:last-child),
+.tableCell:not(:last-child) {
+  border-right: 1px solid var(--service-table-border);
+}
+
+.tableHeaderCell:nth-child(2),
+.tableCell:nth-child(2) {
+  min-width: 5.5rem;
+  white-space: nowrap;
+}
+
+.tableHeaderCell:nth-child(3),
+.tableCell:nth-child(3) {
+  min-width: 6.75rem;
+  white-space: nowrap;
 }
 
 .desc {
   white-space: pre-wrap;
+  overflow-wrap: anywhere;
   font-size: 0.9rem;
   color: var(--ifm-color-emphasis-800);
 }
 
-.deprecatedRow code {
+.deprecatedRow .configKey {
   text-decoration: line-through;
 }
 
@@ -230,6 +287,22 @@
     padding: 1.75rem 0.875rem 3rem;
   }
 
+  .page :global(.theme-code-block),
+  .page :global([class*="codeBlockContainer"]) {
+    overflow: hidden;
+  }
+
+  .page :global(.prism-code) {
+    white-space: pre-wrap;
+    overflow-wrap: anywhere;
+    word-break: break-word;
+  }
+
+  .page :global(.token-line) {
+    white-space: inherit;
+    overflow-wrap: inherit;
+  }
+
   .grid {
     grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
     gap: 0.75rem;
@@ -239,6 +312,10 @@
     padding: 0.85rem 0.9rem;
   }
 
+  .groupSummary {
+    padding: 0.65rem 0.875rem;
+  }
+
   .tableWrap {
     overflow-x: visible;
   }
@@ -248,28 +325,45 @@
     display: block;
   }
 
-  .table thead {
+  .tableHead {
     display: none;
   }
 
-  .table tbody,
-  .table tr,
-  .table td {
+  .tableBody,
+  .tableRow,
+  .tableCell {
+    box-sizing: border-box;
     display: block;
     width: 100%;
   }
 
-  .table tr {
-    padding: 0.8rem 0.875rem;
-    border-bottom: 1px solid var(--ifm-color-emphasis-200);
+  .tableBody > .tableRow {
+    padding: 0;
+    border-bottom: 0;
+    background-color: var(--ifm-background-color);
   }
 
-  .table td {
-    padding: 0.25rem 0;
-    border-bottom: 0;
+  .tableBody > .tableRow:nth-child(2n) {
+    background-color: var(--service-table-stripe);
+  }
+
+  .tableRow > .tableCell {
+    border-bottom: 1px solid var(--service-table-border-strong);
+    border-right: 0;
+  }
+
+  .tableCell {
+    padding: 0.28rem 0.875rem;
+    background-color: transparent;
+  }
+
+  .tableCell:nth-child(2),
+  .tableCell:nth-child(3) {
+    min-width: 0;
+    white-space: normal;
   }
 
-  .table td::before {
+  .tableCell::before {
     content: attr(data-label);
     display: block;
     margin-bottom: 0.1rem;
@@ -279,7 +373,7 @@
     color: var(--ifm-color-emphasis-600);
   }
 
-  .table td:first-child::before {
+  .tableKeyCell::before {
     content: "Key";
   }
 
diff --git a/website/src/components/ui/Breadcrumb.jsx 
b/website/src/components/ui/Breadcrumb.jsx
new file mode 100644
index 000000000..de140805b
--- /dev/null
+++ b/website/src/components/ui/Breadcrumb.jsx
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from "react";
+import Link from "@docusaurus/Link";
+import styles from "./breadcrumb.module.css";
+
+export default function Breadcrumb({
+  rootLabel,
+  rootHref,
+  items,
+  className = "",
+  ...navProps
+}) {
+  const classNames = [styles.breadcrumb, className].filter(Boolean).join(" ");
+
+  return (
+    <nav {...navProps} className={classNames}>
+      <Link to={rootHref}>{rootLabel}</Link>
+      {items.map((item, index) => {
+        const isLast = index === items.length - 1;
+        return (
+          <React.Fragment key={`${item.label}-${index}`}>
+            <span aria-hidden="true"> / </span>
+            {!isLast && item.href ? (
+              <Link to={item.href}>{item.label}</Link>
+            ) : (
+              <span>{item.label}</span>
+            )}
+          </React.Fragment>
+        );
+      })}
+    </nav>
+  );
+}
diff --git a/website/src/components/ui/CodeWindow.jsx 
b/website/src/components/ui/CodeWindow.jsx
index 6c95a37eb..ba2f6283f 100644
--- a/website/src/components/ui/CodeWindow.jsx
+++ b/website/src/components/ui/CodeWindow.jsx
@@ -24,9 +24,9 @@ import styles from "./code-window.module.css";
 function CodeWindowDots() {
   return (
     <div className={styles.windowDots} aria-hidden="true">
-      <span />
-      <span />
-      <span />
+      <span className={styles.windowDot} />
+      <span className={styles.windowDot} />
+      <span className={styles.windowDot} />
     </div>
   );
 }
diff --git a/website/src/components/ui/Tabs.jsx 
b/website/src/components/ui/Tabs.jsx
index 6fbe7c475..90cf10b15 100644
--- a/website/src/components/ui/Tabs.jsx
+++ b/website/src/components/ui/Tabs.jsx
@@ -26,10 +26,10 @@ export default function Tabs({
   onChange,
   getId = (item) => item.id,
   getLabel = (item) => item.label,
-  ariaLabel,
   controlsId,
   id,
   className = "",
+  ...tabListProps
 }) {
   const generatedId = useId();
   const baseId = id || generatedId;
@@ -74,9 +74,9 @@ export default function Tabs({
 
   return (
     <div
+      {...tabListProps}
       className={[styles.tabs, className].filter(Boolean).join(" ")}
       role="tablist"
-      aria-label={ariaLabel}
       onKeyDown={onKeyDown}
     >
       {items.map((item, index) => {
diff --git a/website/src/css/footer.css 
b/website/src/components/ui/breadcrumb.module.css
similarity index 69%
copy from website/src/css/footer.css
copy to website/src/components/ui/breadcrumb.module.css
index ff47cf001..316498c5f 100644
--- a/website/src/css/footer.css
+++ b/website/src/components/ui/breadcrumb.module.css
@@ -17,22 +17,9 @@
  * under the License.
  */
 
-.footer {
-  background-color: var(--odl-bg-subtle);
-  border-top: 1px solid var(--odl-border);
-}
-
-.footer__copyright {
-  font-size: 0.75em;
-  color: var(--odl-fg-subtle);
-  margin-top: var(--odl-space-4);
-}
-
-.footer__logo {
-  margin-top: unset;
-  max-width: 280px;
-}
-
-[data-theme="dark"] .footer__logo {
-  filter: brightness(0) invert(1) opacity(0.7);
+.breadcrumb {
+  font-size: 0.9rem;
+  line-height: var(--ifm-line-height-base);
+  color: var(--ifm-color-emphasis-600);
+  margin-bottom: 1rem;
 }
diff --git a/website/src/components/ui/code-window.module.css 
b/website/src/components/ui/code-window.module.css
index ecb07e687..4a6e2f413 100644
--- a/website/src/components/ui/code-window.module.css
+++ b/website/src/components/ui/code-window.module.css
@@ -40,7 +40,7 @@
   display: flex;
   gap: var(--code-window-dot-gap);
 }
-.windowDots span {
+.windowDot {
   width: var(--code-window-dot-size);
   height: var(--code-window-dot-size);
   border-radius: var(--odl-radius-pill);
@@ -97,13 +97,13 @@
 }
 
 .codeBody :global(.theme-code-block),
-.codeBody :global(div[class*="codeBlockContainer"]) {
+.codeBody :global([class*="codeBlockContainer"]) {
   margin: 0;
   border: 0;
   border-radius: 0;
   box-shadow: none;
 }
-.codeBody :global(pre) {
+.codeBody :global(.prism-code) {
   border: 0 !important;
   border-radius: 0 !important;
   margin: 0;
@@ -112,7 +112,6 @@
   white-space: pre-wrap;
   overflow-wrap: anywhere;
 }
-.codeBody :global(pre code),
 .codeBody :global(.token-line) {
   white-space: inherit;
   overflow-wrap: inherit;
diff --git a/website/src/css/footer.css b/website/src/css/footer.css
index ff47cf001..aa5e47ec2 100644
--- a/website/src/css/footer.css
+++ b/website/src/css/footer.css
@@ -18,10 +18,15 @@
  */
 
 .footer {
-  background-color: var(--odl-bg-subtle);
+  background-color: var(--odl-bg);
   border-top: 1px solid var(--odl-border);
 }
 
+html.docs-wrapper .footer,
+html.plugin-opendal-services-docs .footer {
+  background-color: var(--odl-bg-subtle);
+}
+
 .footer__copyright {
   font-size: 0.75em;
   color: var(--odl-fg-subtle);
diff --git a/website/src/css/global.css b/website/src/css/global.css
index 86447b783..4e9dd1c55 100644
--- a/website/src/css/global.css
+++ b/website/src/css/global.css
@@ -39,6 +39,15 @@ body {
     "ss01" 1;
 }
 
+#__docusaurus,
+.main-wrapper,
+[class*="docsWrapper"],
+[class*="docRoot"],
+[class*="docMainContainer"],
+[class*="docItemWrapper"] {
+  background: var(--odl-bg);
+}
+
 h1,
 h2,
 h3,
diff --git a/website/src/theme/DocBreadcrumbs/index.jsx 
b/website/src/theme/DocBreadcrumbs/index.jsx
new file mode 100644
index 000000000..8c53c10ed
--- /dev/null
+++ b/website/src/theme/DocBreadcrumbs/index.jsx
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from "react";
+import { translate } from "@docusaurus/Translate";
+import { useLocation } from "@docusaurus/router";
+import Breadcrumb from "@site/src/components/ui/Breadcrumb";
+
+const ROOTS = {
+  docs: "Docs",
+  community: "Community",
+};
+
+const LABELS = {
+  core: "Rust",
+  cpp: "C++",
+  dotnet: ".NET",
+  nodejs: "Node.js",
+  ocaml: "OCaml",
+  php: "PHP",
+  "pmc-members": "PMC Members",
+};
+
+function labelFromSegment(segment) {
+  if (LABELS[segment]) {
+    return LABELS[segment];
+  }
+
+  return decodeURIComponent(segment)
+    .split(/[-_]+/)
+    .filter(Boolean)
+    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+    .join(" ");
+}
+
+function breadcrumbFromPath(pathname) {
+  const segments = pathname.split("/").filter(Boolean);
+  const rootIndex = segments.findIndex((segment) => ROOTS[segment]);
+  if (rootIndex === -1) {
+    return null;
+  }
+
+  const root = segments[rootIndex];
+  const pathSegments = segments.slice(rootIndex + 1);
+  const items = [];
+
+  if (root === "docs" && pathSegments[0] === "core") {
+    items.push({ label: labelFromSegment("core"), href: "/docs/core" });
+    pathSegments.slice(1).forEach((segment, index) => {
+      items.push({
+        label: labelFromSegment(segment),
+        href: `/docs/core/${pathSegments.slice(1, index + 2).join("/")}`,
+      });
+    });
+  } else if (root === "docs" && pathSegments[0] === "bindings") {
+    const binding = pathSegments[1];
+    if (binding) {
+      items.push({
+        label: labelFromSegment(binding),
+        href: `/docs/bindings/${binding}`,
+      });
+      pathSegments.slice(2).forEach((segment, index) => {
+        items.push({
+          label: labelFromSegment(segment),
+          href: `/docs/bindings/${binding}/${pathSegments
+            .slice(2, index + 3)
+            .join("/")}`,
+        });
+      });
+    }
+  } else {
+    const current = pathSegments[pathSegments.length - 1] || "overview";
+    items.push({ label: labelFromSegment(current) });
+  }
+
+  return {
+    rootLabel: ROOTS[root],
+    rootHref: `/${root}`,
+    items,
+  };
+}
+
+export default function DocBreadcrumbs() {
+  const location = useLocation();
+  const breadcrumb = breadcrumbFromPath(location.pathname);
+
+  if (!breadcrumb) {
+    return null;
+  }
+
+  const items = breadcrumb.items.map((item, index) => ({
+    ...item,
+    href: index === breadcrumb.items.length - 1 ? undefined : item.href,
+  }));
+
+  return (
+    <Breadcrumb
+      aria-label={translate({
+        id: "theme.docs.breadcrumbs.navAriaLabel",
+        message: "Breadcrumbs",
+        description: "The ARIA label for the breadcrumbs",
+      })}
+      rootLabel={breadcrumb.rootLabel}
+      rootHref={breadcrumb.rootHref}
+      items={items}
+    />
+  );
+}

Reply via email to