You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/06/17 15:17:17 UTC

(opendal) 01/01: docs: rework per-language guides around a task-centric structure

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

Xuanwo pushed a commit to branch xuanwo/dreamy-goldstine-aceb62
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit 1592136ff0e61a7ed697bc1aded754b092d56618
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Wed Jun 17 23:17:06 2026 +0800

    docs: rework per-language guides around a task-centric structure
    
    Every language (Rust core + 16 bindings) now follows the same skeleton — Overview, Getting started, Connecting to your storage, Common tasks, Going to production — with thin/experimental bindings shipping a subset. Languages are listed flat under a "Languages" divider with brand logos, replacing the separate Core/Bindings split.
    
    Each language README is condensed into a user-facing landing that links to the guide; contributor-only build/test content moves into CONTRIBUTING.
---
 bindings/c/README.md                               | 167 +++-------
 bindings/cpp/README.md                             | 122 ++++---
 bindings/d/README.md                               |  52 ++-
 bindings/dart/README.md                            | 119 +++----
 bindings/dotnet/README.md                          | 250 ++------------
 bindings/go/{README.md => CONTRIBUTING.md}         | 121 +------
 bindings/go/README.md                              | 358 ++-------------------
 bindings/haskell/README.md                         |  77 ++---
 bindings/java/CONTRIBUTING.md                      |  71 ++++
 bindings/java/README.md                            | 143 +++-----
 bindings/lua/README.md                             |  91 +++---
 bindings/nodejs/CONTRIBUTING.md                    |  22 ++
 bindings/nodejs/README.md                          | 219 +------------
 bindings/ocaml/README.md                           | 128 ++++----
 bindings/php/README.md                             |  89 ++---
 bindings/python/README.md                          | 110 ++-----
 bindings/ruby/CONTRIBUTING.md                      |  32 ++
 bindings/ruby/README.md                            | 143 ++------
 bindings/swift/README.md                           |  91 ++++--
 bindings/zig/README.md                             |  58 +++-
 core/README.md                                     | 217 +++----------
 website/docs/03-concepts.mdx                       |   4 +-
 website/docs/10-core.mdx                           |  11 -
 website/docs/10-core/01-overview.md                |  67 ++++
 website/docs/10-core/02-getting-started.md         | 103 ++++++
 website/docs/10-core/03-connecting.md              |  98 ++++++
 website/docs/10-core/04-tasks.md                   | 157 +++++++++
 website/docs/10-core/05-production.md              |  89 +++++
 website/docs/20-bindings/_category_.yml            |  23 --
 website/docs/20-bindings/c.mdx                     |  10 -
 website/docs/20-bindings/c/01-overview.md          |  73 +++++
 website/docs/20-bindings/c/02-getting-started.md   | 149 +++++++++
 website/docs/20-bindings/cpp.mdx                   |  10 -
 website/docs/20-bindings/cpp/01-overview.md        |  87 +++++
 website/docs/20-bindings/cpp/02-getting-started.md | 197 ++++++++++++
 website/docs/20-bindings/d.mdx                     |  10 -
 website/docs/20-bindings/d/01-overview.md          |  98 ++++++
 website/docs/20-bindings/d/02-getting-started.md   | 124 +++++++
 website/docs/20-bindings/dart.mdx                  |  10 -
 website/docs/20-bindings/dart/01-overview.md       |  59 ++++
 .../docs/20-bindings/dart/02-getting-started.md    | 111 +++++++
 website/docs/20-bindings/dotnet.mdx                |  10 -
 website/docs/20-bindings/dotnet/01-overview.md     |  64 ++++
 .../docs/20-bindings/dotnet/02-getting-started.md  | 100 ++++++
 website/docs/20-bindings/dotnet/03-connecting.md   |  89 +++++
 website/docs/20-bindings/dotnet/04-tasks.md        | 162 ++++++++++
 website/docs/20-bindings/dotnet/05-production.md   | 112 +++++++
 website/docs/20-bindings/go.mdx                    |  10 -
 website/docs/20-bindings/go/01-overview.md         |  67 ++++
 website/docs/20-bindings/go/02-getting-started.md  | 101 ++++++
 website/docs/20-bindings/go/03-connecting.md       | 109 +++++++
 website/docs/20-bindings/go/04-tasks.md            | 197 ++++++++++++
 website/docs/20-bindings/go/05-production.md       | 103 ++++++
 website/docs/20-bindings/haskell.mdx               |  10 -
 website/docs/20-bindings/haskell/01-overview.md    |  70 ++++
 .../docs/20-bindings/haskell/02-getting-started.md | 195 +++++++++++
 website/docs/20-bindings/java.mdx                  |  10 -
 website/docs/20-bindings/java/01-overview.md       | 105 ++++++
 .../docs/20-bindings/java/02-getting-started.md    |  97 ++++++
 website/docs/20-bindings/java/03-connecting.md     |  92 ++++++
 website/docs/20-bindings/java/04-tasks.md          | 151 +++++++++
 website/docs/20-bindings/java/05-production.md     |  88 +++++
 website/docs/20-bindings/lua.mdx                   |  10 -
 website/docs/20-bindings/lua/01-overview.md        |  73 +++++
 website/docs/20-bindings/lua/02-getting-started.md | 123 +++++++
 website/docs/20-bindings/nodejs.mdx                |  10 -
 website/docs/20-bindings/nodejs/01-overview.md     |  59 ++++
 .../docs/20-bindings/nodejs/02-getting-started.md  |  84 +++++
 website/docs/20-bindings/nodejs/03-connecting.md   |  82 +++++
 website/docs/20-bindings/nodejs/04-tasks.md        | 166 ++++++++++
 website/docs/20-bindings/nodejs/05-production.md   | 120 +++++++
 website/docs/20-bindings/ocaml.mdx                 |  10 -
 website/docs/20-bindings/ocaml/01-overview.md      |  76 +++++
 .../docs/20-bindings/ocaml/02-getting-started.md   | 175 ++++++++++
 website/docs/20-bindings/php.mdx                   |  10 -
 website/docs/20-bindings/php/01-overview.md        | 102 ++++++
 website/docs/20-bindings/php/02-getting-started.md | 120 +++++++
 website/docs/20-bindings/python.mdx                |  10 -
 website/docs/20-bindings/python/01-overview.md     |  56 ++++
 .../docs/20-bindings/python/02-getting-started.md  |  78 +++++
 website/docs/20-bindings/python/03-connecting.md   |  77 +++++
 website/docs/20-bindings/python/04-tasks.md        | 131 ++++++++
 website/docs/20-bindings/python/05-production.md   |  76 +++++
 website/docs/20-bindings/ruby.mdx                  |  10 -
 website/docs/20-bindings/ruby/01-overview.md       |  62 ++++
 .../docs/20-bindings/ruby/02-getting-started.md    |  64 ++++
 website/docs/20-bindings/ruby/03-connecting.md     |  75 +++++
 website/docs/20-bindings/ruby/04-tasks.md          | 130 ++++++++
 website/docs/20-bindings/ruby/05-production.md     |  73 +++++
 website/docs/20-bindings/swift.mdx                 |  10 -
 website/docs/20-bindings/swift/01-overview.md      |  89 +++++
 .../docs/20-bindings/swift/02-getting-started.md   | 101 ++++++
 website/docs/20-bindings/zig.mdx                   |  10 -
 website/docs/20-bindings/zig/01-overview.md        | 104 ++++++
 website/docs/20-bindings/zig/02-getting-started.md | 131 ++++++++
 website/docs/sidebars.js                           |  50 ++-
 website/src/css/custom.css                         |  49 +++
 97 files changed, 6683 insertions(+), 2035 deletions(-)

diff --git a/bindings/c/README.md b/bindings/c/README.md
index c4ad228fa..4fda46e66 100644
--- a/bindings/c/README.md
+++ b/bindings/c/README.md
@@ -2,142 +2,79 @@
 
 [![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/bindings/c/)
 
-![](https://github.com/apache/opendal/assets/5351546/87bbf6e5-f19e-449a-b368-3e283016c887)
+A C binding for OpenDAL, providing blocking access to S3, GCS, Azure Blob, the
+local filesystem, and 50+ more storage backends through a single C API.
 
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
+> **Note**: This binding has its own independent version number, which may differ
+> from the Rust core version.
 
-## Example
-
-A simple read and write example
-
-```C
-#include "assert.h"
-#include "opendal.h"
-#include "stdio.h"
-
-int main()
-{
-    /* Initialize a operator for "memory" backend, with no options */
-    opendal_result_operator_new result = opendal_operator_new("memory", 0);
-    assert(result.op != NULL);
-    assert(result.error == NULL);
-
-    opendal_operator* op = result.op;
-
-    /* Prepare some data to be written */
-    opendal_bytes data = {
-        .data = (uint8_t*)"this_string_length_is_24",
-        .len = 24,
-    };
-
-    /* Write this into path "/testpath" */
-    opendal_error *error = opendal_operator_write(op, "/testpath", &data);
-    assert(error == NULL);
-
-    /* We can read it out, make sure the data is the same */
-    opendal_result_read r = opendal_operator_read(op, "/testpath");
-    opendal_bytes read_bytes = r.data;
-    assert(r.error == NULL);
-    assert(read_bytes.len == 24);
-
-    /* Lets print it out */
-    for (int i = 0; i < 24; ++i) {
-        printf("%c", read_bytes.data[i]);
-    }
-    printf("\n");
-
-    /* the opendal_bytes read is heap allocated, please free it */
-    opendal_bytes_free(&read_bytes);
-
-    /* the operator_ptr is also heap allocated */
-    opendal_operator_free(&op);
-}
-```
-
-For more examples, please refer to `./examples`
-
-## Prerequisites
-
-To build OpenDAL C binding, the following is all you need:
-
-- A compiler that supports **C11** and **C++14**, _e.g._ clang and gcc
-
-- To format the code, you need to install **clang-format**
+**User guide**: https://opendal.apache.org/docs/bindings/c
 
-  - The `opendal.h` is not formatted by hands when you contribute, please do not format the file. **Use `make format` only.**
-  - If your contribution is related to the files under `./tests`, you may format it before submitting your pull request. But notice that different versions of `clang-format` may format the files differently.
+## Building
 
-- (optional) **Doxygen** need to be installed to generate documentations.
-
-For Ubuntu and Debian:
+Requirements: C11/C++14 compiler, CMake, Rust toolchain.
 
 ```shell
-# install C/C++ toolchain
-sudo apt install -y build-essential
-
-# install clang-format
-sudo apt install clang-format
+mkdir -p build && cd build
 
-# install and build GTest library under /usr/lib and softlink to /usr/local/lib
-sudo apt-get install libgtest-dev
+# Default build (memory + fs services)
+cmake ..
+make
 
-# install CMake
-sudo apt-get install cmake
-
-# install Rust
-curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+# Enable additional services
+cmake .. -DFEATURES="opendal/services-s3,opendal/services-gcs"
+make
 ```
 
-## Makefile
-
-- To **build the library and header file**.
-
-  ```sh
-  mkdir -p build && cd build
-  cmake ..
-  make
-  ```
-
-  - The header file `opendal.h` is under `./include`
-
-  - The library is under `../../target/debug` after building.
-
-  - use `FEATURES` to enable services, like `cmake .. -DFEATURES="opendal/services-memory,opendal/services-fs"`
+The header is at `include/opendal.h`. The shared library is written to
+`../../target/debug/` after building.
 
-- To **clean** the build results.
+## Example
 
-  ```sh
-  cargo clean
-  cd build && make clean
-  ```
+```c
+#include <assert.h>
+#include <stdio.h>
+#include "opendal.h"
 
-- To build and run the **tests**. (Note that you need to install Valgrind and GTest)
+int main(void)
+{
+    /* Build an operator for the "memory" service. */
+    opendal_result_operator_new result = opendal_operator_new("memory", NULL);
+    assert(result.op != NULL && result.error == NULL);
+    opendal_operator *op = result.op;
+
+    /* Write bytes. */
+    const char *msg = "Hello, OpenDAL!";
+    opendal_bytes data = { .data = (uint8_t *)msg, .len = 15 };
+    opendal_error *err = opendal_operator_write(op, "/hello.txt", &data);
+    assert(err == NULL);
+
+    /* Read them back. */
+    opendal_result_read r = opendal_operator_read(op, "/hello.txt");
+    assert(r.error == NULL);
+    printf("%.*s\n", (int)r.data.len, r.data.data);
+    opendal_bytes_free(&r.data);
 
-  ```sh
-  cd build
-  make tests && ./tests
-  ```
+    opendal_operator_free(op);
+    return 0;
+}
+```
 
-- To build the **examples**
+See `examples/` for more, including error handling patterns.
 
-  ```sh
-  cd build
-  make basic error_handle
-  ```
+Full documentation is at https://opendal.apache.org/docs/bindings/c.
 
-## Documentation
+## Contributing
 
-The documentation index page source is under `./docs/doxygen/html/index.html`.
-If you want to build the documentations yourself, you could use
+See [`CONTRIBUTING.md`](../../CONTRIBUTING.md) at the repository root. To run
+tests you need Valgrind and GTest:
 
-```sh
-# this requires you to install doxygen
-make doc
+```shell
+cd build && make tests && ./tests
 ```
 
-## Used by
-
-Check out the [users](./users.md) list for more details on who is using OpenDAL.
+Format C sources with `make format` (requires `clang-format`). Do not manually
+format `include/opendal.h`.
 
 ## License and Trademarks
 
diff --git a/bindings/cpp/README.md b/bindings/cpp/README.md
index 76ba7f1d1..4007a61a0 100644
--- a/bindings/cpp/README.md
+++ b/bindings/cpp/README.md
@@ -1,38 +1,30 @@
-# Apache OpenDAL™ CPP Binding (WIP)
+# Apache OpenDAL™ C++ Binding
 
-[![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/bindings/cpp/)
+[![status: unreleased](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/docs/bindings/cpp)
 
-> **Note**: This C++ binding follows the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) for consistent and maintainable code.
+A C++ binding for OpenDAL, providing access to S3, GCS, Azure Blob, HDFS, the
+local filesystem, and 50+ more services through one API.
 
-![](https://github.com/apache/opendal/assets/5351546/87bbf6e5-f19e-449a-b368-3e283016c887)
+> **Note**: This binding is experimental/WIP. Only Clang and AppleClang are
+> currently supported. The API may change without notice.
 
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
+> **Note**: This binding has its own independent version number. When checking
+> compatibility, refer to this binding's version, not the Rust core version.
 
-Documents: [![Documents](https://img.shields.io/badge/opendal-cpp-blue?logo=Apache&logoColor=red)](https://opendal.apache.org/docs/cpp/)
+## Useful Links
 
-## Example
-
-```cpp
-#include "opendal.hpp"
-#include <vector>
-
-int main() {
-    auto op = opendal::Operator("memory");
-    std::vector<uint8_t> data = {1, 2, 3, 4, 5};
-    op.Write("test", data);
-    auto result = op.Read("test");  // result == data
-}
-```
+- **User guide**: [opendal.apache.org/docs/bindings/cpp](https://opendal.apache.org/docs/bindings/cpp)
+- **Services & configuration**: [opendal.apache.org/services](https://opendal.apache.org/services)
+- **Source**: [`bindings/cpp/`](https://github.com/apache/opendal/tree/main/bindings/cpp)
+- **Examples**: [`examples/cpp/`](https://github.com/apache/opendal/tree/main/examples/cpp)
 
-More examples can be found [here](../../examples/cpp).
+## Integration via CMake
 
-## Using
-
-### CMake
-
-You can use `FetchContent` to add OpenDAL to your project.
+### FetchContent
 
 ```cmake
+include(FetchContent)
+
 FetchContent_Declare(
   opendal-cpp
   GIT_REPOSITORY https://github.com/apache/opendal.git
@@ -40,78 +32,74 @@ FetchContent_Declare(
   SOURCE_SUBDIR  bindings/cpp
 )
 FetchContent_MakeAvailable(opendal-cpp)
+
+target_link_libraries(your_target opendal_cpp)
 ```
 
-Or you can download the source code and add it to your project.
+### Vendored source
 
 ```shell
-mkdir third_party
-cd third_party
+mkdir third_party && cd third_party
 git clone https://github.com/apache/opendal.git
-git checkout v0.40.0
+cd opendal && git checkout v0.40.0
 ```
 
 ```cmake
 add_subdirectory(third_party/opendal/bindings/cpp)
-```
-
-Now you can use OpenDAL in your project.
-
-```cmake
 target_link_libraries(your_target opendal_cpp)
 ```
 
-### Others
-
-Support for more package managers is coming soon!
-
-## Compiling
-
 ### Prerequisites
 
-- CMake >= 3.22
-- C++ compiler with C++17 support
-- **Currently only Clang or AppleClang are supported**
+- CMake ≥ 3.22
+- Clang or AppleClang with C++17 support (C++20 required for async)
 
 ### Build
 
-```bash
-mkdir build
-cd build
-# Add -DOPENDAL_DEV=ON to make development environment for OpenDAL
+```shell
+mkdir build && cd build
 cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ ..
 make
 ```
 
-### Dev Setup
-
-We provide a default VSCode configuration in `.vscode/settings.json` at the project root. After installing the clangd extension in VSCode, restart the editor to enable proper code completion and IntelliSense support.
+### CMake options
 
-### Test
+| Option | Default | Description |
+|---|---|---|
+| `OPENDAL_DEV` | `OFF` | Enable all development options at once |
+| `OPENDAL_FEATURES` | `""` | Comma-separated list of services, e.g. `"opendal/services-s3"` |
+| `OPENDAL_ENABLE_ASYNC` | `OFF` | Enable async API (requires C++20) |
+| `OPENDAL_ENABLE_TESTING` | `OFF` | Build and enable tests |
+| `OPENDAL_ENABLE_DOCUMENTATION` | `OFF` | Build Doxygen docs |
+| `OPENDAL_ENABLE_ADDRESS_SANITIZER` | `OFF` | Enable address sanitizer |
 
-You should build the project with `OPENDAL_ENABLE_TESTING` option. Then run:
+## Example
 
-```bash
-make test
-```
+```cpp
+#include "opendal.hpp"
+#include <iostream>
 
-### Docs
+int main() {
+    // Construct an operator. The second argument is a config map.
+    opendal::Operator op("memory");
 
-You should build the project with `OPENDAL_ENABLE_DOCUMENTATION` option. Then run:
+    // Write, read, stat.
+    op.Write("hello.txt", "Hello, OpenDAL!");
+    std::string data = op.Read("hello.txt");
+    std::cout << data << "\n";
 
-```bash
-make docs
+    opendal::Metadata meta = op.Stat("hello.txt");
+    std::cout << "size=" << meta.ContentLength() << "\n";
+}
 ```
 
-### CMake Options
+See the [user guide](https://opendal.apache.org/docs/bindings/cpp) for more
+examples including streaming reads, directory listing, real backends, and the
+async API.
+
+## Contributing
 
-- `OPENDAL_DEV`: Enable development environment for OpenDAL. It will enable most development options. With this option, you don't need to set other options. Default: `OFF`
-- `OPENDAL_ENABLE_ADDRESS_SANITIZER`: Enable address sanitizer. Default: `OFF`
-- `OPENDAL_ENABLE_DOCUMENTATION`: Enable documentation. Default: `OFF`
-- `OPENDAL_DOCS_ONLY`: Only build documentation. Default: `OFF`
-- `OPENDAL_ENABLE_TESTING`: Enable testing. Default: `OFF`
-- `OPENDAL_ENABLE_ASYNC`: Enable async support. Requires Clang or AppleClang with C++20. Default: `OFF`
-- `OPENDAL_FEATURES`: Specify OpenDAL services to include, like `"opendal/services-s3,opendal/services-memory"`. Default: `""`
+See [CONTRIBUTING.md](../../CONTRIBUTING.md) at the repository root.
 
 ## License and Trademarks
 
diff --git a/bindings/d/README.md b/bindings/d/README.md
index 0d0157214..50be5a840 100644
--- a/bindings/d/README.md
+++ b/bindings/d/README.md
@@ -1,28 +1,52 @@
-# Apache OpenDAL™ D Binding (WIP)
+# Apache OpenDAL™ D Binding
 
-[![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/bindings/d)
+[![Status: unreleased](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/docs/bindings/d)
 
-![](https://github.com/apache/opendal/assets/5351546/87bbf6e5-f19e-449a-b368-3e283016c887)
+An early-stage D binding for [Apache OpenDAL](https://opendal.apache.org), built
+on top of the C binding. Access the local filesystem, memory, S3, and
+[50+ other storage services](/services) through one API.
 
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
+**User guide**: https://opendal.apache.org/docs/bindings/d
 
 ## Build
 
-To compile OpenDAL d binding from source code, you need:
+Requirements:
 
-- [dmd/ldc/gdc](https://dlang.org/download)
+- A D compiler: [dmd, ldc, or gdc](https://dlang.org/download)
+- Rust and Cargo (to build the underlying C library)
 
 ```bash
-# Test a specific backend
-export OPENDAL_TEST=memory
-# build libopendal_c (underneath call make -C ../c)
-dub build -b release
-# build and run unit tests
-dub test
+# From bindings/d/
+
+# Build libopendal_c and the D library (release mode)
+OPENDAL_TEST=memory dub build -b release
+
+# Build and run unit tests against the memory backend
+OPENDAL_TEST=memory dub test
 ```
 
+`OPENDAL_TEST` is read by the build script at build time to compile the named
+service into the C library. Omitting it builds only core functionality with no
+storage backends. To test against a different service, set `OPENDAL_TEST` and
+the matching `OPENDAL_{SERVICE}_*` environment variables:
+
+```bash
+OPENDAL_TEST=fs OPENDAL_FS_ROOT=/tmp/test dub test
+```
+
+See the [user guide](https://opendal.apache.org/docs/bindings/d) for a
+full example and API reference.
+
+## Contributing
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, build, and test instructions.
+All contributions are welcome — this binding is at an early stage and there is
+plenty of room to help.
+
 ## License and Trademarks
 
-Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+Licensed under the Apache License, Version 2.0:
+http://www.apache.org/licenses/LICENSE-2.0
 
-Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation.
+Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or
+trademarks of the Apache Software Foundation.
diff --git a/bindings/dart/README.md b/bindings/dart/README.md
index 3bef33a68..c5cdd14ad 100644
--- a/bindings/dart/README.md
+++ b/bindings/dart/README.md
@@ -1,102 +1,89 @@
 # Apache OpenDAL™ Dart Binding (WIP)
 
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
+A Dart binding for [Apache OpenDAL](https://opendal.apache.org/): access S3,
+GCS, Azure Blob, the local filesystem, and 50+ more services through one API.
+Built on the Rust core via [flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge).
 
-## Useful Links
+> **Note**: This binding has its own independent version number, which may
+> differ from the Rust core version.
 
-- [Examples](./examples)
+## Useful Links
 
-## Usage
+- **User guide**: [opendal.apache.org/docs/bindings/dart](https://opendal.apache.org/docs/bindings/dart)
+- **Examples**: [`examples/`](./examples)
+- **Services & configuration**: [opendal.apache.org/services](https://opendal.apache.org/services)
+- **Source**: [`bindings/dart/`](https://github.com/apache/opendal/tree/main/bindings/dart)
 
-Api is designed to be like stdlib style.
+## Installation
 
-This is stdlib
+This binding is not yet published to pub.dev. To use it, clone the repository
+and add it as a path dependency, or work directly from `bindings/dart/`.
 
-```dart
-import 'dart:io';
+You must compile the Rust native library first:
 
-void main() async {
-  final file = File('file.txt');
-  var is_exists = await file.exists();
-  print(is_exists);
-}
+```shell
+cd rust
+cargo build -r
+cd ..
+dart pub get
 ```
 
-This is opendal
+The loader first looks for the library in the platform-specific target-triple
+directory (e.g. `rust/target/aarch64-apple-darwin/release/`), then falls back
+to `rust/target/release/`. For cross-compiled targets, build with
+`--target <triple>` so the library is placed in the matching target directory.
+
+## Quickstart
 
 ```dart
+import 'dart:typed_data';
 import 'package:opendal/opendal.dart';
 
 void main() async {
-  final storage = await Storage.init(schemeStr: "fs", map: {"root": "/tmp"});
+  final storage = await Storage.init(schemeStr: "memory", map: {"root": "/"});
   final File = storage.initFile();
-  // drop-in
-  final file = File('file.txt');
-  var is_exists = await file.exists();
-  print(is_exists);
-}
-
-```
-
-## Test
 
-```
-dart run tests/opendal_test.dart
-```
+  final file = File("hello.txt");
+  await file.write(Uint8List.fromList("Hello, OpenDAL!".codeUnits));
 
-## Development
+  final data = await file.read();
+  print(String.fromCharCodes(data)); // Hello, OpenDAL!
 
-```
-dart pub get
-flutter_rust_bridge_codegen generate
-cd rust
-cargo build -r
-cd ..
-dart test
+  await file.delete();
+}
 ```
 
-For a cross-compiled build, pass the target explicitly:
+The API mirrors `dart:io` — `Storage.initFile()` and `Storage.initDir()` return
+factory functions that behave like `dart:io`'s `File` and `Directory`.
 
-```
-cd rust
-cargo build -r --target x86_64-unknown-linux-gnu # change to your arch, refer to https://doc.rust-lang.org/beta/rustc/platform-support.html
-```
+Every method has a blocking `*Sync` variant alongside the async `Future<T>` form.
 
-`Storage.init` loads the native library from the target-specific release
-directory when it exists, for example `rust/target/aarch64-apple-darwin/release/`
-on macOS arm64. For native local builds, `cargo build -r` writes the library to
-`rust/target/release/`; the loader falls back to that directory automatically.
+See the [user guide](https://opendal.apache.org/docs/bindings/dart) for more
+examples and a full operations reference.
 
-## Update generated code
+## Contributing
 
-This binding uses <https://github.com/fzyzcjy/flutter_rust_bridge>. The codegen
-version and both runtime dependencies must match exactly:
+Run tests after building the native library:
 
-- Generated Dart files record the codegen version in comments like
-  `@generated by flutter_rust_bridge @ <version>`.
-- `lib/src/rust/frb_generated.dart` exposes the generated `codegenVersion`.
-- `rust/src/frb_generated.rs` exposes `FLUTTER_RUST_BRIDGE_CODEGEN_VERSION`.
-- `pubspec.yaml` pins the Dart `flutter_rust_bridge` runtime.
-- `rust/Cargo.toml` pins the Rust `flutter_rust_bridge` runtime.
+```shell
+dart test
+```
 
-If these versions drift apart, startup and tests can fail with a
-flutter_rust_bridge content-hash or version mismatch. When upgrading
-flutter_rust_bridge:
+Update generated code when upgrading `flutter_rust_bridge`:
 
-1. Install and run the matching `flutter_rust_bridge_codegen` version.
+1. Install the matching `flutter_rust_bridge_codegen` version.
 2. Update `flutter_rust_bridge` in both `pubspec.yaml` and `rust/Cargo.toml`.
-3. Regenerate the bindings with `flutter_rust_bridge_codegen generate`.
-4. Build the Rust library with `cargo build -r`.
-5. Run `dart test` from `bindings/dart`.
+3. Run `flutter_rust_bridge_codegen generate`.
+4. Run `cargo build -r` from `rust/`.
+5. Run `dart test` to verify.
 
-Generated APIs can be renamed by flutter_rust_bridge between versions. Keep the
-public Dart wrapper stable unless the PR intentionally makes a breaking API
-change. For example, the generated `Operator` constructor may use a parameter
-name such as `scheme`, while `Storage.init(schemeStr: ...)` can preserve the
-existing public parameter and pass it through internally.
+The codegen version recorded in generated files, `lib/src/rust/frb_generated.dart`,
+and `rust/src/frb_generated.rs` must all match. Version drift causes startup
+or test failures.
 
 ## License and Trademarks
 
 Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
 
-Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation.
+Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or
+trademarks of the Apache Software Foundation.
diff --git a/bindings/dotnet/README.md b/bindings/dotnet/README.md
index 4fba43186..73b20c47c 100644
--- a/bindings/dotnet/README.md
+++ b/bindings/dotnet/README.md
@@ -2,19 +2,26 @@
 
 [![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/bindings/dotnet)
 
+This package provides a native .NET binding for **Apache OpenDAL™**, a data access
+layer that allows you to access various storage services in a unified way.
+
 > **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
 
-## Build
+## Useful Links
+
+- **User guide**: [opendal.apache.org/docs/bindings/dotnet](https://opendal.apache.org/docs/bindings/dotnet) — install, connect, common tasks, and going to production.
+- **Services & configuration**: [opendal.apache.org/services](https://opendal.apache.org/services)
+- **OpenDAL Scheme reference**: [docs.rs/opendal — enum.Scheme](https://docs.rs/opendal/latest/opendal/enum.Scheme.html)
 
-To compile OpenDAL .NET binding from source code, you need:
+## Installation
 
-- [.NET SDK](https://dotnet.microsoft.com/en-us/download/dotnet) for `net8.0` or `net10.0`.
-- Rust toolchain for building the native library.
+This binding is not yet published to NuGet. Build it from source — you need the
+[.NET SDK](https://dotnet.microsoft.com/en-us/download/dotnet) for `net8.0` or
+`net10.0` and a Rust toolchain for the native library:
 
 ```bash
 cargo build
 dotnet build
-dotnet test
 ```
 
 ## Quickstart
@@ -23,232 +30,45 @@ dotnet test
 using OpenDAL;
 using System.Text;
 
-using var executor = new Executor(2);
-using var op = new Operator("memory");
-
-await op.WriteAsync("demo.txt", Encoding.UTF8.GetBytes("hello"), executor);
-
-var bytes = await op.ReadAsync("demo.txt", executor);
-var text = Encoding.UTF8.GetString(bytes);
-
-var meta = await op.StatAsync("demo.txt", executor: executor);
-var entries = await op.ListAsync("", executor: executor);
-
-await op.DeleteAsync("demo.txt", executor);
-```
-
-If you don't pass an `Executor`, OpenDAL uses the default executor.
-
-## Creating an Operator
-
-### With scheme + dictionary options
-
-```csharp
-using OpenDAL;
-
-using var fs = new Operator("fs", new Dictionary<string, string>
-{
-	["root"] = "/tmp/opendal",
-});
-```
-
-### With typed service config
-
-```csharp
-using OpenDAL;
-using OpenDAL.ServiceConfig;
-
-using var fs = new Operator(new FsServiceConfig
-{
-	Root = "/tmp/opendal",
-});
-```
-
-## Executor and Lifetime
-
-- Prefer `using` for `Operator`, `Executor`, and stream instances.
-- Keep an `Executor` alive for the full lifetime of operations using it.
-- Disposing an `Executor` or `Operator` too early causes `ObjectDisposedException`.
-- For async calls, ensure disposal happens after awaited operations complete.
-
-## Core Operations
-
-`Operator` provides sync and async APIs for common object operations:
-
-- Read / Write
-- Stat / List
-- Delete / CreateDir
-- Copy / Rename / RemoveAll
-- PresignRead / PresignWrite / PresignStat / PresignDelete (async)
-- `Info` for scheme/root/name/capabilities
-- `Duplicate()` to create a new handle to the same backend configuration
-
-```csharp
-using OpenDAL;
-using OpenDAL.Options;
-using System.Text;
-
-using var op = new Operator("memory");
-
-op.CreateDir("logs/");
-op.Write("logs/a.txt", Encoding.UTF8.GetBytes("v1"));
-op.Copy("logs/a.txt", "logs/b.txt");
-op.Rename("logs/b.txt", "logs/c.txt");
-
-var read = op.Read("logs/c.txt", new ReadOptions { Offset = 0, Length = 2 });
-var list = op.List("logs/", new ListOptions { Recursive = true });
-
-op.RemoveAll("logs/");
-```
-
-## Options
-
-Use options types in `OpenDAL.Options` to pass operation-specific parameters.
-
-- `ReadOptions`: range, conditions, concurrency, response-header overrides.
-- `WriteOptions`: append, content headers, conditions, concurrency/chunk, user metadata.
-- `StatOptions`: conditional headers and response-header overrides.
-- `ListOptions`: recursive, limit, start-after, versions, deleted.
-
-```csharp
-using OpenDAL.Options;
-
-var writeOptions = new WriteOptions
-{
-	ContentType = "text/plain",
-	CacheControl = "no-cache",
-	IfNotExists = true,
-};
-
-await op.WriteAsync("docs/readme.txt", content, writeOptions);
-
-var listOptions = new ListOptions
-{
-	Recursive = true,
-	Limit = 100,
-};
-
-var entries = await op.ListAsync("docs/", listOptions);
-```
-
-## Streams
-
-OpenDAL exposes .NET `Stream` wrappers:
-
-- `OpenReadStream(path, readOptions, executor)` returns `OperatorInputStream`.
-- `OpenWriteStream(path, writeOptions, bufferSize, executor)` returns `OperatorOutputStream`.
-
-```csharp
-using OpenDAL;
-using System.Text;
-
+// Configure a service, then build an operator from it.
 using var op = new Operator("memory");
 
-await using (var writer = op.OpenWriteStream("stream.txt"))
-{
-	var payload = Encoding.UTF8.GetBytes("stream-data");
-	await writer.WriteAsync(payload, 0, payload.Length);
-	await writer.FlushAsync();
-}
-
-using var reader = op.OpenReadStream("stream.txt");
-using var ms = new MemoryStream();
-reader.CopyTo(ms);
+// The same verbs work on every service.
+op.Write("hello.txt", Encoding.UTF8.GetBytes("Hello, World!"));
+var bytes = op.Read("hello.txt");
+Console.WriteLine(Encoding.UTF8.GetString(bytes));
+Console.WriteLine(op.Stat("hello.txt").ContentLength);
 ```
 
-Notes:
-
-- Dispose write streams to flush/close native resources deterministically.
-- `ReadAsync` / `WriteAsync` on these streams are synchronous wrappers that still honor cancellation checks before execution.
-
-## Presign
-
-Generate presigned HTTP requests with expiration:
+To use a real backend, change the scheme and pass its configuration — the
+operations stay identical:
 
 ```csharp
-using OpenDAL;
-
 using var op = new Operator("s3", new Dictionary<string, string>
 {
-	["bucket"] = "my-bucket",
-	["region"] = "us-east-1",
-	["access_key_id"] = "<ak>",
-	["secret_access_key"] = "<sk>",
+    ["bucket"] = "your_bucket",
+    ["region"] = "your_region",
 });
-
-var request = await op.PresignReadAsync("data/file.txt", TimeSpan.FromMinutes(10));
-
-Console.WriteLine(request.Method);
-Console.WriteLine(request.Uri);
-foreach (var header in request.Headers)
-{
-	Console.WriteLine($"{header.Key}: {header.Value}");
-}
 ```
 
-Available APIs:
-
-- `PresignReadAsync`
-- `PresignWriteAsync`
-- `PresignStatAsync`
-- `PresignDeleteAsync`
-
-## Layers
-
-Apply middleware-like behavior with `WithLayer`:
-
-```csharp
-using OpenDAL;
-using OpenDAL.Layer;
-
-using var baseOp = new Operator("memory");
-
-using var op = baseOp
-	.WithLayer(new ConcurrentLimitLayer(64))
-	.WithLayer(new RetryLayer
-	{
-		MaxTimes = 5,
-		MinDelay = TimeSpan.FromMilliseconds(100),
-		MaxDelay = TimeSpan.FromSeconds(5),
-		Factor = 2f,
-		Jitter = true,
-	})
-	.WithLayer(new TimeoutLayer
-	{
-		Timeout = TimeSpan.FromSeconds(30),
-		IoTimeout = TimeSpan.FromSeconds(5),
-	});
-```
+OpenDAL also has a first-class async API — every operation has an `…Async`
+counterpart. See
+[Getting started](https://opendal.apache.org/docs/bindings/dotnet/getting-started)
+and [Connecting to your storage](https://opendal.apache.org/docs/bindings/dotnet/connecting)
+for the full guide.
 
-## Error Handling and Capability Checks
+## Contributing
 
-Most failures are surfaced as `OpenDALException` with a typed `Code` (`ErrorCode`).
+To build and test the binding from source:
 
-```csharp
-try
-{
-	await op.ReadAsync("missing.txt");
-}
-catch (OpenDALException ex) when (ex.Code == ErrorCode.NotFound)
-{
-	// Handle missing object
-}
-```
-
-Some features depend on backend capability. Check `op.Info.Capability` before using optional options/features:
-
-```csharp
-var cap = op.Info.Capability;
-if (cap.PresignRead)
-{
-	var req = await op.PresignReadAsync("a.txt", TimeSpan.FromMinutes(5));
-}
+```bash
+cargo build
+dotnet build
+dotnet test
 ```
 
-## Path Conventions
-
-- Use backend-native object keys (for example, `a/b/c.txt`).
-- For directory-like operations (`CreateDir`, directory `Stat`, `List` roots), prefer trailing slash paths such as `logs/`.
+For a complete guide on building, testing, and contributing, see the project's
+[CONTRIBUTING](https://github.com/apache/opendal/blob/main/CONTRIBUTING.md) guide.
 
 ## License and Trademarks
 
diff --git a/bindings/go/README.md b/bindings/go/CONTRIBUTING.md
similarity index 82%
copy from bindings/go/README.md
copy to bindings/go/CONTRIBUTING.md
index ecdcb2f86..4be22538b 100644
--- a/bindings/go/README.md
+++ b/bindings/go/CONTRIBUTING.md
@@ -1,92 +1,6 @@
-# Apache OpenDAL™ Go Binding
+# Contributing to the Apache OpenDAL™ Go Binding
 
-[![](https://img.shields.io/badge/status-released-blue)](https://pkg.go.dev/github.com/apache/opendal/bindings/go)
-[![Go Reference](https://pkg.go.dev/badge/github.com/apache/opendal/bindings/go.svg)](https://pkg.go.dev/github.com/apache/opendal/bindings/go)
-
-opendal-go is a **Native** support Go binding without CGO enabled and is built on top of opendal-c.
-
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
-
-```bash
-go get github.com/apache/opendal/bindings/go@latest
-```
-
-opendal-go requires **libffi** to be installed.
-
-## Basic Usage
-
-```go
-package main
-
-import (
-	"fmt"
-	"github.com/apache/opendal-go-services/memory"
-	opendal "github.com/apache/opendal/bindings/go"
-)
-
-func main() {
-	// Initialize a new in-memory operator
-	op, err := opendal.NewOperator(memory.Scheme, opendal.OperatorOptions{})
-	if err != nil {
-		panic(err)
-	}
-	defer op.Close()
-
-	// Write data to a file named "test"
-	err = op.Write("test", []byte("Hello opendal go binding!"))
-	if err != nil {
-		panic(err)
-	}
-
-	// Read data from the file "test"
-	data, err := op.Read("test")
-	if err != nil {
-		panic(err)
-	}
-	fmt.Printf("Read content: %s\n", data)
-
-	// List all entries under the root directory "/"
-	lister, err := op.List("/")
-	if err != nil {
-		panic(err)
-	}
-	defer lister.Close()
-
-	// Iterate through all entries
-	for lister.Next() {
-		entry := lister.Entry()
-
-		// Get entry name (not used in this example)
-		_ = entry.Name()
-
-		// Get metadata for the current entry
-		meta, _ := op.Stat(entry.Path())
-
-		// Print file size
-		fmt.Printf("Size: %d bytes\n", meta.ContentLength())
-
-		// Print last modified time
-		fmt.Printf("Last modified: %s\n", meta.LastModified())
-
-		// Check if the entry is a directory or a file
-		fmt.Printf("Is directory: %v, Is file: %v\n", meta.IsDir(), meta.IsFile())
-	}
-
-	// Check for any errors that occurred during iteration
-	if err := lister.Error(); err != nil {
-		panic(err)
-	}
-
-	// Copy a file
-	op.Copy("test", "test_copy")
-
-	// Rename a file
-	op.Rename("test", "test_rename")
-
-	// Delete a file
-	op.Delete("test_rename")
-}
-```
+This document covers building and testing the Go binding from source. For user-facing usage, see the [README](README.md) and the [user guide](https://opendal.apache.org/docs/bindings/go).
 
 ## Run Tests
 
@@ -287,31 +201,6 @@ geomean         343.6        12.40       -96.39%
 ```
 </details>
 
-## Capabilities
-
-- [x] OperatorInfo
-- [x] Stat
-    - [x] Metadata
-- [x] IsExist
-- [x] Read
-    - [x] Read
-    - [x] Reader -- implement as `io.ReadSeekCloser`
-- [x] Write
-    - [x] Write
-    - [x] Writer -- implement as `io.WriteCloser`
-- [x] Delete
-- [x] CreateDir
-- [x] Lister
-    - [x] Entry
-    - [x] Metadata
-- [x] Copy
-- [x] Rename
-- [x] Presign
-    - [x] PresignRead
-    - [x] PresignWrite
-    - [x] PresignStat
-    - [x] PresignDelete
-
 ## Development
 
 The guide is based on Linux and Windows. For other platforms, please adjust the commands accordingly.
@@ -368,9 +257,3 @@ make bench
 ```
 
 </details>
-
-## License and Trademarks
-
-Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
-
-Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation.
diff --git a/bindings/go/README.md b/bindings/go/README.md
index ecdcb2f86..e35b33745 100644
--- a/bindings/go/README.md
+++ b/bindings/go/README.md
@@ -3,371 +3,67 @@
 [![](https://img.shields.io/badge/status-released-blue)](https://pkg.go.dev/github.com/apache/opendal/bindings/go)
 [![Go Reference](https://pkg.go.dev/badge/github.com/apache/opendal/bindings/go.svg)](https://pkg.go.dev/github.com/apache/opendal/bindings/go)
 
-opendal-go is a **Native** support Go binding without CGO enabled and is built on top of opendal-c.
+opendal-go is a **Native** Go binding for Apache OpenDAL. It is built on top of `opendal-c` without CGO enabled, using [purego](https://github.com/ebitengine/purego) and libffi.
 
 > **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
 
+## Useful Links
+
+- [User guide](https://opendal.apache.org/docs/bindings/go)
+- [API reference](https://pkg.go.dev/github.com/apache/opendal/bindings/go)
+- [Services & configuration](https://opendal.apache.org/services)
+
+## Installation
+
+opendal-go requires **libffi** to be installed.
+
 ```bash
 go get github.com/apache/opendal/bindings/go@latest
 ```
 
-opendal-go requires **libffi** to be installed.
+Each service is a separate companion module. Add the schemes you use, for example the in-memory service:
+
+```bash
+go get github.com/apache/opendal-go-services/memory
+```
 
-## Basic Usage
+## Quickstart
 
 ```go
 package main
 
 import (
 	"fmt"
+	"log"
+
 	"github.com/apache/opendal-go-services/memory"
 	opendal "github.com/apache/opendal/bindings/go"
 )
 
 func main() {
-	// Initialize a new in-memory operator
 	op, err := opendal.NewOperator(memory.Scheme, opendal.OperatorOptions{})
 	if err != nil {
-		panic(err)
+		log.Fatal(err)
 	}
 	defer op.Close()
 
-	// Write data to a file named "test"
-	err = op.Write("test", []byte("Hello opendal go binding!"))
-	if err != nil {
-		panic(err)
+	if err := op.Write("hello.txt", []byte("Hello, OpenDAL!")); err != nil {
+		log.Fatal(err)
 	}
 
-	// Read data from the file "test"
-	data, err := op.Read("test")
+	data, err := op.Read("hello.txt")
 	if err != nil {
-		panic(err)
+		log.Fatal(err)
 	}
 	fmt.Printf("Read content: %s\n", data)
-
-	// List all entries under the root directory "/"
-	lister, err := op.List("/")
-	if err != nil {
-		panic(err)
-	}
-	defer lister.Close()
-
-	// Iterate through all entries
-	for lister.Next() {
-		entry := lister.Entry()
-
-		// Get entry name (not used in this example)
-		_ = entry.Name()
-
-		// Get metadata for the current entry
-		meta, _ := op.Stat(entry.Path())
-
-		// Print file size
-		fmt.Printf("Size: %d bytes\n", meta.ContentLength())
-
-		// Print last modified time
-		fmt.Printf("Last modified: %s\n", meta.LastModified())
-
-		// Check if the entry is a directory or a file
-		fmt.Printf("Is directory: %v, Is file: %v\n", meta.IsDir(), meta.IsFile())
-	}
-
-	// Check for any errors that occurred during iteration
-	if err := lister.Error(); err != nil {
-		panic(err)
-	}
-
-	// Copy a file
-	op.Copy("test", "test_copy")
-
-	// Rename a file
-	op.Rename("test", "test_rename")
-
-	// Delete a file
-	op.Delete("test_rename")
 }
 ```
 
-## Run Tests
-
-### Behavior Tests
-
-```bash
-cd tests/behavior_tests
-# Test a specific backend
-export OPENDAL_TEST=memory
-# Run all tests
-CGO_ENABLE=0 go test -v -run TestBehavior
-# Run specific test
-CGO_ENABLE=0 go test -v -run TestBehavior/Write
-# Run synchronously
-CGO_ENABLE=0 GOMAXPROCS=1 go test -v -run TestBehavior
-```
-
-### Benchmark
-
-```bash
-cd tests/behavior_tests
-# Benchmark a specific backend
-export OPENDAL_TEST=memory
-
-go test -bench .
-```
+For the full guide — connecting to real backends, streaming, listing, presigning, retries, and error handling — see the [user guide](https://opendal.apache.org/docs/bindings/go).
 
-<details>
-  <summary>
-  A benchmark between OpenDAL Go binding and aws-sdk-go on minio S3 compatible storage
-  </summary>
-
-```
-goos: linux
-goarch: amd64
-pkg: opendal_test
-cpu: AMD EPYC 7763 64-Core Processor                
-BenchmarkWrite
-BenchmarkWrite/4KiB/OpenDAL
-BenchmarkWrite/4KiB/OpenDAL-4         	     405	   3430652 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4KiB/OpenDAL-4         	     313	   3781825 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4KiB/OpenDAL-4         	     346	   3354802 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4KiB/OpenDAL-4         	     397	   3374467 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4KiB/OpenDAL-4         	     364	   6171386 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4KiB/OpenDAL-4         	     388	   3789795 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4KiB/AWS_S3
-BenchmarkWrite/4KiB/AWS_S3-4          	     384	   3716060 ns/op	   62929 B/op	     312 allocs/op
-BenchmarkWrite/4KiB/AWS_S3-4          	     402	   3145797 ns/op	   62077 B/op	     312 allocs/op
-BenchmarkWrite/4KiB/AWS_S3-4          	     372	   3050911 ns/op	   61902 B/op	     312 allocs/op
-BenchmarkWrite/4KiB/AWS_S3-4          	     400	   3081028 ns/op	   61556 B/op	     312 allocs/op
-BenchmarkWrite/4KiB/AWS_S3-4          	     342	   3111741 ns/op	   61970 B/op	     312 allocs/op
-BenchmarkWrite/4KiB/AWS_S3-4          	     464	   2933844 ns/op	   61617 B/op	     312 allocs/op
-BenchmarkWrite/256KiB/OpenDAL
-BenchmarkWrite/256KiB/OpenDAL-4       	     228	   6253966 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/256KiB/OpenDAL-4       	     190	   5859882 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/256KiB/OpenDAL-4       	     216	   6008253 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/256KiB/OpenDAL-4       	     200	   5958440 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/256KiB/OpenDAL-4       	     193	   5658798 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/256KiB/OpenDAL-4       	     210	   6250594 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/256KiB/AWS_S3
-BenchmarkWrite/256KiB/AWS_S3-4        	     208	   5665223 ns/op	   90323 B/op	     312 allocs/op
-BenchmarkWrite/256KiB/AWS_S3-4        	     196	   5368602 ns/op	   88719 B/op	     312 allocs/op
-BenchmarkWrite/256KiB/AWS_S3-4        	     218	   6108315 ns/op	   90784 B/op	     312 allocs/op
-BenchmarkWrite/256KiB/AWS_S3-4        	     217	   5828966 ns/op	   90379 B/op	     312 allocs/op
-BenchmarkWrite/256KiB/AWS_S3-4        	     210	   6447429 ns/op	   90981 B/op	     312 allocs/op
-BenchmarkWrite/256KiB/AWS_S3-4        	     177	   6323915 ns/op	   91548 B/op	     313 allocs/op
-BenchmarkWrite/4MiB/OpenDAL
-BenchmarkWrite/4MiB/OpenDAL-4         	      62	  19049500 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4MiB/OpenDAL-4         	      54	  19271521 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4MiB/OpenDAL-4         	      69	  17849900 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4MiB/OpenDAL-4         	      73	  18117693 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4MiB/OpenDAL-4         	      74	  18249816 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4MiB/OpenDAL-4         	      62	  19556730 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/4MiB/AWS_S3
-BenchmarkWrite/4MiB/AWS_S3-4          	      43	  29514084 ns/op	   92211 B/op	     320 allocs/op
-BenchmarkWrite/4MiB/AWS_S3-4          	      39	  29297735 ns/op	   92514 B/op	     320 allocs/op
-BenchmarkWrite/4MiB/AWS_S3-4          	      42	  28956593 ns/op	   92282 B/op	     320 allocs/op
-BenchmarkWrite/4MiB/AWS_S3-4          	      42	  29218821 ns/op	   92282 B/op	     320 allocs/op
-BenchmarkWrite/4MiB/AWS_S3-4          	      40	  28988214 ns/op	   91391 B/op	     320 allocs/op
-BenchmarkWrite/4MiB/AWS_S3-4          	      43	  28668319 ns/op	   91242 B/op	     320 allocs/op
-BenchmarkWrite/16MiB/OpenDAL
-BenchmarkWrite/16MiB/OpenDAL-4        	      21	  53528117 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/16MiB/OpenDAL-4        	      21	  55328986 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/16MiB/OpenDAL-4        	      21	  54221620 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/16MiB/OpenDAL-4        	      20	  54044030 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/16MiB/OpenDAL-4        	      21	  53692610 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/16MiB/OpenDAL-4        	      21	  53288370 ns/op	     384 B/op	      10 allocs/op
-BenchmarkWrite/16MiB/AWS_S3
-BenchmarkWrite/16MiB/AWS_S3-4         	      12	  99294840 ns/op	   99850 B/op	     322 allocs/op
-BenchmarkWrite/16MiB/AWS_S3-4         	      12	  97405067 ns/op	   99850 B/op	     322 allocs/op
-BenchmarkWrite/16MiB/AWS_S3-4         	      12	  97906212 ns/op	   99848 B/op	     322 allocs/op
-BenchmarkWrite/16MiB/AWS_S3-4         	      12	  98766864 ns/op	   96378 B/op	     322 allocs/op
-BenchmarkWrite/16MiB/AWS_S3-4         	      12	  97967605 ns/op	   99850 B/op	     322 allocs/op
-BenchmarkWrite/16MiB/AWS_S3-4         	      12	  97842268 ns/op	   96376 B/op	     322 allocs/op
-BenchmarkRead
-BenchmarkRead/4KiB/OpenDAL
-BenchmarkRead/4KiB/OpenDAL-4          	     972	   1217197 ns/op	    4760 B/op	      15 allocs/op
-BenchmarkRead/4KiB/OpenDAL-4          	     985	   1226752 ns/op	    4760 B/op	      15 allocs/op
-BenchmarkRead/4KiB/OpenDAL-4          	     982	   1216827 ns/op	    4760 B/op	      15 allocs/op
-BenchmarkRead/4KiB/OpenDAL-4          	     987	   1227682 ns/op	    4760 B/op	      15 allocs/op
-BenchmarkRead/4KiB/OpenDAL-4          	     987	   1214420 ns/op	    4760 B/op	      15 allocs/op
-BenchmarkRead/4KiB/OpenDAL-4          	     987	   1215074 ns/op	    4760 B/op	      15 allocs/op
-BenchmarkRead/4KiB/AWS_S3
-BenchmarkRead/4KiB/AWS_S3-4           	    1594	    759864 ns/op	   34988 B/op	     344 allocs/op
-BenchmarkRead/4KiB/AWS_S3-4           	    1605	    751628 ns/op	   34990 B/op	     344 allocs/op
-BenchmarkRead/4KiB/AWS_S3-4           	    1620	    744649 ns/op	   34991 B/op	     344 allocs/op
-BenchmarkRead/4KiB/AWS_S3-4           	    1623	    752885 ns/op	   34994 B/op	     344 allocs/op
-BenchmarkRead/4KiB/AWS_S3-4           	    1629	    742307 ns/op	   34991 B/op	     344 allocs/op
-BenchmarkRead/4KiB/AWS_S3-4           	    1622	    750455 ns/op	   34992 B/op	     344 allocs/op
-BenchmarkRead/256KiB/OpenDAL
-BenchmarkRead/256KiB/OpenDAL-4        	     741	   1612259 ns/op	  262812 B/op	      15 allocs/op
-BenchmarkRead/256KiB/OpenDAL-4        	     727	   1633753 ns/op	  262812 B/op	      15 allocs/op
-BenchmarkRead/256KiB/OpenDAL-4        	     751	   1619770 ns/op	  262812 B/op	      15 allocs/op
-BenchmarkRead/256KiB/OpenDAL-4        	     756	   1619435 ns/op	  262812 B/op	      15 allocs/op
-BenchmarkRead/256KiB/OpenDAL-4        	     750	   1629211 ns/op	  262812 B/op	      15 allocs/op
-BenchmarkRead/256KiB/OpenDAL-4        	     757	   1611646 ns/op	  262812 B/op	      15 allocs/op
-BenchmarkRead/256KiB/AWS_S3
-BenchmarkRead/256KiB/AWS_S3-4         	     736	   1655133 ns/op	 1209847 B/op	     365 allocs/op
-BenchmarkRead/256KiB/AWS_S3-4         	     735	   1673714 ns/op	 1209874 B/op	     365 allocs/op
-BenchmarkRead/256KiB/AWS_S3-4         	     735	   1653903 ns/op	 1209852 B/op	     365 allocs/op
-BenchmarkRead/256KiB/AWS_S3-4         	     728	   1643222 ns/op	 1209855 B/op	     365 allocs/op
-BenchmarkRead/256KiB/AWS_S3-4         	     726	   1674202 ns/op	 1209842 B/op	     365 allocs/op
-BenchmarkRead/256KiB/AWS_S3-4         	     717	   1656092 ns/op	 1209876 B/op	     365 allocs/op
-BenchmarkRead/4MiB/OpenDAL
-BenchmarkRead/4MiB/OpenDAL-4          	     250	   4933426 ns/op	 4194991 B/op	      15 allocs/op
-BenchmarkRead/4MiB/OpenDAL-4          	     255	   4874283 ns/op	 4194991 B/op	      15 allocs/op
-BenchmarkRead/4MiB/OpenDAL-4          	     252	   4769956 ns/op	 4194992 B/op	      16 allocs/op
-BenchmarkRead/4MiB/OpenDAL-4          	     244	   4800674 ns/op	 4194992 B/op	      16 allocs/op
-BenchmarkRead/4MiB/OpenDAL-4          	     242	   4826974 ns/op	 4194992 B/op	      15 allocs/op
-BenchmarkRead/4MiB/OpenDAL-4          	     254	   4882666 ns/op	 4194996 B/op	      16 allocs/op
-BenchmarkRead/4MiB/AWS_S3
-BenchmarkRead/4MiB/AWS_S3-4           	     122	  10379507 ns/op	21133764 B/op	     388 allocs/op
-BenchmarkRead/4MiB/AWS_S3-4           	     100	  10838399 ns/op	21133778 B/op	     389 allocs/op
-BenchmarkRead/4MiB/AWS_S3-4           	      92	  11831700 ns/op	21133738 B/op	     388 allocs/op
-BenchmarkRead/4MiB/AWS_S3-4           	     100	  10002235 ns/op	21133764 B/op	     388 allocs/op
-BenchmarkRead/4MiB/AWS_S3-4           	     100	  10416467 ns/op	21133731 B/op	     388 allocs/op
-BenchmarkRead/4MiB/AWS_S3-4           	     100	  10452328 ns/op	21133719 B/op	     388 allocs/op
-BenchmarkRead/16MiB/OpenDAL
-BenchmarkRead/16MiB/OpenDAL-4         	      81	  14654347 ns/op	16777903 B/op	      15 allocs/op
-BenchmarkRead/16MiB/OpenDAL-4         	      78	  14977822 ns/op	16777908 B/op	      16 allocs/op
-BenchmarkRead/16MiB/OpenDAL-4         	      81	  14833921 ns/op	16777906 B/op	      16 allocs/op
-BenchmarkRead/16MiB/OpenDAL-4         	      76	  14663855 ns/op	16777906 B/op	      16 allocs/op
-BenchmarkRead/16MiB/OpenDAL-4         	      75	  14545209 ns/op	16777906 B/op	      16 allocs/op
-BenchmarkRead/16MiB/OpenDAL-4         	      80	  14585250 ns/op	16777905 B/op	      16 allocs/op
-BenchmarkRead/16MiB/AWS_S3
-BenchmarkRead/16MiB/AWS_S3-4          	      52	  24826707 ns/op	102660545 B/op	     398 allocs/op
-BenchmarkRead/16MiB/AWS_S3-4          	      46	  24588321 ns/op	102660529 B/op	     397 allocs/op
-BenchmarkRead/16MiB/AWS_S3-4          	      44	  26526367 ns/op	102660527 B/op	     397 allocs/op
-BenchmarkRead/16MiB/AWS_S3-4          	      44	  26996660 ns/op	102660522 B/op	     397 allocs/op
-BenchmarkRead/16MiB/AWS_S3-4          	      42	  26319939 ns/op	102660552 B/op	     398 allocs/op
-BenchmarkRead/16MiB/AWS_S3-4          	      45	  25286454 ns/op	102660542 B/op	     398 allocs/op
-PASS
-ok  	opendal_test	147.053s
-```
-
-**Diff** with [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat)
-```
-benchstat aws.txt opendal.txt
-goos: linux
-goarch: amd64
-pkg: opendal_test
-cpu: AMD EPYC 7763 64-Core Processor                
-             │    aws.txt    │             opendal.txt              │
-             │    sec/op     │    sec/op      vs base               │
-Write/4KiB      3.096m ± 20%    3.606m ± 71%  +16.47% (p=0.015 n=6)
-Write/256KiB    5.969m ± 10%    5.983m ±  5%        ~ (p=1.000 n=6)
-Write/4MiB      29.10m ±  1%    18.65m ±  5%  -35.92% (p=0.002 n=6)
-Write/16MiB     97.94m ±  1%    53.87m ±  3%  -45.00% (p=0.002 n=6)
-Read/4KiB       751.0µ ±  1%   1217.0µ ±  1%  +62.04% (p=0.002 n=6)
-Read/256KiB     1.656m ±  1%    1.620m ±  1%   -2.18% (p=0.002 n=6)
-Read/4MiB      10.434m ± 13%    4.851m ±  2%  -53.51% (p=0.002 n=6)
-Read/16MiB      25.80m ±  5%    14.66m ±  2%  -43.19% (p=0.002 n=6)
-geomean         8.050m          6.461m        -19.73%
-
-             │    aws.txt    │             opendal.txt             │
-             │     B/op      │     B/op      vs base               │
-Write/4KiB      61936.0 ± 2%     384.0 ± 0%  -99.38% (p=0.002 n=6)
-Write/256KiB    90581.5 ± 2%     384.0 ± 0%  -99.58% (p=0.002 n=6)
-Write/4MiB      92246.5 ± 1%     384.0 ± 0%  -99.58% (p=0.002 n=6)
-Write/16MiB     99849.0 ± 3%     384.0 ± 0%  -99.62% (p=0.002 n=6)
-Read/4KiB      34.171Ki ± 0%   4.648Ki ± 0%  -86.40% (p=0.002 n=6)
-Read/256KiB    1181.5Ki ± 0%   256.7Ki ± 0%  -78.28% (p=0.002 n=6)
-Read/4MiB      20.155Mi ± 0%   4.001Mi ± 0%  -80.15% (p=0.002 n=6)
-Read/16MiB      97.90Mi ± 0%   16.00Mi ± 0%  -83.66% (p=0.002 n=6)
-geomean         500.3Ki        14.12Ki       -97.18%
-
-             │   aws.txt   │            opendal.txt            │
-             │  allocs/op  │ allocs/op   vs base               │
-Write/4KiB     312.00 ± 0%   10.00 ± 0%  -96.79% (p=0.002 n=6)
-Write/256KiB   312.00 ± 0%   10.00 ± 0%  -96.79% (p=0.002 n=6)
-Write/4MiB     320.00 ± 0%   10.00 ± 0%  -96.88% (p=0.002 n=6)
-Write/16MiB    322.00 ± 0%   10.00 ± 0%  -96.89% (p=0.002 n=6)
-Read/4KiB      344.00 ± 0%   15.00 ± 0%  -95.64% (p=0.002 n=6)
-Read/256KiB    365.00 ± 0%   15.00 ± 0%  -95.89% (p=0.002 n=6)
-Read/4MiB      388.00 ± 0%   15.50 ± 3%  -96.01% (p=0.002 n=6)
-Read/16MiB     397.50 ± 0%   16.00 ± 6%  -95.97% (p=0.002 n=6)
-geomean         343.6        12.40       -96.39%
-```
-</details>
-
-## Capabilities
-
-- [x] OperatorInfo
-- [x] Stat
-    - [x] Metadata
-- [x] IsExist
-- [x] Read
-    - [x] Read
-    - [x] Reader -- implement as `io.ReadSeekCloser`
-- [x] Write
-    - [x] Write
-    - [x] Writer -- implement as `io.WriteCloser`
-- [x] Delete
-- [x] CreateDir
-- [x] Lister
-    - [x] Entry
-    - [x] Metadata
-- [x] Copy
-- [x] Rename
-- [x] Presign
-    - [x] PresignRead
-    - [x] PresignWrite
-    - [x] PresignStat
-    - [x] PresignDelete
-
-## Development
-
-The guide is based on Linux and Windows. For other platforms, please adjust the commands accordingly.
-
-To develop the Go binding, you need to have the following dependencies installed:
-
-- zstd
-- Rust toolchain
-- Go
-- (Required for Windows) libffi-8.dll in the root of the workspace directory
-
-We use `go workspace` to manage and build the dependencies. To set up the workspace, run the following commands:
-
-<details>
-  <summary>
-  For Linux, macOS and Windows (MSVC)
-  </summary>
-
-<br/>
-
-```bash
-mkdir opendal_workspace
-cd opendal_workspace
-git clone --depth 1 git@github.com:apache/opendal.git
-git clone --depth 1 git@github.com:apache/opendal-go-services.git
-
-go work init
-go work use ./opendal/bindings/go
-go work use ./opendal/bindings/go/tests/behavior_tests
-
-cp opendal/bindings/go/Makefile .
-
-cd -
-```
-
-To build and run tests, run the following commands:
-
-```bash
-cd opendal_workspace
-
-# specify the backend to test
-export OPENDAL_TEST=fs
-export OPENDAL_FS_ROOT=/tmp/opendal
-
-make tests
-
-cd -
-```
-
-To run the benchmarks, you can use the following command:
-
-```bash
-make bench
-```
+## Contributing
 
-</details>
+This document is for users of the binding. If you want to build the binding from source, run the tests, or run benchmarks, see [CONTRIBUTING.md](CONTRIBUTING.md). Contributions are welcome; please also read the project-wide [contributing guide](https://github.com/apache/opendal/blob/main/CONTRIBUTING.md).
 
 ## License and Trademarks
 
diff --git a/bindings/haskell/README.md b/bindings/haskell/README.md
index a2db32f67..58a6ab93d 100644
--- a/bindings/haskell/README.md
+++ b/bindings/haskell/README.md
@@ -1,63 +1,66 @@
 # Apache OpenDAL™ Haskell Binding (WIP)
 
-[![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/bindings/haskell)
+[![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/docs/bindings/haskell)
 
-![](https://github.com/apache/opendal/assets/5351546/87bbf6e5-f19e-449a-b368-3e283016c887)
+> **Note**: This binding has its own independent version number, which may differ from the Rust core version.
 
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
+Access S3, GCS, Azure Blob, the local filesystem, and 50+ more services from
+Haskell through a single, consistent API backed by the OpenDAL Rust core.
 
-## Example
+## Useful Links
 
-Basic usage
-
-```haskell
-import OpenDAL
-
-main :: IO ()
-main = do
-  Right op <- newOperator "memory"
-  runOp op operation
-  where
-    operation = do
-      writeOp op "key1" "value1"
-      writeOp op "key2" "value2"
-      value1 <- readOp op "key1"
-      value2 <- readOp op "key2"
-```
-
-Use logger
-
-```haskell
-import OpenDAL
-import Colog (simpleMessageAction)
-
-main :: IO ()
-main = do
-  Right op <- newOperator "memory" {ocLogAction = Just simpleMessageAction}
-  return ()
-```
+- **User guide**: https://opendal.apache.org/docs/bindings/haskell
+- **Services & configuration**: https://opendal.apache.org/services
+- **Source**: [`bindings/haskell/`](https://github.com/apache/opendal/tree/main/bindings/haskell)
 
 ## Build
 
+Requires a working Rust toolchain (`cargo`) in addition to GHC and `cabal`.
+The custom build step compiles the Rust FFI library automatically.
+
 ```bash
 cabal build
 ```
 
-## Test
+Run the test suite:
 
 ```bash
 cabal test
 ```
 
-## Doc
-
-To generate the documentation:
+Generate Haddock documentation:
 
 ```bash
 cabal haddock
+# cabal haddock --open   (cabal >= 3.8)
+```
+
+## Quickstart
+
+```haskell
+import OpenDAL
+
+main :: IO ()
+main = do
+  Right op <- newOperator "memory"
+  result <- runOp op $ do
+    writeOp "hello.txt" "Hello, World!"
+    content <- readOp "hello.txt"
+    deleteOp "hello.txt"
+    return content
+  case result of
+    Left err    -> putStrLn $ "error: " ++ show (errorCode err)
+    Right bytes -> print bytes
 ```
 
-If your `cabal` version is greater than `3.8`, you can use `cabal haddock --open` to open the documentation in your browser. Otherwise, you can visit the documentation from `dist-newstyle/build/$ARCH/ghc-$VERSION/opendal-$VERSION/doc/html/opendal/index.html`.
+See the [user guide](https://opendal.apache.org/docs/bindings/haskell) for a
+full walkthrough, including real backends, error handling, streaming writes, and
+directory listing.
+
+## Contributing
+
+See [`CONTRIBUTING.md`](CONTRIBUTING.md) for how to build, test, and submit
+changes.
 
 ## License and Trademarks
 
diff --git a/bindings/java/CONTRIBUTING.md b/bindings/java/CONTRIBUTING.md
new file mode 100644
index 000000000..a0b9e2e97
--- /dev/null
+++ b/bindings/java/CONTRIBUTING.md
@@ -0,0 +1,71 @@
+# Contributing to Apache OpenDAL™ Java Bindings
+
+This document covers building, testing, and styling the Java binding. For usage,
+see the [user guide](https://opendal.apache.org/docs/bindings/java).
+
+## Build
+
+This project provides OpenDAL Java bindings with artifact name `opendal`. It depends on JDK 8 or later.
+
+You can use Maven to build both the Rust dynamic lib and JAR files with one command:
+
+```shell
+./mvnw clean package -DskipTests=true
+```
+
+## Run tests
+
+Currently, all tests are written in Java.
+
+You can run the base tests with the following command:
+
+```shell
+./mvnw clean verify
+```
+
+## Code style
+
+This project uses [spotless](https://github.com/diffplug/spotless) for code formatting so that all developers share a consistent code style without bikeshedding on it.
+
+You can apply the code style with the following command:
+
+```shell
+./mvnw spotless:apply
+```
+
+## Run behavior tests
+
+Services behavior tests read necessary configs from env vars or the `.env` file.
+
+You can copy [.env.example](/.env.example) to `${project.rootdir}/.env` and change the values on need, or directly set env vars with `export KEY=VALUE`.
+
+Take `fs` for example, we need to enable bench on `fs` on `/tmp/`:
+
+```properties
+OPENDAL_TEST=fs
+OPENDAL_FS_ROOT=/tmp/
+```
+
+You can run service behavior tests of enabled services with the following command:
+
+```shell
+./mvnw test -Dtest="behavior.*Test"
+```
+
+## Classified library
+
+Note that the dependency without classifier ships all classes and resources except the "opendal_java" shared library. And those with classifier bundle only the shared library.
+
+For downstream usage, it's recommended:
+
+* Depend on the one without classifier to write code;
+* Depend on the classified ones with "test" for testing.
+
+To load the shared library correctly, you can choose one of the following approaches:
+
+* Append the classified JARs to the classpath at the runtime;
+* Depend on the classified JARs and build a fat JAR (You may need to depend on all the provided classified JARs for running on multiple platforms);
+* Build your own "opendal_java" shared library and specify "-Djava.library.path" to the folder containing that shared library.
+
+The musl native library is dynamically linked against musl libc and the GCC runtime. On Alpine Linux, install `libgcc` before loading the native library.
+</content>
diff --git a/bindings/java/README.md b/bindings/java/README.md
index 6d4d1fe29..1aedab665 100644
--- a/bindings/java/README.md
+++ b/bindings/java/README.md
@@ -4,44 +4,26 @@
 [![Maven Central](https://img.shields.io/maven-central/v/org.apache.opendal/opendal.svg?logo=Apache+Maven&logoColor=blue)](https://central.sonatype.com/search?q=opendal&smo=true)
 [![Website](https://img.shields.io/badge/opendal-OpenDAL_Website-red?logo=Apache&logoColor=red)](https://opendal.apache.org/docs/java/)
 
-![](https://github.com/apache/opendal/assets/5351546/87bbf6e5-f19e-449a-b368-3e283016c887)
+A native Java binding for Apache OpenDAL™: access S3, GCS, Azure Blob, HDFS, the
+local filesystem, and many more services through one API.
 
 > **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
 
 ## Useful Links
 
-- [Documentation](https://opendal.apache.org/docs/java/)
-- [Upgrade Guide](./upgrade.md)
+- **User guide**: [opendal.apache.org/docs/bindings/java](https://opendal.apache.org/docs/bindings/java) — install, connect, common tasks, and going to production.
+- **API reference**: [opendal.apache.org/docs/java](https://opendal.apache.org/docs/java/)
+- **Services & configuration**: [opendal.apache.org/services](https://opendal.apache.org/services)
+- **Upgrade guide**: [`upgrade.md`](./upgrade.md)
 
-## Example
+## Installation
 
-```java
-import java.util.HashMap;
-import java.util.Map;
-import org.apache.opendal.AsyncOperator;
-import org.apache.opendal.Operator;
-
-public class Main {
-  public static void main(String[] args) {
-    final Map<String, String> conf = new HashMap<>();
-    conf.put("root", "/tmp");
-
-    try (AsyncOperator op = AsyncOperator.of("fs", conf)) {
-      op.write("/path/to/data", "Hello world").join();
-      System.out.println(new String(op.read("/path/to/data").join()));
-    }
-  }
-}
-```
-
-## Getting Started
-
-This project is built upon the native OpenDAL lib. And it is released for multiple platforms that you can use a classifier to specify the platform you are building the application on.
+The binding ships a platform-specific native library, so you depend on the main
+artifact plus a classifier for your platform. An OS detector plugin fills in the
+classifier automatically.
 
 ### Maven
 
-Generally, you can first add the `os-maven-plugin` for automatically detect the classifier based on your platform:
-
 ```xml
 <build>
 <extensions>
@@ -52,11 +34,7 @@ Generally, you can first add the `os-maven-plugin` for automatically detect the
   </extension>
 </extensions>
 </build>
-```
-
-Then add the dependency to `opendal` as following:
 
-```xml
 <dependencies>
   <dependency>
     <groupId>org.apache.opendal</groupId>
@@ -72,102 +50,56 @@ Then add the dependency to `opendal` as following:
 </dependencies>
 ```
 
-On musl-based Linux distributions such as Alpine, use the musl classified artifact instead of the default Linux classifier:
-
-```xml
-<classifier>linux-x86_64-musl</classifier>
-```
-
-Use `linux-aarch_64-musl` for aarch64 Linux musl environments.
-
-The musl native library is dynamically linked against musl libc and the GCC runtime. On Alpine Linux, install `libgcc` before loading the native library.
-
 ### Gradle
 
-For Gradle, you can first add the `com.google.osdetector` for automatically detect the classifier based on your platform:
-
 ```groovy
 plugins {
     id "com.google.osdetector" version "1.7.3"
 }
-```
-
-Then add the dependency to `opendal as following:
 
-```groovy
 dependencies {
     implementation "org.apache.opendal:opendal:$opendalVersion"
     implementation "org.apache.opendal:opendal:$opendalVersion:$osdetector.classifier"
 }
 ```
 
-On musl-based Linux distributions such as Alpine, use `linux-x86_64-musl` or `linux-aarch_64-musl` as the classifier.
-
-The musl native library is dynamically linked against musl libc and the GCC runtime. On Alpine Linux, install `libgcc` before loading the native library.
-
-### Classified library
-
-Note that the dependency without classifier ships all classes and resources except the "opendal_java" shared library. And those with classifier bundle only the shared library.
-
-For downstream usage, it's recommended:
-
-* Depend on the one without classifier to write code;
-* Depend on the classified ones with "test" for testing.
-
-To load the shared library correctly, you can choose one of the following approaches:
-
-* Append the classified JARs to the classpath at the runtime;
-* Depend on the classified JARs and build a fat JAR (You may need to depend on all the provided classified JARs for running on multiple platforms);
-* Build your own "opendal_java" shared library and specify "-Djava.library.path" to the folder containing that shared library.
-
-## Build
-
-This project provides OpenDAL Java bindings with artifact name `opendal`. It depends on JDK 8 or later.
-
-You can use Maven to build both Rust dynamic lib and JAR files with one command now:
-
-```shell
-./mvnw clean package -DskipTests=true
-```
-
-## Run tests
-
-Currently, all tests are written in Java.
+On musl-based Linux distributions such as Alpine, use the `linux-x86_64-musl` or
+`linux-aarch_64-musl` classifier instead of the detected one.
 
-You can run the base tests with the following command:
+## Quickstart
 
-```shell
-./mvnw clean verify
-```
-
-## Code style
-
-This project uses [spotless](https://github.com/diffplug/spotless) for code formatting so that all developers share a consistent code style without bikeshedding on it.
+```java
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.opendal.AsyncOperator;
 
-You can apply the code style with the following command::
+public class Main {
+  public static void main(String[] args) {
+    final Map<String, String> conf = new HashMap<>();
+    conf.put("root", "/tmp");
 
-```shell
-./mvnw spotless:apply
+    try (AsyncOperator op = AsyncOperator.of("fs", conf)) {
+      op.write("/path/to/data", "Hello world").join();
+      System.out.println(new String(op.read("/path/to/data").join()));
+    }
+  }
+}
 ```
 
-## Run behavior tests
+Use the synchronous `Operator` for blocking calls, or `AsyncOperator` for
+`CompletableFuture`-based calls.
 
-Services behavior tests read necessary configs from env vars or the `.env` file.
+## Documentation
 
-You can copy [.env.example](/.env.example) to `${project.rootdir}/.env` and change the values on need, or directly set env vars with `export KEY=VALUE`.
+The full user guide — getting started, connecting to services, common tasks, and
+going to production — lives at
+[opendal.apache.org/docs/bindings/java](https://opendal.apache.org/docs/bindings/java).
 
-Take `fs` for example, we need to enable bench on `fs` on `/tmp/`:
+## Contributing
 
-```properties
-OPENDAL_TEST=fs
-OPENDAL_FS_ROOT=/tmp/
-```
-
-You can run service behavior tests of enabled with the following command:
-
-```shell
-./mvnw test -Dtest="behavior.*Test"
-```
+This project is built upon the native OpenDAL library and depends on JDK 8 or
+later. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to build the binding, run
+tests, and apply the code style.
 
 ## Used by
 
@@ -178,3 +110,4 @@ Check out the [users](./users.md) list for more details on who is using OpenDAL.
 Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
 
 Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation.
+</content>
diff --git a/bindings/lua/README.md b/bindings/lua/README.md
index 4ec017db1..78995488e 100644
--- a/bindings/lua/README.md
+++ b/bindings/lua/README.md
@@ -1,79 +1,76 @@
 # Apache OpenDAL™ Lua Binding (WIP)
 
-[![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/bindings/lua/)
+[![status: unreleased](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/docs/bindings/lua)
 
-![](https://github.com/apache/opendal/assets/5351546/87bbf6e5-f19e-449a-b368-3e283016c887)
+A Lua binding for [Apache OpenDAL](https://opendal.apache.org/): access the
+local filesystem, in-memory storage, S3, and 50+ services through one API.
 
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
+> **Note**: This binding has its own independent version number, which may
+> differ from the Rust core version.
 
-## Example
+## Useful Links
 
-```lua
-local opendal = require("opendal")
+- **User guide**: https://opendal.apache.org/docs/bindings/lua
+- **Services & configuration**: https://opendal.apache.org/docs/category/services/
+- **Source**: https://github.com/apache/opendal/tree/main/bindings/lua
 
-local op, err = opendal.operator.new("fs",{root="/tmp"})
-if err ~= nil then
-    print(err)
-    return
-end
-op:write("test.txt","hello world")
-print("read: ", op:read("test.txt"))
-```
+## Build and install
 
-## Lua version
+### Build from source
 
-You have to enable one of the features: lua54, lua53, lua52, lua51, luajit(52) or luau in `Cargo.toml`, according to the chosen Lua version. Default Lua version is 5.2.
+```bash
+# Currently only Lua 5.2 is supported (the lua52 Cargo feature).
+cd bindings/lua
+cargo build --package opendal-lua --release
 
-## Build from source
+# Copy the native library to your Lua shared-library directory.
+cp ../../target/release/libopendal_lua.so /usr/lib/lua/5.2/opendal.so
+```
 
-1. Build OpenDAL Lua Interface
+### Install with LuaRocks
 
 ```bash
-$ cd bindings/lua
-$ cargo build --package opendal-lua --release
+cd bindings/lua
+luarocks make
 ```
 
-2. Install opendal Lua library
+## Quickstart
 
-```bash
-# copy to lua share library directory
-# default lua5.2 share library directory is /usr/lib/lua/5.2
-$ cp ../../target/release/libopendal_lua.so /usr/lib/lua/5.2/opendal.so
-```
+```lua
+local opendal = require("opendal")
 
-## Install from LuaRocks
+-- Errors are raised as Lua errors; use pcall to catch them.
+local ok, op = pcall(opendal.operator.new, "fs", { root = "/tmp" })
+if not ok then
+    print(op)
+    return
+end
 
-```bash
-$ luarocks make
+op:write("test.txt", "hello world")
+print("read:", op:read("test.txt"))   -- "hello world"
 ```
 
-## Usage
+Run the bundled example:
 
 ```bash
-$ lua5.2 example/fs.lua
-read:   hello world
+lua5.2 example/fs.lua
 ```
 
-## Test
+For more examples and full API documentation, see the
+[User guide](https://opendal.apache.org/docs/bindings/lua).
+
+## Contributing
+
+Contributions are welcome. Run tests with
+[busted](https://lunarmodules.github.io/busted/):
 
 ```bash
-$ busted -o gtest test/opendal_test.lua
-[==========] Running tests from scanned files.
-[----------] Global test environment setup.
-[----------] Running tests from test/opendal_test.lua
-[ RUN      ] test/opendal_test.lua @ 24: opendal unit test opendal fs schema operator function in fs schema
-[       OK ] test/opendal_test.lua @ 24: opendal unit test opendal fs schema operator function in fs schema (1.52 ms)
-[ RUN      ] test/opendal_test.lua @ 36: opendal unit test opendal fs schema meta function in fs schema
-[       OK ] test/opendal_test.lua @ 36: opendal unit test opendal fs schema meta function in fs schema (0.24 ms)
-[----------] 2 tests from test/opendal_test.lua (3.47 ms total)
-
-[----------] Global test environment teardown.
-[==========] 2 tests from 1 test file ran. (3.54 ms total)
-[  PASSED  ] 2 tests.
+busted -o gtest test/opendal_test.lua
 ```
 
 ## License and Trademarks
 
 Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
 
-Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation.
+Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or
+trademarks of the Apache Software Foundation.
diff --git a/bindings/nodejs/CONTRIBUTING.md b/bindings/nodejs/CONTRIBUTING.md
index 01d49424b..1b8eaee31 100644
--- a/bindings/nodejs/CONTRIBUTING.md
+++ b/bindings/nodejs/CONTRIBUTING.md
@@ -6,6 +6,7 @@
     - [Bring your own toolbox](#bring-your-own-toolbox)
   - [Build](#build)
   - [Test](#test)
+  - [Docs](#docs)
 
 ## Setup
 
@@ -101,3 +102,24 @@ Taking 'service-memory' as an example, you can use the following command to run
    Start at  01:42:07
    Duration  233ms (transform 25ms, setup 0ms, collect 56ms, tests 40ms, environment 0ms, prepare 52ms)
 ```
+
+Services behavior tests read their config from env vars or a `.env` file. Copy
+[`.env.example`](/.env.example) to `$(pwd)/.env` and adjust the values, or export
+the vars directly. For example, to run the `fs` service tests against `/tmp`:
+
+```properties
+OPENDAL_TEST=fs
+OPENDAL_FS_ROOT=/tmp
+```
+
+## Docs
+
+To build the docs locally:
+
+```shell
+# Only needed once, unless you want to update the docs theme.
+pnpm run build:theme
+
+# Build the docs.
+pnpm run docs
+```
diff --git a/bindings/nodejs/README.md b/bindings/nodejs/README.md
index ab2842514..38f8d7e2a 100644
--- a/bindings/nodejs/README.md
+++ b/bindings/nodejs/README.md
@@ -6,11 +6,15 @@
 
 ![](https://github.com/apache/opendal/assets/5351546/87bbf6e5-f19e-449a-b368-3e283016c887)
 
+A native Node.js binding for Apache OpenDAL: access S3, GCS, Azure Blob, HDFS, the local filesystem, and 50+ more services through one API.
+
 > **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
 
 ## Useful Links
 
-- [Documentation](https://opendal.apache.org/docs/nodejs/)
+- [User guide](https://opendal.apache.org/docs/bindings/nodejs)
+- [API documentation](https://opendal.apache.org/docs/nodejs/)
+- [Services & configuration](https://opendal.apache.org/services)
 - [Upgrade Guide](./upgrade.md)
 
 ## Installation
@@ -19,38 +23,7 @@
 npm install opendal
 ```
 
-## Docs
-
-To build the docs locally, please run the following commands:
-
-```shell
-# Only need to run once unless you want to update the docs theme
-pnpm run build:theme
-
-# Build the docs
-pnpm run docs
-```
-
-## Tests
-
-Services behavior tests read necessary configs from env vars or the `.env` file.
-
-You can copy [.env.example](/.env.example) to `$(pwd)/.env` and change the values on need, or directly set env vars with `export KEY=VALUE`.
-
-Take `fs` for example, we need to enable bench on `fs` on `/tmp`:
-
-```properties
-OPENDAL_TEST=fs
-OPENDAL_FS_ROOT=/tmp
-```
-
-You can run service behavior tests of enabled with the following command:
-
-```shell
-pnpm build && pnpm test
-```
-
-## Usage
+## Quickstart
 
 ```javascript
 import { Operator } from "opendal";
@@ -67,183 +40,21 @@ async function main() {
 main();
 ```
 
-## Layers
-
-OpenDAL provides layers to add additional functionality to operators. You can chain multiple layers together to build powerful data access pipelines.
-
-### Available Layers
-
-- **RetryLayer** - Retry failed operations automatically
-- **ConcurrentLimitLayer** - Limit concurrent requests
-- **TimeoutLayer** - Add timeout for operations
-- **LoggingLayer** - Log all operations
-- **ThrottleLayer** - Limit bandwidth usage
-
-### TimeoutLayer
-
-Prevents operations from hanging indefinitely:
-
-```javascript
-import { Operator, TimeoutLayer } from "opendal";
-
-const op = new Operator("fs", { root: "/tmp" });
-
-const timeout = new TimeoutLayer();
-timeout.timeout = 10000; // 10 seconds for non-IO operations (ms)
-timeout.ioTimeout = 3000; // 3 seconds for IO operations (ms)
-op.layer(timeout.build());
-```
-
-**Default values:** timeout: 60s, ioTimeout: 10s
-
-### LoggingLayer
-
-Add structured logging for debugging and monitoring:
-
-```javascript
-import { Operator, LoggingLayer } from "opendal";
-
-const op = new Operator("fs", { root: "/tmp" });
-
-const logging = new LoggingLayer();
-op.layer(logging.build());
-
-// All operations will be logged
-await op.write("test.txt", "Hello World");
-```
-
-Enable logging output:
-
-```bash
-# Show debug logs
-RUST_LOG=debug node app.js
-
-# Show only OpenDAL logs
-RUST_LOG=opendal::services=debug node app.js
-```
-
-### ThrottleLayer
-
-Control bandwidth usage with rate limiting:
-
-```javascript
-import { Operator, ThrottleLayer } from "opendal";
-
-const op = new Operator("s3", {
-  bucket: "my-bucket",
-  region: "us-east-1",
-});
-
-// Limit to 10 KiB/s with 10 MiB burst
-const throttle = new ThrottleLayer(10 * 1024, 10 * 1024 * 1024);
-op.layer(throttle.build());
-```
-
-**Parameters:**
-
-- `bandwidth`: Maximum bytes per second
-- `burst`: Maximum burst size (must be larger than any operation size)
-
-### RetryLayer
-
-Automatically retry temporary failed operations:
-
-```javascript
-import { Operator, RetryLayer } from "opendal";
-
-const op = new Operator("s3", {
-  bucket: "my-bucket",
-  region: "us-east-1",
-});
-
-const retry = new RetryLayer();
-retry.maxTimes = 3;
-retry.jitter = true;
-op.layer(retry.build());
-```
-
-### ConcurrentLimitLayer
-
-Limit concurrent requests to storage services:
-
-```javascript
-import { Operator, ConcurrentLimitLayer } from "opendal";
-
-const op = new Operator("s3", {
-  bucket: "my-bucket",
-  region: "us-east-1",
-});
-
-// Allow max 1024 concurrent operations
-const limit = new ConcurrentLimitLayer(1024);
-limit.httpPermits = 512; // Limit HTTP requests separately
-op.layer(limit.build());
-```
-
-### Combining Multiple Layers
-
-Stack multiple layers for comprehensive control:
-
-```javascript
-import {
-  Operator,
-  LoggingLayer,
-  TimeoutLayer,
-  RetryLayer,
-  ThrottleLayer,
-} from "opendal";
-
-const op = new Operator("s3", {
-  bucket: "my-bucket",
-  region: "us-east-1",
-});
-
-// Layer 1: Logging for observability
-const logging = new LoggingLayer();
-op.layer(logging.build());
-
-// Layer 2: Timeout protection
-const timeout = new TimeoutLayer();
-timeout.timeout = 30000;
-timeout.ioTimeout = 10000;
-op.layer(timeout.build());
-
-// Layer 3: Retry on failures
-const retry = new RetryLayer();
-retry.maxTimes = 3;
-retry.jitter = true;
-op.layer(retry.build());
-
-// Layer 4: Bandwidth throttling
-const throttle = new ThrottleLayer(100 * 1024, 10 * 1024 * 1024);
-op.layer(throttle.build());
-
-// Now the operator has full production-ready protection
-await op.write("data.json", JSON.stringify(data));
-```
-
-## Usage with Next.js
-
-Config automatically be bundled by [Next.js](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages).
-
-```javascript
-/** @type {import('next').NextConfig} */
-const nextConfig = {
-  serverExternalPackages: ["opendal"],
-};
-
-module.exports = nextConfig;
-```
+The API is promise-based; every operation has a synchronous `*Sync` variant too.
+For configuring backends, streaming, layers (retry, timeout, logging, throttle,
+concurrency limits), presigned URLs, and Next.js usage, see the full
+[user guide](https://opendal.apache.org/docs/bindings/nodejs).
 
 ## Contributing
 
-- Start with [Contributing Guide](CONTRIBUTING.md).
-- Submit [Issues](https://github.com/apache/opendal/issues/new) for bug report or feature requests.
-- Asking questions in the [Discussions](https://github.com/apache/opendal/discussions/new?category=q-a).
-  - Talk to community at [Discord](https://opendal.apache.org/discord).
+- Start with the [Contributing Guide](CONTRIBUTING.md).
+- Submit [Issues](https://github.com/apache/opendal/issues/new) for bug reports or feature requests.
+- Ask questions in the [Discussions](https://github.com/apache/opendal/discussions/new?category=q-a).
+- Talk to the community on [Discord](https://opendal.apache.org/discord).
 
 ## License and Trademarks
 
 Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
 
 Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation.
+</content>
diff --git a/bindings/ocaml/README.md b/bindings/ocaml/README.md
index 4eae1d043..6844b6abc 100644
--- a/bindings/ocaml/README.md
+++ b/bindings/ocaml/README.md
@@ -1,105 +1,115 @@
 # Apache OpenDAL™ OCaml Binding (WIP)
 
-[![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/bindings/ocaml)
+[![status: unreleased](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/docs/bindings/ocaml)
 
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
+An OCaml binding for [Apache OpenDAL](https://opendal.apache.org/) — access
+S3, GCS, Azure Blob, local filesystem, and 50+ more storage services through
+one API, backed by the Rust core.
 
-## Requirements
+> **Experimental / Work in Progress.** APIs may change without notice. Not
+> published to opam. Build from source only.
 
-* OCaml version > 4.03 and < 5.0.0
+## Useful Links
 
-## Contributing
+- **User guide**: https://opendal.apache.org/docs/bindings/ocaml
+- **Services & configuration**: https://opendal.apache.org/services
+- **Source**: [`bindings/ocaml/`](https://github.com/apache/opendal/tree/main/bindings/ocaml)
 
-### Setup
+## Requirements
 
-We recommend using `OPAM the OCaml Package Manager` to install and manage the OCaml environment.
+- OCaml >= 4.10 and < 5.0 (OCaml 5 is not yet supported)
+- Rust stable toolchain (for the `build.rs` native compilation step)
+- dune >= 2.1
 
-#### Install OPAM
+## Build & Install
 
-The quickest way to get the latest opam up and working is to run this script:
+Install opam and create a 4.x switch:
 
 ```bash
+# Install opam (if needed)
 bash -c "sh <(curl -fsSL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)"
-```
 
-Similarly, you can also use your distribution's package manager to install
+# macOS
+brew install opam
 
-##### Arch
+# Debian/Ubuntu
+apt-get install opam
 
-```bash
+# Arch
 pacman -S opam
 ```
 
-##### Debian | Ubuntu
-
-```bash
-apt-get install opam
-```
-
-#### macOS
+Initialize opam and create a switch:
 
 ```bash
-brew install opam
+opam init --bare -a -y          # use --disable-sandboxing on WSL1
+opam switch create opendal-ocaml4.14 ocaml-base-compiler.4.14.0
+eval $(opam env)
 ```
 
-#### Init OPAM
-
-*Do not put sudo in front of any opam commands. That would break your OCaml installation.*
-
-After Installing OPAM, we need to initialize it
-
-For the general case, we can execute
+Install optional dev tools (recommended for IDE support):
 
 ```bash
-opam init --bare -a -y
+opam install -y utop odoc ounit2 ocaml-lsp-server ocamlformat ocamlformat-rpc
 ```
 
-If you are using WSL1 on windows, run:
+Build and test:
 
 ```bash
-opam init --bare -a -y --disable-sandboxing
+cd bindings/ocaml
+dune build
+dune test
 ```
 
-#### Create OPAM Switch
+## Quickstart
 
-Using opam, we can have multiple versions of ocaml at the same time; this is called switch.
+```ocaml
+open Opendal
 
-Due to the upstream `ocaml-rs`, we currently do not support OCaml5, and recommend using the latest version of OCaml4
-We can create use this command:
+(* Create an operator for the local filesystem *)
+let () =
+  match Operator.new_operator "fs" [ ("root", "/tmp") ] with
+  | Error err -> Printf.eprintf "Error: %s\n" err
+  | Ok op ->
 
-```bash
-opam switch create opendal-ocaml4.14 ocaml-base-compiler.4.14.0
+  (* Write a file *)
+  ignore (Operator.write op "hello.txt" (Bytes.of_string "Hello, World!"));
 
-eval $(opam env)
+  (* Read it back *)
+  (match Operator.read op "hello.txt" with
+  | Ok content ->
+      let text = content |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string in
+      print_endline text    (* Hello, World! *)
+  | Error err -> Printf.eprintf "Error: %s\n" err)
 ```
 
-#### Install OPAM Package
+Every fallible function returns `('a, string) result`. For S3 or other
+backends, change the scheme and pass the appropriate config keys:
 
-OpenDAL does not depend on opam package except `ounit2` for testing.
-However, to facilitate development in an IDE such as vscode, it is usually necessary to install the following content
-
-```bash
-opam install -y utop odoc ounit2 ocaml-lsp-server ocamlformat ocamlformat-rpc
+```ocaml
+match Operator.new_operator "s3"
+  [ ("bucket", "my-bucket"); ("region", "us-east-1")
+  ; ("access_key_id", "..."); ("secret_access_key", "...") ]
+with
+| Error err -> Printf.eprintf "Error: %s\n" err
+| Ok _op -> ()   (* use _op here *)
 ```
 
-### Build
+See the [user guide](https://opendal.apache.org/docs/bindings/ocaml) for
+examples covering streaming reads/writes, directory listing, and capability
+checks.
 
-```bash
-cd bindings/ocaml
-dune build
-```
-
-### Test
-
-To execute unit tests, we can simply use the following command:
+## Contributing
 
-```bash
-cd bindings/ocaml
-dune test
-```
+Contributions are welcome. Standard OCaml style; format with `ocamlformat`
+before submitting. See the repository
+[CONTRIBUTING.md](https://github.com/apache/opendal/blob/main/CONTRIBUTING.md)
+for the general process.
 
 ## License and Trademarks
 
-Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+Licensed under the Apache License, Version 2.0:
+http://www.apache.org/licenses/LICENSE-2.0
 
-Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation.
+Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or
+trademarks of the Apache Software Foundation.
diff --git a/bindings/php/README.md b/bindings/php/README.md
index 7ce2c3c3b..254328386 100644
--- a/bindings/php/README.md
+++ b/bindings/php/README.md
@@ -1,49 +1,38 @@
-# Apache OpenDAL™ PHP Binding (WIP)
+# Apache OpenDAL™ PHP Binding
 
-[![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/bindings/php/)
+[![status: unreleased](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/docs/bindings/php)
 
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
+A PHP extension for OpenDAL: access S3, GCS, Azure Blob, the local filesystem,
+and 50+ more services through one synchronous API, built on the Rust core.
 
-## Example
+**Status**: experimental / work in progress. Not published to any package
+registry. Expect breaking changes.
 
-```php
-use OpenDAL\Operator;
+## Useful Links
 
-$op = new Operator("fs", ["root" => "/tmp"]);
-$op->write("test.txt", "hello world");
-
-echo $op->read("test.txt"); // hello world
-```
+- **User guide**: [opendal.apache.org/docs/bindings/php](https://opendal.apache.org/docs/bindings/php)
+- **Services & configuration**: [opendal.apache.org/services](https://opendal.apache.org/services)
+- **Source**: [`bindings/php/`](https://github.com/apache/opendal/tree/main/bindings/php)
+- **ext-php-rs**: [github.com/davidcole1340/ext-php-rs](https://github.com/davidcole1340/ext-php-rs)
 
 ## Requirements
 
-* PHP 8.1+
-* Composer
+- PHP 8.1+
+- Rust and Cargo — [rust-lang.org/tools/install](https://www.rust-lang.org/tools/install)
 
-## Install Extension
+## Build and install the extension
 
-We use [ext-php-rs](https://github.com/davidcole1340/ext-php-rs) to build PHP extensions natively in Rust, it's different from the traditional PHP extension development and cannot be installed using `pecl` or `phpize`. Before installing the extension, it is necessary to install Rust and Cargo. For instructions on how to install them, please refer to [Rust's website](https://www.rust-lang.org/tools/install).
-
-1. Clone the repository
-
-```bash
-git clone git@github.com:apache/opendal.git
-```
-
-2. Build the opendal-php extension
+The extension cannot be installed via `pecl` or Composer. Build from source:
 
 ```bash
+git clone https://github.com/apache/opendal.git
 cd opendal/bindings/php
-cargo build
+cargo build          # add --release for production
 ```
 
-> don't forget to add `--release` flag for production use.
-
-3. Enable extension for PHP Manually
+Copy the library to PHP's extension directory, then register it in `php.ini`:
 
 ```bash
-cd opendal
-
 # Linux
 cp target/debug/libopendal_php.so $(php -r "echo ini_get('extension_dir');")/libopendal_php.so
 echo "extension=libopendal_php.so" >> $(php -r "echo php_ini_loaded_file();")
@@ -57,34 +46,56 @@ cp target/debug/libopendal_php.dll $(php -r "echo ini_get('extension_dir');")/li
 echo "extension=libopendal_php.dll" >> $(php -r "echo php_ini_loaded_file();")
 ```
 
-4. Enable extension for PHP using cargo-php
-
-You can also use cargo-php directly to install the extension, see [cargo-php](https://davidcole1340.github.io/ext-php-rs/getting-started/cargo-php.html) for more details.
+Or use `cargo-php` to build and install in one step:
 
 ```bash
 cargo install cargo-php
-cd opendal/bindings/php
 cargo php install
 ```
-This command will automatically build the extension and copy it to the extension directory of the current PHP version.
-
-5. Test
 
-use `php -m` to check if the extension is installed successfully.
+Verify with:
 
 ```bash
 php -m | grep opendal-php
 ```
 
-Composer test:
+## Quickstart
+
+```php
+use OpenDAL\Operator;
+
+$op = new Operator("memory", []);
+
+$op->write("hello.txt", "Hello, World!");
+
+echo $op->read("hello.txt"); // Hello, World!
+
+$meta = $op->stat("hello.txt");
+echo $meta->content_length;  // 13
+
+$op->delete("hello.txt");
+```
+
+Replace `"memory"` with any supported scheme and pass its configuration as the
+second argument — for example, `new Operator("fs", ["root" => "/tmp"])`.
+
+For the full guide including error handling and binary writes, see the
+[user guide](https://opendal.apache.org/docs/bindings/php).
+
+## Running the tests
 
 ```bash
 cd opendal/bindings/php
-
 composer install
 composer test
 ```
 
+## Contributing
+
+All contributions go through the main OpenDAL repository at
+[github.com/apache/opendal](https://github.com/apache/opendal). Open an issue
+or pull request there.
+
 ## License and Trademarks
 
 Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
diff --git a/bindings/python/README.md b/bindings/python/README.md
index 22aea23f9..b77924daf 100644
--- a/bindings/python/README.md
+++ b/bindings/python/README.md
@@ -2,7 +2,7 @@
 
 [![Status](https://img.shields.io/badge/status-released-blue)](https://pypi.org/project/opendal/)
 [![PyPI](https://img.shields.io/pypi/v/opendal.svg?logo=PyPI)](https://pypi.org/project/opendal/)
-[![Website](https://img.shields.io/badge/opendal-OpenDAL_Website-red?logo=Apache&logoColor=red)](https://opendal.apache.org/docs/python/)
+[![Website](https://img.shields.io/badge/opendal-OpenDAL_Website-red?logo=Apache&logoColor=red)](https://opendal.apache.org/docs/bindings/python)
 
 This package provides a native Python binding for **Apache OpenDAL™**, a data access
 layer that allows you to access various storage services in a unified way.
@@ -13,119 +13,49 @@ layer that allows you to access various storage services in a unified way.
 
 ## Useful Links
 
-- [Documentation](https://opendal.apache.org/docs/python/)
-- [Examples](./docs/examples)
-- [Upgrade Guide](./upgrade.md)
-
----
-
-## Features
-
-- **Unified API**: Access S3, GCS, Azure Blob, HDFS, FTP, and more with the same set of
-  commands.
-- **Native Performance**: Built in Rust for high performance and safety.
-- **Async Support**: First-class `async` API for modern Python applications.
-- **Easy to Use**: Simple and intuitive API design.
-
----
+- **User guide**: [opendal.apache.org/docs/bindings/python](https://opendal.apache.org/docs/bindings/python) — install, connect, common tasks, and going to production.
+- **API reference**: [opendal.apache.org/docs/python](https://opendal.apache.org/docs/python/)
+- **Services & configuration**: [opendal.apache.org/services](https://opendal.apache.org/services)
+- **Upgrade guide**: [`upgrade.md`](./upgrade.md)
+- **Examples**: [`docs/examples`](./docs/examples)
 
 ## Installation
 
-Install the package directly from PyPI:
-
 ```bash
 pip install opendal
 ```
 
----
-
-## Usage
-
-Here are a few examples of how to use OpenDAL with different storage backends.
-
-### Local Filesystem (`fs`)
+## Quickstart
 
 ```python
 import opendal
 
-# Initialize the operator for the local filesystem
+# Configure a service, then build an operator from it.
 op = opendal.Operator("fs", root="/tmp")
 
-# Write data to a file
-op.write("test.txt", b"Hello World")
-
-# Read data from the file
-content = op.read("test.txt")
-print(op.read("test.txt"))
-
-# Get metadata
-metadata = op.stat("test.txt")
-print(f"Content length: {metadata.content_length}") # Output: 11
-```
-
-### Amazon S3
-
-The API remains the same—just change the scheme and credentials.
-
-```python
-import opendal
-
-# Initialize the operator for S3
-op = opendal.Operator(
-    "s3",
-    bucket="your_bucket_name",
-    region="your_region",
-    root="/path/to/root"
-)
-
+# The same verbs work on every service.
 op.write("test.txt", b"Hello World")
 print(op.read("test.txt"))
 print(op.stat("test.txt").content_length)
 ```
 
-### Async Usage (`s3`)
-
-OpenDAL also provides a fully asynchronous API.
+To use a real backend, change the scheme and pass its configuration — the
+operations stay identical:
 
 ```python
-import asyncio
-import opendal
-
-async def main():
-    # Use AsyncOperator for async operations
-    op = opendal.AsyncOperator("s3", root="/tmp", bucket="your_bucket_name", region="your_region")
-
-    await op.write("test.txt", b"Hello World")
-    print(await op.read("test.txt"))
-
-asyncio.run(main())
+op = opendal.Operator("s3", bucket="your_bucket", region="your_region")
 ```
 
----
-
-## Development
-
-This project uses [`just`](https://github.com/casey/just) as a command runner to
-simplify the development workflow.
-
-1. **Clone the repository and set up the environment:**
-
-   ```shell
-   # This will create a virtual environment and install all dependencies
-   just setup
-   ```
-
-2. **Run tests:**
-
-   ```shell
-   # Example: Run tests for the 'fs' operator
-   OPENDAL_TEST=fs OPENDAL_FS_ROOT=/tmp just test
-   ```
+OpenDAL also has a first-class async API via `opendal.AsyncOperator`. See
+[Getting started](https://opendal.apache.org/docs/bindings/python/getting-started)
+and [Connecting to your storage](https://opendal.apache.org/docs/bindings/python/connecting)
+for the full guide.
 
-For a complete guide on building, testing, and contributing, please see our
-**[CONTRIBUTING.md](./CONTRIBUTING.md)** file.
+## Contributing
 
----
+This project uses [`just`](https://github.com/casey/just) as a command runner.
+For a complete guide on building, testing, and contributing, see
+**[CONTRIBUTING.md](./CONTRIBUTING.md)**.
 
 ## Used By
 
diff --git a/bindings/ruby/CONTRIBUTING.md b/bindings/ruby/CONTRIBUTING.md
new file mode 100644
index 000000000..d8c69689f
--- /dev/null
+++ b/bindings/ruby/CONTRIBUTING.md
@@ -0,0 +1,32 @@
+# Contributing to Apache OpenDAL™ Ruby Binding
+
+## Development
+
+Install gem and its dependencies:
+
+```shell
+bundle
+```
+
+Build bindings:
+
+```shell
+bundle exec rake compile
+```
+
+Run tests:
+
+```shell
+bundle exec rake test:base
+
+# Optional
+OPENDAL_TEST=fs bundle exec rake test:service
+```
+
+Run linters:
+
+```shell
+bundle exec rake standard:fix
+rustfmt --config-path ../../rustfmt.toml src/*.rs # Run rustfmt for Rust files
+cargo clippy --fix --all-targets # Run rust linter clippy
+```
diff --git a/bindings/ruby/README.md b/bindings/ruby/README.md
index 77fb560ed..51c8aefca 100644
--- a/bindings/ruby/README.md
+++ b/bindings/ruby/README.md
@@ -3,159 +3,62 @@
 [![Gem Version](https://img.shields.io/gem/v/opendal)](https://rubygems.org/gems/opendal)
 [![Gem Downloads (for latest version)](https://img.shields.io/gem/dtv/opendal)](https://rubygems.org/gems/opendal)
 
-OpenDAL's Ruby [gem](https://rubygems.org/gems/opendal).
+OpenDAL's Ruby [gem](https://rubygems.org/gems/opendal): access S3, GCS, Azure Blob, the local filesystem, and 50+ more services through one API.
 
 ![](https://github.com/apache/opendal/assets/5351546/87bbf6e5-f19e-449a-b368-3e283016c887)
 
 > **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
 
-Read OpenDAL's [ruby](https://opendal.apache.org/docs/ruby/) documentation.
+## Useful Links
 
-You can find Rust documentation [here](https://docs.rs/opendal/latest/opendal/index.html).
+- [User guide](https://opendal.apache.org/docs/bindings/ruby)
+- [Services & configuration](https://opendal.apache.org/services)
+- [API reference](https://opendal.apache.org/docs/ruby/)
+- [Rust core documentation](https://docs.rs/opendal/latest/opendal/index.html)
 
-## Get started
-
-### Installation
-
-Install gem:
+## Installation
 
 ```shell
-bundle add opendal
+gem install opendal
 ```
 
-or add it in Gemfile:
+Or add it to your `Gemfile`:
 
 ```ruby
 # Gemfile
-
-source "https://rubygems.org"
-
-gem 'opendal'
+gem "opendal"
 ```
 
-### Examples
-
-#### File operations with an in-memory storage 
+## Quickstart
 
 ```ruby
-require 'opendal'
+require "opendal"
 
 op = OpenDal::Operator.new("memory", {})
-op.write("file", "hello world")
-puts op.read("file") # => "hello world"
-puts ""
-
-puts "List:", op.list("").map { |e| e.path }
-puts ""
-
-puts "Stat"
-puts op.stat("file").inspect # => #<OpenDal::Metadata mode: File,         content_type: ,         content_length: 11>
-puts ""
-
-puts "Deleting 'file'"
-op.delete("/file")
-puts ""
-
-puts "Exist?", op.exist?("/file") # => false
-puts ""
-
-puts "Info:", op.info.inspect # => #<OpenDal::OperatorInfo scheme: "memory", root: "/">
-```
+op.write("hello.txt", "Hello, World!")
+puts op.read("hello.txt") # => "Hello, World!"
 
-#### A S3 operator
-
-```ruby
-require 'opendal'
-
-op = OpenDal::Operator.new("s3", {
-  "endpoint" => "http://localhost:9000",
-  "access_key_id" => "minioadmin" ,
-  "secret_access_key" => "minioadmin",
-  "bucket" => "test",
-  "region" => "us-east-1",
-})
-op.write("file", "hello world")
-puts op.read("file") # => "hello world"
-puts ""
-
-puts "List:", op.list("").map { |e| e.path }
-puts ""
-
-puts "Stat"
-puts op.stat("file").inspect # => #<OpenDal::Metadata mode: File,         content_type: binary/octet-stream,         content_length: 11>
-puts ""
-
-puts "Deleting 'file'"
-op.delete("file")
-puts ""
-
-puts "Exist?", op.exist?("file") # => false
-puts ""
+op.list("").each do |entry|
+  puts entry.path
+end
 
-puts "Info:", op.info.inspect # => #<OpenDal::OperatorInfo scheme: "s3", root: "/">
+op.delete("hello.txt")
 ```
 
-#### Use middleware
+Point it at a real backend by changing the scheme and passing its configuration:
 
 ```ruby
-require 'opendal'
-
 op = OpenDal::Operator.new("s3", {
-  "endpoint" => "http://localhost:9000",
-  "access_key_id" => "minioadmin" ,
-  "secret_access_key" => "minioadmin",
-  "bucket" => "test",
+  "bucket" => "my-bucket",
   "region" => "us-east-1",
 })
-
-op.middleware(OpenDal::Middleware::ConcurrentLimit.new(5))
-op.middleware(OpenDal::Middleware::Retry.new)
-op.middleware(OpenDal::Middleware::Timeout.new(1, 2))
-
-op.list("/").map do |e|
-  puts e.inspect
-end
-```
-
-## Documentation
-
-More detailed documentation is a work in progress.
-
-- OpenDAL's [ruby](https://opendal.apache.org/docs/ruby/) documentation
-- Rust [documentation](https://docs.rs/opendal/latest/opendal/index.html)
-- Rust documentation for [services](https://docs.rs/opendal/latest/opendal/services/index.html)
-- Rust documentation for [layers](https://docs.rs/opendal/latest/opendal/layers/index.html) (middlewares in Ruby)
-
-## Development
-
-Install gem and its dependencies:
-
-```shell
-bundle
-```
-
-Build bindings:
-
-```shell
-bundle exec rake compile
 ```
 
-Run tests:
+See the [user guide](https://opendal.apache.org/docs/bindings/ruby) for connecting to storage, common tasks, middlewares, and error handling.
 
-```shell
-bundle exec rake test:base
+## Contributing
 
-# Optional
-OPENDAL_TEST=fs bundle exec rake test:service
-```
-
-Run linters:
-
-```shell
-bundle exec rake standard:fix
-rustfmt --config-path ../../rustfmt.toml src/*.rs # Run rustfmt for Rust files
-cargo clippy --fix --all-targets # Run rust linter clippy
-```
+Development setup and contribution instructions live in [CONTRIBUTING.md](CONTRIBUTING.md).
 
 ## License and Trademarks
 
diff --git a/bindings/swift/README.md b/bindings/swift/README.md
index 4daa63b3c..6331a23c5 100644
--- a/bindings/swift/README.md
+++ b/bindings/swift/README.md
@@ -1,73 +1,98 @@
 # Apache OpenDAL™ Swift Binding (WIP)
 
-[![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/bindings/swift/)
+[![status: unreleased](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/docs/bindings/swift)
 
-![](https://github.com/apache/opendal/assets/5351546/87bbf6e5-f19e-449a-b368-3e283016c887)
+A Swift binding for OpenDAL. Access storage services through one consistent
+API backed by the Rust core. **Experimental — not suitable for production use.**
 
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
+> This binding has its own independent version number. Always check the
+> binding's version for compatibility, not the Rust core's version.
 
-## Using the Package
+## Useful Links
 
-### Build C Dependencies
+- **User guide**: https://opendal.apache.org/docs/bindings/swift
+- **Services & configuration**: https://opendal.apache.org/services
 
-The Swift binding depends on the C binding to OpenDAL. Before using this package, you need to build the C library first:
+## Build
 
-```
+The Swift binding wraps the C binding. Build the C library before using the package:
+
+```shell
 cd bindings/swift
 make build-c
 ```
 
-To check whether the package is ready to use, simply run the test:
+To verify everything works:
 
-```
+```shell
 make test
 ```
 
-### Add Dependency to Your Project
+## Installation
 
-The package manifest is not located at the root directory of its repository. To use it, add the path of this package to the `Package.swift` manifest of your project:
+The package is not published to any registry. Reference it by local path in
+your project's `Package.swift`:
 
 ```swift
 // swift-tools-version:5.7
 import PackageDescription
 
 let package = Package(
-  name: "MyTool",
-  dependencies: [
-    .package(path: "/path/to/opendal/bindings/swift/OpenDAL"),
-  ],
-  targets: [
-    .target(name: "MyTool", dependencies: [
-      .product(name: "OpenDAL", package: "OpenDAL"),
-    ]),
-  ]
+    name: "MyTool",
+    dependencies: [
+        .package(path: "/path/to/opendal/bindings/swift/OpenDAL"),
+    ],
+    targets: [
+        .target(name: "MyTool", dependencies: [
+            .product(name: "OpenDAL", package: "OpenDAL"),
+        ]),
+    ]
 )
 ```
 
-## Example
-
-The demo below shows how to write a key to the memory storage, and read it back:
+## Quickstart
 
 ```swift
 import OpenDAL
 
-// Create an operator with `memory` backend.
+// Create an operator backed by the in-memory service.
 let op = try Operator(scheme: "memory")
 
-// Write some data into path `/demo`.
-let someData = Data([1, 2, 3, 4])
-try op.blockingWrite(someData, to: "/demo")
+// Write bytes to a path.
+var data = Data([1, 2, 3, 4])
+try op.blockingWrite(&data, to: "/demo")
+
+// Read them back.
+let result = try op.blockingRead("/demo")
+print(result)
+```
+
+Both `blockingWrite` and `blockingRead` throw `OperatorError` on failure.
 
-// Read the data back.
-let readData = try op.blockingRead("/demo")
+For a real backend, pass the service scheme and options:
 
-// You can use the read data here.
-print(readData!)
+```swift
+let op = try Operator(
+    scheme: "s3",
+    options: [
+        "bucket": "my-bucket",
+        "region": "us-east-1",
+    ]
+)
 ```
 
+See the [user guide](https://opendal.apache.org/docs/bindings/swift) for more
+examples, and [Services](https://opendal.apache.org/services) for the full list
+of backends and their configuration keys.
+
+## Contributing
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up the development
+environment and submit patches.
+
 ## License and Trademarks
 
 Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
 
-Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation.
-
+Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or
+trademarks of the Apache Software Foundation.
diff --git a/bindings/zig/README.md b/bindings/zig/README.md
index 6647c24f3..9b7e3a037 100644
--- a/bindings/zig/README.md
+++ b/bindings/zig/README.md
@@ -1,26 +1,62 @@
-# Apache OpenDAL™ Zig Binding (WIP)
+# Apache OpenDAL™ Zig Binding
 
-[![](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/bindings/zig)
+[![Status: Unreleased](https://img.shields.io/badge/status-unreleased-red)](https://opendal.apache.org/docs/bindings/zig)
 
-![](https://github.com/apache/opendal/assets/5351546/87bbf6e5-f19e-449a-b368-3e283016c887)
+A Zig binding for [Apache OpenDAL](https://opendal.apache.org) — blocking access to
+S3, GCS, Azure Blob, the local filesystem, and 50+ more storage services through one
+API, backed by the Rust core.
 
-> **Note**: This binding has its own independent version number, which may differ from the Rust core version. When checking for updates or compatibility, always refer to this binding's version rather than the core version.
+> **Early-stage / Experimental.** The API is a work in progress and may change
+> between releases. There are no pre-built packages; you must build from source.
+
+## User Guide
+
+Full documentation is at
+**<https://opendal.apache.org/docs/bindings/zig>**
+
+The guide covers installation, building, operator construction, read/write/stat/delete,
+listing directories, and error handling.
 
 ## Build
 
-To compile OpenDAL Zig binding from source code, you need:
+Requirements: Zig 0.14.0 or higher, a C/C++ compiler, CMake, and a Rust toolchain.
 
-- [Zig](https://ziglang.org/download) 0.14.0 or higher
+```shell
+# From bindings/zig/
 
-```bash
-# build libopendal_c (underneath call make -C ../c)
+# Build the underlying C library
 zig build libopendal_c
-# build and run unit tests
+
+# Build and run the tests
 zig build test --summary all
 ```
 
+The build pulls the C binding header from `../c/include/opendal.h` and links against
+`libopendal_c` produced by the C binding's CMake build.
+
+## Quick example
+
+```zig
+const opendal = @import("opendal");
+
+var op = try opendal.Operator.init("memory", null);
+defer op.deinit();
+
+try op.write("/hello.txt", "Hello, World!");
+const data = try op.read("/hello.txt");
+```
+
+See the [User Guide](https://opendal.apache.org/docs/bindings/zig) for more.
+
+## Contributing
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, build steps, and how to run
+the test suite.
+
 ## License and Trademarks
 
-Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+Licensed under the Apache License, Version 2.0:
+<http://www.apache.org/licenses/LICENSE-2.0>
 
-Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of the Apache Software Foundation.
+Apache OpenDAL, OpenDAL, and Apache are either registered trademarks or trademarks of
+the Apache Software Foundation.
diff --git a/core/README.md b/core/README.md
index 9b586e91a..b4e9bc33e 100644
--- a/core/README.md
+++ b/core/README.md
@@ -16,199 +16,84 @@ Apache OpenDAL™ is an Open Data Access Layer that enables seamless interaction
 
 ## Useful Links
 
-- Documentation: [release](https://docs.rs/opendal/) | [dev](https://opendal.apache.org/docs/rust/opendal/)
+- **User guide**: [opendal.apache.org/docs/core](https://opendal.apache.org/docs/core) — install, operations, layers, idioms, and extending the core.
+- **API reference**: [docs.rs/opendal](https://docs.rs/opendal/) (release) | [dev](https://opendal.apache.org/docs/rust/opendal/)
+- **Services & configuration**: [opendal.apache.org/services](https://opendal.apache.org/services)
+- **Concepts**: [opendal.apache.org/docs/concepts](https://opendal.apache.org/docs/concepts)
+- [Upgrade Guide](https://docs.rs/opendal/latest/opendal/docs/upgrade/index.html) | [Release Notes](https://docs.rs/opendal/latest/opendal/docs/changelog/index.html) | [RFCs](https://docs.rs/opendal/latest/opendal/docs/rfcs/index.html)
 - [Examples](./examples)
-- [Release Notes](https://docs.rs/opendal/latest/opendal/docs/changelog/index.html)
-- [Upgrade Guide](https://docs.rs/opendal/latest/opendal/docs/upgrade/index.html)
-- [RFC List](https://docs.rs/opendal/latest/opendal/docs/rfcs/index.html)
 
-## Services
-
-OpenDAL supports the following storage [services](https://docs.rs/opendal/latest/opendal/services/index.html):
-
-| Type                           | Services                                                                                                                                 | 
-|--------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| 
-| Standard Storage Protocols     | ftp http [sftp] [webdav]                                                                                                                 |
-| Object Storage Services        | [azblob] [cos] [gcs] [obs] [oss] [s3] <br> [b2] [huggingface] [openstack_swift] [upyun] [vercel-blob]                                    |
-| File Storage Services          | fs [alluxio] [azdls] [azfile] [compfs] <br> [dbfs] [gridfs] [hdfs] [hdfs-native] [ipfs] [webhdfs]                                        |
-| Consumer Cloud Storage Service | [aliyun-drive] [gdrive] [onedrive] [dropbox] [koofr] <br> [pcloud] [seafile] [yandex-disk]                                      |
-| Key-Value Storage Services     | [cacache] [cloudflare-kv] [dashmap] memory [etcd] <br> [foundationdb] [persy] [redis] [rocksdb] [sled] <br> [redb] [tikv] |
-| Database Storage Services      | [d1] [mongodb] [mysql] [postgresql] [sqlite] [surrealdb]                                                                                 |
-| Cache Storage Services         | [ghac] [memcached] [mini-moka] [moka] [vercel-artifacts]                                                                                 |
-| Git Based Storage Services     | [huggingface]                                                                                                                            |
-
-[sftp]: https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02
-[webdav]: https://datatracker.ietf.org/doc/html/rfc4918
-
-[azblob]: https://azure.microsoft.com/en-us/services/storage/blobs/
-[cos]: https://www.tencentcloud.com/products/cos
-[gcs]: https://cloud.google.com/storage
-[obs]: https://www.huaweicloud.com/intl/en-us/product/obs.html
-[oss]: https://www.aliyun.com/product/oss
-[s3]: https://aws.amazon.com/s3/
-[b2]: https://www.backblaze.com/
-[openstack_swift]: https://docs.openstack.org/swift/latest/
-[upyun]: https://www.upyun.com/
-[vercel-blob]: https://vercel.com/docs/storage/vercel-blob
-
-[alluxio]: https://docs.alluxio.io/os/user/stable/en/api/REST-API.html
-[azdls]: https://azure.microsoft.com/en-us/products/storage/data-lake-storage/
-[azfile]: https://learn.microsoft.com/en-us/rest/api/storageservices/file-service-rest-api
-[compfs]: https://github.com/compio-rs/compio/
-[dbfs]: https://docs.databricks.com/en/dbfs/index.html
-[gridfs]: https://www.mongodb.com/docs/manual/core/gridfs/
-[hdfs]: https://hadoop.apache.org/docs/r3.3.4/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html
-[hdfs-native]: https://github.com/Kimahriman/hdfs-native
-[ipfs]: https://ipfs.tech/
-[webhdfs]: https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/WebHDFS.html
-
-[aliyun-drive]: https://www.aliyundrive.com/
-[gdrive]: https://www.google.com/drive/
-[onedrive]: https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage
-[dropbox]: https://www.dropbox.com/
-[koofr]: https://koofr.eu/
-[pcloud]: https://www.pcloud.com/
-[seafile]: https://www.seafile.com/
-[yandex-disk]: https://360.yandex.com/disk/
-
-[cacache]: https://crates.io/crates/cacache
-[cloudflare-kv]: https://developers.cloudflare.com/kv/
-[dashmap]: https://github.com/xacrimon/dashmap
-[etcd]: https://etcd.io/
-[foundationdb]: https://www.foundationdb.org/
-[persy]: https://crates.io/crates/persy
-[redis]: https://redis.io/
-[rocksdb]: http://rocksdb.org/
-[sled]: https://crates.io/crates/sled
-[redb]: https://crates.io/crates/redb
-[tikv]: https://tikv.org/
-
-[d1]: https://developers.cloudflare.com/d1/
-[mongodb]: https://www.mongodb.com/
-[mysql]: https://www.mysql.com/
-[postgresql]: https://www.postgresql.org/
-[sqlite]: https://www.sqlite.org/
-[surrealdb]: https://surrealdb.com/
-
-[ghac]: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows
-[memcached]: https://memcached.org/
-[mini-moka]: https://github.com/moka-rs/mini-moka
-[moka]: https://github.com/moka-rs/moka
-[vercel-artifacts]: https://vercel.com/docs/concepts/monorepos/remote-caching
-
-[huggingface]: https://huggingface.co/
+## Installation
 
-## Layers
+```shell
+cargo add opendal
+```
 
-OpenDAL supports the following storage [layers](https://docs.rs/opendal/latest/opendal/layers/index.html) to extend the behavior:
-
-| Name                      | Depends                  | Description                                                                           |
-|---------------------------|--------------------------|---------------------------------------------------------------------------------------|
-| [`AsyncBacktraceLayer`]   | [async-backtrace]        | Add Efficient, logical 'stack' traces of async functions for the underlying services. |
-| [`AwaitTreeLayer`]        | [await-tree]             | Add a Instrument await-tree for actor-based applications to the underlying services.  |
-| [`BlockingLayer`]         | [tokio]                  | Add blocking API support for non-blocking services.                                   |
-| [`ChaosLayer`]            | [rand]                   | Inject chaos into underlying services for robustness test.                            |
-| [`ConcurrentLimitLayer`]  | [tokio]                  | Add concurrent request limit.                                                         |
-| [`DtraceLayer`]           | [probe]                  | Support User Statically-Defined Tracing(aka USDT) on Linux                            |
-| [`LoggingLayer`]          | [log]                    | Add log for every operations.                                                         |
-| [`MetricsLayer`]          | [metrics]                | Add metrics for every operations.                                                     |
-| [`MimeGuessLayer`]        | [mime_guess]             | Add `Content-Type` automatically based on the file extension in the operation path.   |
-| [`FastraceLayer`]         | [fastrace]               | Add fastrace for every operations.                                                    |
-| [`OtelMetricsLayer`]      | [opentelemetry::metrics] | Add opentelemetry::metrics for every operations.                                      |
-| [`OtelTraceLayer`]        | [opentelemetry::trace]   | Add opentelemetry::trace for every operations.                                        |
-| [`PrometheusClientLayer`] | [prometheus_client]      | Add prometheus metrics for every operations.                                          |
-| [`PrometheusLayer`]       | [prometheus]             | Add prometheus metrics for every operations.                                          | 
-| [`RetryLayer`]            | [backon]                 | Add retry for temporary failed operations.                                            |
-| [`ThrottleLayer`]         | [governor]               | Add a bandwidth rate limiter to the underlying services.                              |
-| [`TimeoutLayer`]          | [tokio]                  | Add timeout for every operations to avoid slow or unexpected hang operations.         |
-| [`TracingLayer`]          | [tracing]                | Add tracing for every operations.                                                     |
-
-[`AsyncBacktraceLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.AsyncBacktraceLayer.html
-[async-backtrace]: https://github.com/tokio-rs/async-backtrace
-[`AwaitTreeLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.AwaitTreeLayer.html
-[await-tree]: https://github.com/risingwavelabs/await-tree
-[`BlockingLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.BlockingLayer.html
-[tokio]: https://github.com/tokio-rs/tokio
-[`ChaosLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.ChaosLayer.html
-[rand]: https://github.com/rust-random/rand
-[`ConcurrentLimitLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.ConcurrentLimitLayer.html
-[`DtraceLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.DtraceLayer.html
-[probe]: https://github.com/cuviper/probe-rs
-[`LoggingLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.LoggingLayer.html
-[log]: https://github.com/rust-lang/log
-[`MetricsLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.MetricsLayer.html
-[metrics]: https://github.com/metrics-rs/metrics
-[`MimeGuessLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.MimeGuessLayer.html
-[mime_guess]: https://github.com/abonander/mime_guess
-[`FastraceLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.FastraceLayer.html
-[fastrace]: https://github.com/fastracelabs/fastrace
-[`OtelMetricsLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.OtelMetricsLayer.html
-[`OtelTraceLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.OtelTraceLayer.html
-[opentelemetry::trace]: https://docs.rs/opentelemetry/latest/opentelemetry/trace/index.html
-[`PrometheusClientLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.PrometheusClientLayer.html
-[prometheus_client]: https://github.com/prometheus/client_rust
-[`PrometheusLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.PrometheusLayer.html
-[prometheus]: https://github.com/tikv/rust-prometheus
-[`RetryLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.RetryLayer.html
-[backon]: https://github.com/Xuanwo/backon
-[`ThrottleLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.ThrottleLayer.html
-[governor]: https://github.com/boinkor-net/governor
-[`TimeoutLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.TimeoutLayer.html
-[`TracingLayer`]: https://docs.rs/opendal/latest/opendal/layers/struct.TracingLayer.html
-[tracing]: https://github.com/tokio-rs/tracing
+Each service is a feature flag. The in-memory service is always available; enable
+any other service with its `services-*` feature, e.g. `features = ["services-s3"]`.
 
 ## Quickstart
 
 ```rust
-use opendal::Result;
-use opendal::layers::LoggingLayer;
 use opendal::services;
 use opendal::Operator;
+use opendal::Result;
 
 #[tokio::main]
 async fn main() -> Result<()> {
-    // Pick a builder and configure it.
-    let mut builder = services::S3::default();
-    builder.bucket("test");
+    // Configure a service, then build an operator from it.
+    let op = Operator::new(services::Memory::default())?;
 
-    // Init an operator
-    let op = Operator::new(builder)?
-        // Init with logging layer enabled.
-        .layer(LoggingLayer::default());
-
-    // Write data
+    // The same verbs work on every service.
     op.write("hello.txt", "Hello, World!").await?;
-
-    // Read data
-    let bs = op.read("hello.txt").await?;
-
-    // Fetch metadata
+    let bytes = op.read("hello.txt").await?;
     let meta = op.stat("hello.txt").await?;
-    let mode = meta.mode();
-    let length = meta.content_length();
-
-    // Delete
     op.delete("hello.txt").await?;
 
+    println!("read {} bytes", meta.content_length());
     Ok(())
 }
 ```
 
-## Examples
+To use a real backend, swap `Memory` for another service and configure it — the
+operations stay identical. See [Getting started](https://opendal.apache.org/docs/core/getting-started)
+and [Connecting to your storage](https://opendal.apache.org/docs/core/connecting).
 
-| Name                | Description                                                   |
-|---------------------|---------------------------------------------------------------|
-| [Basic]             | Show how to use opendal to operate storage service.           |
-| [Concurrent Upload] | Show how to perform upload concurrently to a storage service. |
-| [Multipart Upload]  | Show how to perform a multipart upload to a storage service.  |
+## Services
+
+OpenDAL talks to 50+ backends through one API. A selection by category:
+
+| Type                       | Services                                                        |
+|----------------------------|-----------------------------------------------------------------|
+| Standard Protocols         | ftp, http, sftp, webdav                                         |
+| Object Storage             | s3, gcs, azblob, oss, cos, obs, b2, vercel-blob, …             |
+| File Storage               | fs, hdfs, azdls, azfile, webhdfs, ipfs, …                      |
+| Consumer Cloud Storage     | gdrive, onedrive, dropbox, aliyun-drive, koofr, …             |
+| Key-Value & Database       | redis, etcd, tikv, rocksdb, sqlite, postgresql, mongodb, …    |
+| Cache                      | memcached, moka, mini-moka, ghac, vercel-artifacts            |
+
+See [Services](https://opendal.apache.org/services) for the full list and each
+service's configuration keys.
+
+## Layers
+
+Wrap an operator with layers to add retry, logging, timeout, metrics, and other
+cross-cutting behavior without touching your storage code. Retry, logging,
+timeout, and concurrency limit are built in:
+
+```rust
+use opendal::layers::RetryLayer;
+
+let op = op.layer(RetryLayer::new());
+```
 
-[Basic]: ./examples/basic
-[Concurrent Upload]: ./examples/concurrent-upload
-[Multipart Upload]: ./examples/multipart-upload
+See [Going to production](https://opendal.apache.org/docs/core/production) and the
+[`layers` module](https://docs.rs/opendal/latest/opendal/layers/index.html).
 
 ## Contributing
 
-Check out the [CONTRIBUTING](./CONTRIBUTING.md) guide for more details on getting started with contributing to this project.
+Check out the [CONTRIBUTING](./CONTRIBUTING.md) guide for building, testing, and
+submitting changes to the core.
 
 ## Used by
 
diff --git a/website/docs/03-concepts.mdx b/website/docs/03-concepts.mdx
index 468a71d23..f0a144999 100644
--- a/website/docs/03-concepts.mdx
+++ b/website/docs/03-concepts.mdx
@@ -66,5 +66,5 @@ The same four steps, in the language you already ship:
 ## Next steps
 
 - [Services](/services) — every supported backend and its configuration.
-- [Core](./10-core.mdx) — the Rust core library that powers everything.
-- [Bindings](/docs/category/bindings) — use OpenDAL from your language.
+- [Rust core](./10-core/01-overview.md) — the reference implementation that powers every binding.
+- Pick your language from the sidebar — [Python](/docs/bindings/python), [Node.js](/docs/bindings/nodejs), [Java](/docs/bindings/java), and more.
diff --git a/website/docs/10-core.mdx b/website/docs/10-core.mdx
deleted file mode 100644
index 5c2d7dad8..000000000
--- a/website/docs/10-core.mdx
+++ /dev/null
@@ -1,11 +0,0 @@
----
-title: Core
-sidebar_position: 4
----
-
-import GitHubReadme from '../components/GitHubReadme';
-import Content from '../../core/README.md';
-
-<GitHubReadme basePath="core/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/10-core/01-overview.md b/website/docs/10-core/01-overview.md
new file mode 100644
index 000000000..82ff091b7
--- /dev/null
+++ b/website/docs/10-core/01-overview.md
@@ -0,0 +1,67 @@
+---
+title: Rust Core
+sidebar_label: Overview
+slug: /core
+description: The Rust core library that powers OpenDAL — install it, see what it supports, and where to go next.
+---
+
+# Apache OpenDAL™ Rust Core
+
+`opendal` is the Rust core that every OpenDAL binding is built on. It is the
+most complete and most mature surface of the project: every service, every
+layer, and the full async API live here first.
+
+New to the model behind the API? Read [Concepts](../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+## Status
+
+Stable and production-ready. The crate follows semver; breaking changes are
+batched into minor releases and documented in the [Upgrade Guide].
+
+## Capabilities
+
+- **Async-first**, with an optional [blocking API](./02-getting-started.md#blocking-vs-async).
+- **Streaming** reads and writes for data that does not fit in memory.
+- **50+ services** behind one API — see [Services](/services) for the full list
+  and per-service configuration.
+- **Composable layers** for retry, logging, timeout, and metrics — see
+  [Going to production](./05-production.md).
+- **Presigned URLs** for services that support them (S3, GCS, Azblob, …).
+
+## Installation
+
+```shell
+cargo add opendal
+```
+
+OpenDAL is modular: each service is its own feature flag. The in-memory service
+is always available; enable any other service — including `fs` — with its
+`services-*` feature.
+
+```toml
+[dependencies]
+opendal = { version = "0.57", features = ["services-s3"] }
+```
+
+## Useful Links
+
+- **API reference**: [docs.rs/opendal][docs.rs] (release) · [dev build][dev-docs]
+- **Services & configuration**: [/services](/services)
+- **Upgrade guide**: [docs.rs][Upgrade Guide]
+- **Changelog**: [docs.rs][Changelog]
+- **Source & examples**: [`core/`][source] · [`core/examples/`][examples]
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — a runnable program, then a real backend.
+2. [Connecting to your storage](./03-connecting.md) — build an operator for any service.
+3. [Common tasks](./04-tasks.md) — read, write, stream, list, presign, and more.
+4. [Going to production](./05-production.md) — retry, timeouts, errors, observability.
+
+[docs.rs]: https://docs.rs/opendal/
+[dev-docs]: https://opendal.apache.org/docs/rust/opendal/
+[Upgrade Guide]: https://docs.rs/opendal/latest/opendal/docs/upgrade/index.html
+[Changelog]: https://docs.rs/opendal/latest/opendal/docs/changelog/index.html
+[source]: https://github.com/apache/opendal/tree/main/core
+[examples]: https://github.com/apache/opendal/tree/main/core/examples
diff --git a/website/docs/10-core/02-getting-started.md b/website/docs/10-core/02-getting-started.md
new file mode 100644
index 000000000..29d8b85bf
--- /dev/null
+++ b/website/docs/10-core/02-getting-started.md
@@ -0,0 +1,103 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Run your first OpenDAL program in Rust, then point it at a real storage backend.
+---
+
+# Getting started
+
+## Your first program
+
+This program builds an operator, writes a file, reads it back, inspects its
+metadata, and deletes it. It runs against the in-memory service with no
+credentials, so you can paste it into a fresh project and run it as-is.
+
+```toml
+# Cargo.toml
+[dependencies]
+opendal = "0.57"
+tokio = { version = "1", features = ["full"] }
+```
+
+```rust
+use opendal::services;
+use opendal::Operator;
+use opendal::Result;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    // Configure a service, then build an operator from it.
+    let op = Operator::new(services::Memory::default())?;
+
+    // The same verbs work on every service.
+    op.write("hello.txt", "Hello, World!").await?;
+
+    let bytes = op.read("hello.txt").await?;
+    println!("read {} bytes", bytes.len());
+
+    let meta = op.stat("hello.txt").await?;
+    println!("size = {} bytes", meta.content_length());
+
+    op.delete("hello.txt").await?;
+    Ok(())
+}
+```
+
+## Point it at a real backend
+
+Only the service changes; the operations stay identical. To use S3, enable its
+feature and configure the builder:
+
+```toml
+opendal = { version = "0.57", features = ["services-s3"] }
+```
+
+```rust
+use opendal::services;
+use opendal::Operator;
+
+let op = Operator::new(
+    services::S3::default()
+        .bucket("my-bucket")
+        .region("us-east-1"),
+)?;
+
+op.write("hello.txt", "Hello from S3!").await?;
+```
+
+Each service is gated behind its own `services-*` feature, so enable the ones
+you use. The next page, [Connecting to your storage](./03-connecting.md), covers
+the construction options and credentials in depth; [Services](/services) lists
+every backend and its configuration keys.
+
+## Blocking vs async {#blocking-vs-async}
+
+OpenDAL is async-first. [`Operator`] exposes `async` methods and needs an async
+runtime (Tokio by default).
+
+For synchronous code, enable the `blocking` feature and use
+[`blocking::Operator`]. Build it from an async operator, then call the same verbs
+without `.await`:
+
+```toml
+opendal = { version = "0.57", features = ["services-memory", "blocking"] }
+```
+
+```rust
+use opendal::blocking;
+use opendal::services;
+use opendal::Operator;
+
+// Build the async operator first, outside any async runtime.
+let op = Operator::new(services::Memory::default())?;
+let bop = blocking::Operator::new(op)?;
+
+bop.write("hello.txt", "Hello, World!")?;
+let bytes = bop.read("hello.txt")?;
+```
+
+The blocking operator drives the async one on a managed runtime, so construct it
+outside of an existing async context.
+
+[`Operator`]: https://docs.rs/opendal/latest/opendal/struct.Operator.html
+[`blocking::Operator`]: https://docs.rs/opendal/latest/opendal/blocking/struct.Operator.html
diff --git a/website/docs/10-core/03-connecting.md b/website/docs/10-core/03-connecting.md
new file mode 100644
index 000000000..a0006c0e3
--- /dev/null
+++ b/website/docs/10-core/03-connecting.md
@@ -0,0 +1,98 @@
+---
+title: Connecting to your storage
+sidebar_label: Connecting to your storage
+description: Build an OpenDAL operator for any backend — typed builders, key-value config, URIs, credentials, and feature flags.
+---
+
+# Connecting to your storage
+
+Switching backends is a configuration change, not a code change. This page
+covers the ways to build an operator and the things that trip people up:
+feature flags and credentials. For the configuration keys of a specific service,
+see [Services](/services).
+
+## Enable the service feature
+
+Every service is behind a `services-*` Cargo feature. Enable the ones you use:
+
+```toml
+[dependencies]
+opendal = { version = "0.57", features = ["services-s3", "services-gcs"] }
+```
+
+The in-memory service is always available; every other service needs its
+`services-*` feature. Forgetting the feature is the most common reason a service
+"doesn't exist" at compile time.
+
+## Build an operator
+
+### From a typed builder (recommended)
+
+Each service has a builder with checked, discoverable options:
+
+```rust
+use opendal::services;
+use opendal::Operator;
+
+let op = Operator::new(
+    services::S3::default()
+        .bucket("my-bucket")
+        .region("us-east-1")
+        .endpoint("https://s3.amazonaws.com"),
+)?;
+```
+
+A local filesystem operator roots every path under one directory:
+
+```rust
+let op = Operator::new(services::Fs::default().root("/tmp/opendal"))?;
+```
+
+### From a key-value map
+
+When configuration comes from a file or environment, build from key-value pairs
+instead of naming each setter. The keys are the same as the builder fields:
+
+```rust
+use std::collections::HashMap;
+use opendal::services;
+use opendal::Operator;
+
+let cfg = HashMap::from([
+    ("bucket".to_string(), "my-bucket".to_string()),
+    ("region".to_string(), "us-east-1".to_string()),
+]);
+
+let op = Operator::from_iter::<services::S3>(cfg)?;
+```
+
+### From a URI
+
+Registered services can be constructed from a URI, with the scheme selecting the
+backend at runtime:
+
+```rust
+use opendal::Operator;
+
+let op = Operator::from_uri("memory://")?;
+```
+
+Use `Operator::via_iter(scheme, pairs)` for the same runtime-selected
+construction with explicit key-value config.
+
+## Credentials
+
+Credentials are just more configuration keys. Set them on the builder (for
+example S3's `access_key_id` / `secret_access_key`), or supply them through the
+key-value map above. Some services can also load credentials from their
+platform's default sources — see each service's page under [Services](/services)
+for the exact keys and credential behavior.
+
+Avoid hard-coding secrets in source. Read them from the environment or a secret
+manager and pass them in when you build the operator.
+
+## One operator per service and root
+
+An operator maps to one service and one root path. To work with two buckets or
+two roots, build two operators — they are cheap, lightweight handles that are
+safe to share across threads and tasks.
diff --git a/website/docs/10-core/04-tasks.md b/website/docs/10-core/04-tasks.md
new file mode 100644
index 000000000..eb5f89447
--- /dev/null
+++ b/website/docs/10-core/04-tasks.md
@@ -0,0 +1,157 @@
+---
+title: Common tasks
+sidebar_label: Common tasks
+description: Task-oriented recipes for OpenDAL in Rust — read, write, stream, list, delete, copy, and presign.
+---
+
+# Common tasks
+
+Recipes for the things you actually do with storage. Each works on every
+service. They assume an `op: Operator` built as in
+[Getting started](./02-getting-started.md). For full method signatures and
+options, follow the links to the [API reference][operator].
+
+A few conventions used throughout:
+
+- Paths are relative to the operator's root; a trailing `/` means a directory
+  (`logs/app/` is a directory, `logs/app` is a file).
+- Reads return a [`Buffer`]; writes accept anything that converts into one
+  (`&str`, `String`, `Vec<u8>`, `Bytes`).
+- Most verbs have a `*_with` companion (`read_with`, `write_with`, …) for extra
+  options like ranges, content type, and concurrency.
+
+## Read a whole file
+
+```rust
+let bytes = op.read("path/to/file").await?;
+let text = String::from_utf8(bytes.to_vec())?;
+```
+
+## Read part of a file
+
+```rust
+let bytes = op.read_with("path/to/file").range(0..1024).await?;
+```
+
+## Stream a large file
+
+Don't buffer gigabytes in memory — stream chunks instead:
+
+```rust
+use futures::TryStreamExt;
+
+let mut stream = op.reader("big.bin").await?.into_bytes_stream(..).await?;
+while let Some(chunk) = stream.try_next().await? {
+    // process chunk (bytes::Bytes)
+}
+```
+
+## Write a whole file
+
+```rust
+let _meta = op.write("path/to/file", "Hello, World!").await?;
+let _meta = op.write("path/to/file", vec![0u8; 1024]).await?;
+```
+
+## Stream a large upload
+
+Use a [`Writer`] for data produced incrementally. Call `write` repeatedly, then
+`close` to commit (use `abort` to discard):
+
+```rust
+let mut writer = op.writer("big.bin").await?;
+writer.write(first_chunk).await?;
+writer.write(second_chunk).await?;
+let _meta = writer.close().await?;
+```
+
+## Upload concurrently
+
+For large objects on services with multipart support, upload parts in parallel:
+
+```rust
+let mut writer = op.writer_with("big.bin").concurrent(8).await?;
+writer.write(data).await?;
+writer.close().await?;
+```
+
+## Check existence and metadata
+
+```rust
+if op.exists("path/to/file").await? {
+    let meta = op.stat("path/to/file").await?;
+    println!("{} bytes, dir = {}", meta.content_length(), meta.is_dir());
+}
+```
+
+## List a directory
+
+`list` returns the direct children of a directory:
+
+```rust
+for entry in op.list("dir/").await? {
+    println!("{} ({:?})", entry.path(), entry.metadata().mode());
+}
+```
+
+For large directories, stream entries with a [`Lister`] instead of collecting
+them into a `Vec`:
+
+```rust
+use futures::TryStreamExt;
+
+let mut lister = op.lister("dir/").await?;
+while let Some(entry) = lister.try_next().await? {
+    println!("{}", entry.path());
+}
+```
+
+## Walk a tree recursively
+
+```rust
+let entries = op.list_with("dir/").recursive(true).await?;
+```
+
+`lister_with("dir/").recursive(true)` gives the streaming equivalent.
+
+## Delete a file or a whole tree
+
+```rust
+op.delete("path/to/file").await?;   // single path; idempotent
+op.remove_all("dir/").await?;       // a path and everything under it
+```
+
+`delete` succeeds even if the path does not exist.
+
+## Create a directory
+
+```rust
+op.create_dir("path/to/dir/").await?;   // the trailing slash is required
+```
+
+## Copy and rename
+
+```rust
+let _meta = op.copy("from.txt", "to.txt").await?;
+op.rename("old.txt", "new.txt").await?;
+```
+
+Both operate within a single operator and require a service that supports them;
+see [capability checks](./05-production.md#capability-checks).
+
+## Generate a presigned URL
+
+Hand a time-limited URL to a third party so they can access an object without
+your credentials:
+
+```rust
+use std::time::Duration;
+
+let req = op.presign_read("path/to/file", Duration::from_secs(3600)).await?;
+// req.method(), req.uri(), req.header() describe the HTTP request to make
+```
+
+[operator]: https://docs.rs/opendal/latest/opendal/struct.Operator.html
+[`Buffer`]: https://docs.rs/opendal/latest/opendal/struct.Buffer.html
+[`Writer`]: https://docs.rs/opendal/latest/opendal/struct.Writer.html
+[`Lister`]: https://docs.rs/opendal/latest/opendal/struct.Lister.html
diff --git a/website/docs/10-core/05-production.md b/website/docs/10-core/05-production.md
new file mode 100644
index 000000000..4039cc1cc
--- /dev/null
+++ b/website/docs/10-core/05-production.md
@@ -0,0 +1,89 @@
+---
+title: Going to production
+sidebar_label: Going to production
+description: Make OpenDAL robust in production — retries, timeouts, concurrency limits, error handling, and observability.
+---
+
+# Going to production
+
+The basics read and write data. Production code also has to survive transient
+failures, bound its resource use, observe what happens, and react to errors.
+OpenDAL handles the first three with **layers** and gives you typed **errors**
+and **capability checks** for the rest.
+
+## Layers
+
+A layer wraps an operator to add cross-cutting behavior without touching your
+storage code. Apply one with `.layer()`; it returns a new operator. Layers nest
+like an onion, and the **last** layer applied is the **outermost** — it sees the
+request first and the response last. A robust order is logging on the outside,
+retry on the inside, so each retry attempt is logged:
+
+```rust
+use std::time::Duration;
+use opendal::layers::{ConcurrentLimitLayer, LoggingLayer, RetryLayer, TimeoutLayer};
+
+let op = Operator::new(builder)?
+    .layer(RetryLayer::new().with_max_times(3))      // retry transient failures
+    .layer(TimeoutLayer::new().with_timeout(Duration::from_secs(10))) // bound slow calls
+    .layer(ConcurrentLimitLayer::new(16))            // cap in-flight requests
+    .layer(LoggingLayer::default());                 // log every attempt (outermost)
+```
+
+These four ship in the default features. See [Concepts](../03-concepts.mdx#layer)
+for the model.
+
+## Observability
+
+`LoggingLayer` (above) emits logs through the `log` crate. For metrics and
+tracing, enable the matching feature and apply the layer the same way:
+
+| Feature | Layer | Use it for |
+|---------|-------|------------|
+| `layers-metrics` | `MetricsLayer` | `metrics`-crate counters and histograms |
+| `layers-prometheus` | `PrometheusLayer` | Prometheus metrics |
+| `layers-tracing` | `TracingLayer` | `tracing` spans |
+| `layers-fastrace` | `FastraceLayer` | distributed tracing with fastrace |
+| `layers-otel-trace` | `OtelTraceLayer` | OpenTelemetry traces |
+
+See the [`layers` module][layers] for the full list and each layer's options.
+
+## Error handling
+
+Every fallible call returns `opendal::Result<T>`, an alias for
+`Result<T, opendal::Error>`. Match on [`ErrorKind`] to branch on what went wrong
+instead of parsing messages:
+
+```rust
+use opendal::ErrorKind;
+
+match op.read("maybe-missing.txt").await {
+    Ok(bytes) => { /* ... */ }
+    Err(err) if err.kind() == ErrorKind::NotFound => { /* handle absence */ }
+    Err(err) => return Err(err),
+}
+```
+
+Common kinds include `NotFound`, `PermissionDenied`, `AlreadyExists`,
+`RateLimited`, and `Unsupported`. Note that `delete` is idempotent — deleting a
+missing path succeeds rather than returning `NotFound`.
+
+## Capability checks {#capability-checks}
+
+Not every service supports every operation. Query what a backend can do with
+[`Capability`] before calling optional operations like `copy`, `rename`, or
+presign:
+
+```rust
+let cap = op.info().capability();
+if cap.copy {
+    op.copy("a.txt", "b.txt").await?;
+}
+```
+
+Calling an unsupported operation returns an error with `ErrorKind::Unsupported`,
+so capability checks are an optimization, not a requirement for safety.
+
+[layers]: https://docs.rs/opendal/latest/opendal/layers/index.html
+[`ErrorKind`]: https://docs.rs/opendal/latest/opendal/enum.ErrorKind.html
+[`Capability`]: https://docs.rs/opendal/latest/opendal/struct.Capability.html
diff --git a/website/docs/20-bindings/_category_.yml b/website/docs/20-bindings/_category_.yml
deleted file mode 100644
index f22424ab5..000000000
--- a/website/docs/20-bindings/_category_.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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.
-
-label: "Bindings"
-collapsible: true
-collapsed: false
-link:
-  type: generated-index
-  title: Bindings Overview
diff --git a/website/docs/20-bindings/c.mdx b/website/docs/20-bindings/c.mdx
deleted file mode 100644
index 79f863426..000000000
--- a/website/docs/20-bindings/c.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: C 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/c/README.md';
-
-<GitHubReadme basePath="bindings/c/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/c/01-overview.md b/website/docs/20-bindings/c/01-overview.md
new file mode 100644
index 000000000..7954fff87
--- /dev/null
+++ b/website/docs/20-bindings/c/01-overview.md
@@ -0,0 +1,73 @@
+---
+title: C
+sidebar_label: Overview
+slug: /bindings/c
+description: The OpenDAL C binding — experimental, built from source, blocking API over 50+ storage backends.
+---
+
+# Apache OpenDAL™ C Binding
+
+A C binding for OpenDAL, giving C programs blocking access to S3, GCS, Azure Blob,
+the local filesystem, and [50+ more services](/services) through one API backed by
+the Rust core.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+:::caution Experimental
+This binding is **experimental and a work in progress**. The API surface is limited,
+there are no pre-built packages, and breaking changes may occur between releases.
+It must be built from source.
+:::
+
+## Status
+
+Unreleased and experimental. The C binding exposes a blocking-only API generated
+via `cbindgen` from the Rust core. There is no async interface and no package
+registry distribution; you must compile the shared library yourself.
+
+## Capabilities
+
+- **Blocking operations** — write, read, stat, delete, copy, rename, list, and
+  create-dir through a single `opendal_operator *`.
+- **Streaming reader and writer** — `opendal_reader` and `opendal_writer` for
+  chunk-at-a-time I/O with seek support on the reader.
+- **All services via build flags** — enable individual backends with `-DFEATURES`
+  at CMake time.
+- **C11 compatible** — the header `opendal.h` requires C11 or later (and a C++14
+  compiler for the Rust-generated glue).
+- **Explicit memory ownership** — every heap-allocated value has a matching
+  `*_free` function; results carry an `opendal_error *` that is `NULL` on success.
+
+## Building
+
+Requirements: a C11/C++14 compiler, CMake, and a Rust toolchain.
+
+```shell
+cd bindings/c
+mkdir -p build && cd build
+
+# Build with the default memory + fs services
+cmake ..
+make
+
+# Enable additional services
+cmake .. -DFEATURES="opendal/services-s3,opendal/services-gcs"
+make
+```
+
+The shared library lands in `../../target/debug/` and the header is at
+`bindings/c/include/opendal.h`.
+
+## Useful Links
+
+- **User guide**: [opendal.apache.org/docs/bindings/c](https://opendal.apache.org/docs/bindings/c)
+- **Services & configuration**: [/services](/services)
+- **Source & examples**: [`bindings/c/`](https://github.com/apache/opendal/tree/main/bindings/c)
+- **Header file**: [`bindings/c/include/opendal.h`](https://github.com/apache/opendal/blob/main/bindings/c/include/opendal.h)
+- **Contributing**: [`CONTRIBUTING.md`](https://github.com/apache/opendal/blob/main/CONTRIBUTING.md)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — build an operator, write and read
+   data, handle errors, and free resources.
diff --git a/website/docs/20-bindings/c/02-getting-started.md b/website/docs/20-bindings/c/02-getting-started.md
new file mode 100644
index 000000000..f3ba1c5c5
--- /dev/null
+++ b/website/docs/20-bindings/c/02-getting-started.md
@@ -0,0 +1,149 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Build an OpenDAL operator in C, write and read data, check errors, and free resources.
+---
+
+# Getting started
+
+## Build the library
+
+From `bindings/c/`:
+
+```shell
+mkdir -p build && cd build
+cmake ..
+make
+```
+
+This produces `libopendal_c` under `../../target/debug/`. Link against it and
+include `bindings/c/include/opendal.h`.
+
+## Your first program
+
+This program builds a memory-backed operator, writes data, reads it back,
+inspects the metadata, and frees every heap-allocated value. It requires no
+credentials and no filesystem access.
+
+```c
+#include <assert.h>
+#include <stdio.h>
+#include "opendal.h"
+
+int main(void)
+{
+    /* Build an operator for the "memory" service with no options. */
+    opendal_result_operator_new result = opendal_operator_new("memory", NULL);
+    assert(result.op != NULL);
+    assert(result.error == NULL);
+    opendal_operator *op = result.op;
+
+    /* Write bytes to a path. opendal_operator_write consumes the bytes. */
+    const char *msg = "Hello, OpenDAL!";
+    opendal_bytes data = {
+        .data = (uint8_t *)msg,
+        .len  = 15,
+    };
+    opendal_error *err = opendal_operator_write(op, "/hello.txt", &data);
+    assert(err == NULL);
+
+    /* Read the bytes back. */
+    opendal_result_read r = opendal_operator_read(op, "/hello.txt");
+    assert(r.error == NULL);
+    assert(r.data.len == 15);
+    printf("%.*s\n", (int)r.data.len, r.data.data);  /* Hello, OpenDAL! */
+    opendal_bytes_free(&r.data);
+
+    /* Stat the path to get metadata. */
+    opendal_result_stat s = opendal_operator_stat(op, "/hello.txt");
+    assert(s.error == NULL);
+    printf("size = %llu\n", (unsigned long long)opendal_metadata_content_length(s.meta));
+    opendal_metadata_free(s.meta);
+
+    /* Delete the file. */
+    opendal_error *del_err = opendal_operator_delete(op, "/hello.txt");
+    assert(del_err == NULL);
+
+    opendal_operator_free(op);
+    return 0;
+}
+```
+
+Every operation returns either an `opendal_error *` (NULL on success) or a
+result struct whose `.error` field is NULL on success. See
+[Error handling](#error-handling) below.
+
+## Point it at a real backend
+
+The only change is the scheme and the options you pass to `opendal_operator_new`.
+Operations stay identical across all services.
+
+```c
+/* Filesystem operator rooted at /tmp/mydata */
+opendal_operator_options *opts = opendal_operator_options_new();
+opendal_operator_options_set(opts, "root", "/tmp/mydata");
+
+opendal_result_operator_new result = opendal_operator_new("fs", opts);
+opendal_operator_options_free(opts);   /* options are copied; free immediately */
+
+assert(result.op != NULL);
+opendal_operator *op = result.op;
+/* ... same write/read/stat/delete calls ... */
+opendal_operator_free(op);
+```
+
+For S3 or any other service, set the configuration keys documented on
+[/services](/services) as key-value pairs via `opendal_operator_options_set`.
+
+## Error handling
+
+C has no exceptions. Every function that can fail returns either a bare
+`opendal_error *` or a result struct with an `.error` field:
+
+- `NULL` → success.
+- non-NULL → failure; inspect `.code` (an `opendal_code` enum) and `.message`
+  (an `opendal_bytes` containing the error text, **not** NUL-terminated).
+  Call `opendal_error_free` when done.
+
+```c
+opendal_result_read r = opendal_operator_read(op, "/no-such-file");
+if (r.error != NULL) {
+    /* .message is NOT NUL-terminated; use the length. */
+    fprintf(stderr, "error %d: %.*s\n",
+            r.error->code,
+            (int)r.error->message.len,
+            r.error->message.data);
+    opendal_error_free(r.error);
+    /* r.data is invalid when error != NULL */
+}
+```
+
+Common error codes (from `opendal_code`):
+
+| Code | Meaning |
+|------|---------|
+| `OPENDAL_NOT_FOUND` | Path does not exist |
+| `OPENDAL_PERMISSION_DENIED` | Insufficient permissions |
+| `OPENDAL_ALREADY_EXISTS` | Path already exists |
+| `OPENDAL_UNSUPPORTED` | Operation not supported by this service |
+| `OPENDAL_CONFIG_INVALID` | Operator configuration is invalid |
+
+## Memory ownership rules
+
+Every heap-allocated value returned by OpenDAL must be freed by the caller:
+
+| Type | Free with |
+|------|-----------|
+| `opendal_operator *` | `opendal_operator_free` |
+| `opendal_bytes` (field, not pointer) | `opendal_bytes_free` (pass by address) |
+| `opendal_error *` | `opendal_error_free` |
+| `opendal_metadata *` | `opendal_metadata_free` |
+| `opendal_lister *` | `opendal_lister_free` |
+| `opendal_entry *` | `opendal_entry_free` |
+| `opendal_reader *` | `opendal_reader_free` |
+| `opendal_writer *` | `opendal_writer_free` |
+| `char *` returned by metadata / entry / info accessors | `opendal_string_free` |
+| `opendal_operator_options *` | `opendal_operator_options_free` |
+
+Do not use `free()` directly on any of these; they are allocated inside the Rust
+runtime and must be released through the provided functions.
diff --git a/website/docs/20-bindings/cpp.mdx b/website/docs/20-bindings/cpp.mdx
deleted file mode 100644
index ad4a11b80..000000000
--- a/website/docs/20-bindings/cpp.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Cpp 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/cpp/README.md';
-
-<GitHubReadme basePath="bindings/cpp/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/cpp/01-overview.md b/website/docs/20-bindings/cpp/01-overview.md
new file mode 100644
index 000000000..6aaee5d63
--- /dev/null
+++ b/website/docs/20-bindings/cpp/01-overview.md
@@ -0,0 +1,87 @@
+---
+title: C++
+sidebar_label: Overview
+slug: /bindings/cpp
+description: The OpenDAL C++ binding — experimental/WIP, built from source via CMake.
+---
+
+# Apache OpenDAL™ C++ Binding
+
+A C++ binding for OpenDAL: access S3, GCS, Azure Blob, HDFS, the local
+filesystem, and [50+ more services](/services) through one API, backed by the
+Rust core.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, and operation are the same four ideas in every language.
+
+:::warning Experimental
+This binding is **experimental and a work in progress**. There is no published
+package; you must build from source with CMake. The API may change without
+notice between releases. It is not yet recommended for production use.
+:::
+
+## Status
+
+Unreleased. Built from source only. Only **Clang** and **AppleClang** are
+currently supported as compilers; MSVC and GCC are not yet tested.
+
+## Capabilities
+
+- **Sync API** (`opendal.hpp`) — `opendal::Operator` with read, write, stat,
+  list, copy, rename, remove, and directory creation.
+- **Async API** (`opendal_async.hpp`, opt-in via `-DOPENDAL_ENABLE_ASYNC=ON`,
+  requires C++20) — `opendal::async::Operator` exposing the same operations
+  as futures.
+- **Streaming** — `Reader` / `ReaderStream` for large-object reads without an
+  intermediate copy; `Lister` / `Lister::Iterator` for directory traversal.
+- **Capability introspection** — `Operator::Info()` returns a `Capability`
+  struct so you can check at runtime which features a backend supports.
+- **Services** controlled at build time via the `OPENDAL_FEATURES` CMake
+  variable (e.g. `"opendal/services-s3,opendal/services-memory"`).
+
+## Integration via CMake
+
+### FetchContent (recommended)
+
+```cmake
+include(FetchContent)
+
+FetchContent_Declare(
+  opendal-cpp
+  GIT_REPOSITORY https://github.com/apache/opendal.git
+  GIT_TAG        v0.40.0
+  SOURCE_SUBDIR  bindings/cpp
+)
+FetchContent_MakeAvailable(opendal-cpp)
+
+target_link_libraries(your_target opendal_cpp)
+```
+
+### Vendored source
+
+```shell
+mkdir third_party && cd third_party
+git clone https://github.com/apache/opendal.git
+cd opendal && git checkout v0.40.0
+```
+
+```cmake
+add_subdirectory(third_party/opendal/bindings/cpp)
+target_link_libraries(your_target opendal_cpp)
+```
+
+### Prerequisites
+
+- CMake ≥ 3.22
+- Clang or AppleClang with C++17 support (C++20 required for async)
+
+## Useful Links
+
+- **User guide**: [opendal.apache.org/docs/bindings/cpp](https://opendal.apache.org/docs/bindings/cpp)
+- **Services & configuration**: [/services](/services)
+- **Source**: [`bindings/cpp/`](https://github.com/apache/opendal/tree/main/bindings/cpp)
+- **Examples**: [`examples/cpp/`](https://github.com/apache/opendal/tree/main/examples/cpp)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — a runnable program, then a real backend.
diff --git a/website/docs/20-bindings/cpp/02-getting-started.md b/website/docs/20-bindings/cpp/02-getting-started.md
new file mode 100644
index 000000000..6a2265b52
--- /dev/null
+++ b/website/docs/20-bindings/cpp/02-getting-started.md
@@ -0,0 +1,197 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Build and run your first OpenDAL C++ program, then connect it to a real storage backend.
+---
+
+# Getting started
+
+## Build the binding
+
+Prerequisites: CMake ≥ 3.22, Clang or AppleClang, Rust toolchain (the binding
+compiles the Rust core as part of the build).
+
+```shell
+cd bindings/cpp
+mkdir build && cd build
+cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ ..
+make
+```
+
+For development (enables tests and docs):
+
+```shell
+cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
+      -DOPENDAL_DEV=ON ..
+make
+```
+
+## Your first program
+
+This program constructs an operator against the in-memory service (no
+credentials needed), writes data, reads it back, and inspects its metadata.
+
+```cpp
+#include "opendal.hpp"
+#include <iostream>
+#include <string>
+
+int main() {
+    // Construct an operator for the "memory" service.
+    opendal::Operator op("memory");
+
+    // Write a string.
+    op.Write("hello.txt", "Hello, OpenDAL!");
+
+    // Read it back as a std::string.
+    std::string data = op.Read("hello.txt");
+    std::cout << data << "\n";  // Hello, OpenDAL!
+
+    // Inspect metadata.
+    opendal::Metadata meta = op.Stat("hello.txt");
+    std::cout << "size=" << meta.ContentLength() << "\n";
+    std::cout << "is_file=" << meta.IsFile() << "\n";
+
+    // Check existence.
+    std::cout << "exists=" << op.Exists("hello.txt") << "\n";
+
+    // Remove.
+    op.Remove("hello.txt");
+    std::cout << "after remove, exists=" << op.Exists("hello.txt") << "\n";
+}
+```
+
+Compile and run (assuming the binding has been installed into your build tree):
+
+```shell
+clang++ -std=c++17 -I/path/to/opendal/bindings/cpp/include \
+        -L/path/to/build -lopendal_cpp \
+        hello.cpp -o hello
+./hello
+```
+
+## Point it at a real backend
+
+Only the scheme and config map change; every operation is identical. Pass
+backend options as a `std::unordered_map<std::string, std::string>`:
+
+```cpp
+#include "opendal.hpp"
+#include <iostream>
+
+int main() {
+    opendal::Operator op("s3", {
+        {"bucket", "my-bucket"},
+        {"region", "us-east-1"},
+    });
+
+    op.Write("hello.txt", "Hello from S3!");
+    std::string data = op.Read("hello.txt");
+    std::cout << data << "\n";
+}
+```
+
+The configuration keys match the per-service docs at [/services](/services).
+You must enable the service at CMake configure time via `OPENDAL_FEATURES`:
+
+```cmake
+cmake -DOPENDAL_FEATURES="opendal/services-s3" ...
+```
+
+## Streaming reads
+
+For large objects, use `GetReader` to avoid an unnecessary copy into a
+`std::string`. `ReaderStream` wraps the reader as a standard `std::istream`:
+
+```cpp
+#include "opendal.hpp"
+#include <iostream>
+#include <string>
+
+int main() {
+    opendal::Operator op("memory");
+    op.Write("data.bin", "some large content");
+
+    // Read in chunks via Reader.
+    {
+        auto reader = op.GetReader("data.bin");
+        char buf[64];
+        std::streamsize n = reader.Read(buf, sizeof(buf));
+        std::cout.write(buf, n);
+        std::cout << "\n";
+    }
+
+    // Or wrap as a standard istream.
+    {
+        opendal::ReaderStream stream(op.GetReader("data.bin"));
+        std::string token;
+        stream >> token;  // reads first whitespace-delimited token
+        std::cout << token << "\n";
+    }
+}
+```
+
+## Listing a directory
+
+`Operator::List` returns all entries at once. `GetLister` gives a lazy
+iterator you can loop over:
+
+```cpp
+#include "opendal.hpp"
+#include <iostream>
+
+int main() {
+    opendal::Operator op("memory");
+    op.Write("dir/a.txt", "a");
+    op.Write("dir/b.txt", "b");
+
+    // Eager list — returns std::vector<opendal::Entry>.
+    auto entries = op.List("dir/");
+    for (const auto& entry : entries) {
+        std::cout << entry.path << "\n";
+    }
+
+    // Lazy lister — useful for large directories.
+    auto lister = op.GetLister("dir/");
+    for (const auto& entry : lister) {
+        std::cout << entry.path << "\n";
+    }
+}
+```
+
+## Async API
+
+The async API is a separate opt-in header that requires C++20 and Clang/AppleClang.
+Enable it at configure time:
+
+```shell
+cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
+      -DOPENDAL_ENABLE_ASYNC=ON ..
+```
+
+Then use `opendal::async::Operator` from `opendal_async.hpp`. Operations return
+Rust futures which you `.await` or block on via your async runtime:
+
+```cpp
+#include "opendal_async.hpp"
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+int main() {
+    opendal::async::Operator op("memory");
+
+    // Write requires a std::span<uint8_t> as the second argument.
+    std::string_view content = "Hello, OpenDAL!";
+    std::vector<uint8_t> buf(content.begin(), content.end());
+    auto write_fut = op.Write("hello.txt", std::span<uint8_t>(buf));
+    // ... await write_fut ...
+
+    auto read_fut = op.Read("hello.txt");
+    // ... await read_fut ...
+}
+```
+
+The async API is more experimental than the sync one. Consult
+[`bindings/cpp/include/opendal_async.hpp`](https://github.com/apache/opendal/blob/main/bindings/cpp/include/opendal_async.hpp)
+for the current method signatures.
diff --git a/website/docs/20-bindings/d.mdx b/website/docs/20-bindings/d.mdx
deleted file mode 100644
index d78398308..000000000
--- a/website/docs/20-bindings/d.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: D 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/d/README.md';
-
-<GitHubReadme basePath="bindings/d/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/d/01-overview.md b/website/docs/20-bindings/d/01-overview.md
new file mode 100644
index 000000000..dae1be357
--- /dev/null
+++ b/website/docs/20-bindings/d/01-overview.md
@@ -0,0 +1,98 @@
+---
+title: D
+sidebar_label: Overview
+slug: /bindings/d
+description: The OpenDAL D binding — early-stage and experimental, built from source on top of the C binding.
+---
+
+# Apache OpenDAL™ D Binding
+
+A D language binding for OpenDAL, wrapping the [C binding](../c) to give D programs
+access to the same unified storage API as every other OpenDAL binding.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, and operation are the same four ideas in every language.
+
+:::warning
+This binding is **early-stage and experimental**. It is not yet released or
+published to a package registry. The API surface is minimal, the build requires
+building from source, and breaking changes can happen at any time.
+:::
+
+## Status
+
+Unreleased. Must be built from source. Suitable for experimentation and
+contribution, not for production use.
+
+## Capabilities
+
+The current API exposes a single `Operator` struct with the following methods:
+
+| Method | Description |
+|---|---|
+| `write(path, data)` | Write `ubyte[]` to `path` |
+| `read(path)` | Return `ubyte[]` from `path` |
+| `remove(path)` | Delete the object at `path` |
+| `exists(path)` | Return `bool` indicating whether `path` exists |
+| `stat(path)` | Return a `Metadata` struct for `path` |
+| `list(path)` | Return an `Entry[]` listing the directory at `path` |
+| `createDir(path)` | Create a directory at `path` |
+| `rename(src, dest)` | Rename / move `src` to `dest` |
+| `copy(src, dest)` | Copy `src` to `dest` |
+
+Three operations have parallel variants: `writeParallel`, `readParallel`,
+and `listParallel`. Each dispatches the call to a `std.parallelism.TaskPool`.
+Parallel variants are enabled by passing `useParallel = true` to the
+`Operator` constructor.
+
+`Metadata` exposes `contentLength()`, `isFile()`, `isDir()`, and
+`lastModified()`. `Entry` exposes `path()` and `name()`.
+
+Most methods are `@trusted` wrappers around the underlying C binding.
+`writeParallel` is `@safe`. `Entry.path()` and `Entry.name()` carry no
+safety attribute (default `@system`) because they call into the C binding
+without a `@trusted` annotation. There is no async API at this stage.
+
+## Building from Source
+
+You need:
+
+- A D compiler: [dmd, ldc, or gdc](https://dlang.org/download)
+- Rust and Cargo (to build the underlying C library)
+
+```shell
+# From the bindings/d/ directory
+
+# Build libopendal_c and the D library (release mode)
+OPENDAL_TEST=memory dub build -b release
+
+# Build and run unit tests against the memory backend
+OPENDAL_TEST=memory dub test
+```
+
+The pre-build script (`build.d`) reads `OPENDAL_TEST` at **build time** to
+decide which Cargo `--features` (i.e. which storage backends) get compiled
+into the C library. The resulting static library is copied into `lib/` before
+`dub` links the D code. Omitting `OPENDAL_TEST` builds only core
+functionality with no storage backends.
+
+At runtime the test suite also reads `OPENDAL_TEST` to select which backend
+scheme to pass to `Operator(...)`. The runtime backend is constructed with
+`Operator(scheme, options)` where `options` holds service-specific keys such
+as `root`.
+
+To test against a service other than `memory`, set `OPENDAL_TEST` and the
+matching `OPENDAL_{SERVICE}_*` configuration variables:
+
+```shell
+OPENDAL_TEST=fs OPENDAL_FS_ROOT=/tmp/test dub test
+```
+
+## Useful Links
+
+- **Source**: [`bindings/d/`](https://github.com/apache/opendal/tree/main/bindings/d)
+- **Services & configuration**: [/services](/services)
+
+## Next Steps
+
+- [Getting started](./02-getting-started.md) — write, read, and list with the memory backend.
diff --git a/website/docs/20-bindings/d/02-getting-started.md b/website/docs/20-bindings/d/02-getting-started.md
new file mode 100644
index 000000000..b25c39fce
--- /dev/null
+++ b/website/docs/20-bindings/d/02-getting-started.md
@@ -0,0 +1,124 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Write, read, and list with the OpenDAL D binding against the in-memory backend.
+---
+
+# Getting started
+
+## Prerequisites
+
+Build the D binding from source before running any code. From `bindings/d/`:
+
+```shell
+OPENDAL_TEST=memory dub build -b release
+```
+
+See [Overview](./01-overview.md) for the full build requirements.
+
+## Your first program
+
+The example below creates an `Operator` against the in-memory backend (no
+credentials needed), writes a file, reads it back, checks that it exists,
+inspects its metadata, and deletes it.
+
+```d
+import opendal;
+import std.stdio : writeln;
+
+void main() @safe
+{
+    // Build an OperatorOptions, then construct the Operator.
+    auto options = new OperatorOptions();
+    auto op = Operator("memory", options);
+
+    // Write bytes to a path.
+    string data = "Hello, OpenDAL!";
+    op.write("hello.txt", cast(ubyte[]) data.dup);
+
+    // Read them back.
+    ubyte[] result = op.read("hello.txt");
+    writeln(cast(string) result.idup);   // Hello, OpenDAL!
+
+    // Inspect metadata.
+    auto meta = op.stat("hello.txt");
+    writeln("size = ", meta.contentLength(), " bytes");
+    writeln("isFile = ", meta.isFile());
+
+    // Check existence, then delete.
+    assert(op.exists("hello.txt"));
+    op.remove("hello.txt");
+    assert(!op.exists("hello.txt"));
+}
+```
+
+## Listing a directory
+
+`createDir` and `list` work together:
+
+```d
+import opendal;
+import std.stdio : writeln;
+
+void main() @safe
+{
+    auto options = new OperatorOptions();
+    auto op = Operator("memory", options);
+
+    op.createDir("logs/");
+    op.write("logs/a.txt", cast(ubyte[]) "first".dup);
+    op.write("logs/b.txt", cast(ubyte[]) "second".dup);
+
+    Entry[] entries = op.list("logs/");
+    foreach (e; entries)
+        writeln(e.path(), "  name=", e.name());
+}
+```
+
+## Pointing at a real backend
+
+Only the `OperatorOptions` change; all method calls stay identical.
+
+```d
+import opendal;
+
+void main() @safe
+{
+    auto options = new OperatorOptions();
+    options.set("root", "/tmp/mydata");
+
+    auto op = Operator("fs", options);
+    op.write("hello.txt", cast(ubyte[]) "Hello from the filesystem!".dup);
+}
+```
+
+`OPENDAL_TEST` is read by the build script at **build time** to compile the
+named service into the C library. To use the `fs` backend you must build with
+it enabled:
+
+```shell
+OPENDAL_TEST=fs OPENDAL_FS_ROOT=/tmp/mydata dub build -b release
+```
+
+At runtime the backend is selected by the scheme string passed to `Operator`
+(e.g. `"fs"`), and backend-specific settings are passed through
+`OperatorOptions` (e.g. `options.set("root", "/tmp/mydata")`). See
+[Services](/services) for every available backend and its configuration keys.
+
+## Parallel operations
+
+Pass `useParallel = true` to spawn a `TaskPool` and use `writeParallel`,
+`readParallel`, and `listParallel`:
+
+```d
+import opendal;
+
+void main() @safe
+{
+    auto options = new OperatorOptions();
+    auto op = Operator("memory", options, true /* useParallel */);
+
+    op.writeParallel("hello.txt", cast(ubyte[]) "data".dup);
+    ubyte[] result = op.readParallel("hello.txt");
+}
+```
diff --git a/website/docs/20-bindings/dart.mdx b/website/docs/20-bindings/dart.mdx
deleted file mode 100644
index 017146980..000000000
--- a/website/docs/20-bindings/dart.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Dart 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/dart/README.md';
-
-<GitHubReadme basePath="bindings/dart/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/dart/01-overview.md b/website/docs/20-bindings/dart/01-overview.md
new file mode 100644
index 000000000..7fd3e4c74
--- /dev/null
+++ b/website/docs/20-bindings/dart/01-overview.md
@@ -0,0 +1,59 @@
+---
+title: Dart
+sidebar_label: Overview
+slug: /bindings/dart
+description: The OpenDAL Dart binding — experimental, stdlib-style API for accessing storage services from Dart/Flutter.
+---
+
+# Apache OpenDAL™ Dart Binding
+
+A Dart binding for OpenDAL: access S3, GCS, Azure Blob, the local filesystem,
+and [50+ more services](/services) through one API, backed by the Rust core via
+[flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge).
+
+The API follows the `dart:io` stdlib style — `Storage`, `File`, and `Directory`
+objects that should feel familiar to any Dart developer.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, and operation are the same four ideas in every language.
+
+:::caution Experimental
+This binding is a work in progress. The public API may change between releases.
+It is not yet published to pub.dev. Expect rough edges and missing features
+compared to the Rust core or the Python binding.
+:::
+
+## Capabilities
+
+- **Async and sync** — every `File` and `Directory` method has both an `async`
+  (`Future<T>`) and a blocking (`*Sync`) variant.
+- **File operations** — `read`, `write`, `exists`, `stat`, `delete`, `rename`.
+- **Directory operations** — `create`, `exists`, `stat`, `delete`, `rename`.
+- **stdlib-style API** — `Storage.initFile()` and `Storage.initDir()` return
+  factory functions that mirror `dart:io`.
+
+The binding currently exposes read, write, stat, exists, delete, rename, and
+createDir. Listing, copy, and presigned URLs are not yet surfaced.
+
+## Installation
+
+This binding is not yet published to pub.dev. Add it as a path dependency from
+the repository, or build it from source following the
+[Development guide](https://github.com/apache/opendal/tree/main/bindings/dart#development).
+
+The native Rust library must be compiled separately:
+
+```shell
+cd bindings/dart/rust
+cargo build -r
+```
+
+## Useful Links
+
+- **Source & examples**: [`bindings/dart/`](https://github.com/apache/opendal/tree/main/bindings/dart)
+- **Services & configuration**: [/services](/services)
+- **Concepts**: [Concepts](../../03-concepts.mdx)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — build and run a minimal program.
diff --git a/website/docs/20-bindings/dart/02-getting-started.md b/website/docs/20-bindings/dart/02-getting-started.md
new file mode 100644
index 000000000..6954b41be
--- /dev/null
+++ b/website/docs/20-bindings/dart/02-getting-started.md
@@ -0,0 +1,111 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Build and run your first OpenDAL program in Dart — write a file and read it back.
+---
+
+# Getting started
+
+## Prerequisites
+
+The Dart binding wraps the Rust core via `flutter_rust_bridge`. You need to
+build the native library before running Dart code.
+
+```shell
+# From the repository root
+cd bindings/dart/rust
+cargo build -r
+cd ..
+dart pub get
+```
+
+The loader picks up the native library from `rust/target/release/` automatically
+for native (non-cross-compiled) builds.
+
+## Your first program
+
+This program creates an in-memory storage, writes a file, reads it back, and
+checks metadata. It uses the `memory` service so there are no credentials or
+paths to configure.
+
+```dart
+import 'dart:typed_data';
+import 'package:opendal/opendal.dart';
+
+void main() async {
+  // Initialize storage with a service scheme and configuration map.
+  final storage = await Storage.init(schemeStr: "memory", map: {"root": "/"});
+
+  // initFile() returns a factory function that mirrors dart:io File.
+  final File = storage.initFile();
+
+  final file = File("hello.txt");
+
+  // Write bytes to the file.
+  await file.write(Uint8List.fromList("Hello, OpenDAL!".codeUnits));
+
+  // Read bytes back.
+  final data = await file.read();
+  print(String.fromCharCodes(data)); // Hello, OpenDAL!
+
+  // Inspect metadata.
+  final meta = await file.stat();
+  print("isFile: ${meta.isFile}");
+  print("contentLength: ${meta.contentLength}");
+
+  // Delete the file.
+  await file.delete();
+}
+```
+
+`read()` returns `Uint8List`. `write()` takes `Uint8List`; a plain `List<int>` must be converted first with `Uint8List.fromList(list)`.
+
+`meta.contentLength` is typed `BigInt?`, not `int`. Use `.toInt()` or `.toString()` if you need a plain integer or a string.
+
+## Point it at a real backend
+
+Only `Storage.init` changes — the `File` and `Directory` API stays the same:
+
+```dart
+// Local filesystem rooted at /tmp
+final storage = await Storage.init(schemeStr: "fs", map: {"root": "/tmp"});
+final File = storage.initFile();
+
+final file = File("test.txt");
+await file.write(Uint8List.fromList("Hello from fs!".codeUnits));
+print(await file.exists()); // true
+```
+
+For S3 or other services, pass the scheme and any configuration keys that
+service requires — see [Services](/services) for the full list.
+
+## Sync variants
+
+Every async method has a blocking `*Sync` counterpart. Use them when you cannot
+`await`:
+
+```dart
+final storage = await Storage.init(schemeStr: "memory", map: {"root": "/"});
+final File = storage.initFile();
+final file = File("sync.txt");
+
+file.writeSync(Uint8List.fromList([1, 2, 3]));
+final data = file.readSync();
+print(data); // [1, 2, 3]
+```
+
+## Working with directories
+
+`storage.initDir()` returns a factory for `Directory` objects:
+
+```dart
+final storage = await Storage.init(schemeStr: "memory", map: {"root": "/"});
+final Directory = storage.initDir();
+
+final dir = Directory("my-dir/");
+await dir.create();
+print(await dir.exists()); // true
+
+final meta = await dir.stat();
+print("isDirectory: ${meta.isDirectory}"); // true
+```
diff --git a/website/docs/20-bindings/dotnet.mdx b/website/docs/20-bindings/dotnet.mdx
deleted file mode 100644
index 1920b1d83..000000000
--- a/website/docs/20-bindings/dotnet.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Dotnet 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/dotnet/README.md';
-
-<GitHubReadme basePath="bindings/dotnet/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/dotnet/01-overview.md b/website/docs/20-bindings/dotnet/01-overview.md
new file mode 100644
index 000000000..a4f230a22
--- /dev/null
+++ b/website/docs/20-bindings/dotnet/01-overview.md
@@ -0,0 +1,64 @@
+---
+title: .NET
+sidebar_label: Overview
+slug: /bindings/dotnet
+description: The OpenDAL .NET binding — install it, see what it supports, and where to go next.
+---
+
+# Apache OpenDAL™ .NET Binding
+
+A native .NET binding for OpenDAL: access S3, GCS, Azure Blob, the local
+filesystem, and [50+ more services](/services) through one API, with the
+performance of the Rust core underneath.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+:::note
+This binding has its own version number, independent of the Rust core. Check
+compatibility against the binding's version, not the core's.
+:::
+
+## Status
+
+Unreleased. The package is not yet published to NuGet, so today you build the
+binding from source (see [Installation](#installation)). The intended package id
+is `Apache.OpenDAL`. The API is functional and covered by tests.
+
+## Capabilities
+
+- **Sync and async** — every operation has a blocking method and an
+  `…Async` (`Task`-based) counterpart with `CancellationToken` support.
+- **`Stream` wrappers** — `OpenReadStream` / `OpenWriteStream` return ordinary
+  .NET `Stream`s you can pipe through.
+- **All services included** — the binding bundles every backend; there are no
+  build flags to enable individual services.
+- **Composable layers** for retry, timeout, and concurrency limits.
+- **Presigned URLs** for services that support them (async API).
+
+## Installation
+
+The binding targets `net8.0` and `net10.0`. Until a NuGet package is published,
+build it from source — you need the [.NET SDK](https://dotnet.microsoft.com/en-us/download/dotnet)
+and a Rust toolchain for the native library:
+
+```bash
+cargo build
+dotnet build
+```
+
+See [Getting started](./02-getting-started.md) for a runnable example and the
+[build details](https://github.com/apache/opendal/tree/main/bindings/dotnet).
+
+## Useful Links
+
+- **Services & configuration**: [/services](/services)
+- **OpenDAL Scheme reference**: [docs.rs/opendal — enum.Scheme](https://docs.rs/opendal/latest/opendal/enum.Scheme.html)
+- **Source & examples**: [`bindings/dotnet/`](https://github.com/apache/opendal/tree/main/bindings/dotnet)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — a runnable program, then a real backend.
+2. [Connecting to your storage](./03-connecting.md) — build an operator for any service.
+3. [Common tasks](./04-tasks.md) — read, write, stream, list, presign, and more.
+4. [Going to production](./05-production.md) — layers, errors, and capability checks.
diff --git a/website/docs/20-bindings/dotnet/02-getting-started.md b/website/docs/20-bindings/dotnet/02-getting-started.md
new file mode 100644
index 000000000..422caefe2
--- /dev/null
+++ b/website/docs/20-bindings/dotnet/02-getting-started.md
@@ -0,0 +1,100 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Run your first OpenDAL program in .NET, then point it at a real storage backend.
+---
+
+# Getting started
+
+## Your first program
+
+This program builds an operator, writes a file, reads it back, inspects its
+metadata, and deletes it. It runs against the in-memory service with no
+credentials, so you can run it as soon as the binding is built.
+
+```csharp
+using OpenDAL;
+using System.Text;
+
+// Configure a service, then build an operator from it.
+using var op = new Operator("memory");
+
+// The same verbs work on every service.
+op.Write("hello.txt", Encoding.UTF8.GetBytes("Hello, World!"));
+
+var bytes = op.Read("hello.txt");
+Console.WriteLine(Encoding.UTF8.GetString(bytes));
+
+var meta = op.Stat("hello.txt");
+Console.WriteLine($"size = {meta.ContentLength} bytes");
+
+op.Delete("hello.txt");
+```
+
+`Write` takes a `byte[]`, and `Read` returns a `byte[]`. The `using` declaration
+disposes the operator and frees its native handle when it goes out of scope —
+always dispose `Operator` instances.
+
+## Point it at a real backend
+
+Only the service changes; the operations stay identical. Pass the scheme and a
+dictionary of configuration keys:
+
+```csharp
+using OpenDAL;
+using System.Text;
+
+using var op = new Operator("s3", new Dictionary<string, string>
+{
+    ["bucket"] = "my-bucket",
+    ["region"] = "us-east-1",
+});
+
+op.Write("hello.txt", Encoding.UTF8.GetBytes("Hello from S3!"));
+Console.WriteLine(Encoding.UTF8.GetString(op.Read("hello.txt")));
+```
+
+The binding bundles every service, so there is nothing extra to install. The
+next page, [Connecting to your storage](./03-connecting.md), covers construction
+and credentials in depth; [Services](/services) lists every backend and its keys.
+
+## Async usage {#blocking-vs-async}
+
+Every operation has an `…Async` counterpart that returns a `Task` and accepts a
+`CancellationToken`. Await each call:
+
+```csharp
+using OpenDAL;
+using System.Text;
+
+using var op = new Operator("memory");
+
+await op.WriteAsync("hello.txt", Encoding.UTF8.GetBytes("Hello, World!"));
+var bytes = await op.ReadAsync("hello.txt");
+Console.WriteLine(Encoding.UTF8.GetString(bytes));
+```
+
+## Executors {#executors}
+
+Async operations run on a native Tokio runtime. By default OpenDAL uses a shared
+executor, so you can call the `…Async` methods without configuring anything. To
+control the worker-thread count, create an [`Executor`] and pass it to each call:
+
+```csharp
+using OpenDAL;
+using System.Text;
+
+using var executor = new Executor(2); // two Tokio worker threads
+using var op = new Operator("memory");
+
+await op.WriteAsync("hello.txt", Encoding.UTF8.GetBytes("Hello, World!"), executor);
+var bytes = await op.ReadAsync("hello.txt", executor);
+```
+
+Keep an `Executor` alive for the full lifetime of the operations using it.
+Disposing an `Executor` (or `Operator`) while awaited work is still running
+throws `ObjectDisposedException`, so dispose them only after the awaited
+operations complete. See [Going to production](./05-production.md#executor-and-lifetime)
+for the lifetime rules.
+
+[`Executor`]: https://github.com/apache/opendal/blob/main/bindings/dotnet/OpenDAL/Executor.cs
diff --git a/website/docs/20-bindings/dotnet/03-connecting.md b/website/docs/20-bindings/dotnet/03-connecting.md
new file mode 100644
index 000000000..800cc4c4d
--- /dev/null
+++ b/website/docs/20-bindings/dotnet/03-connecting.md
@@ -0,0 +1,89 @@
+---
+title: Connecting to your storage
+sidebar_label: Connecting to your storage
+description: Build an OpenDAL operator in .NET for any backend — scheme + dictionary, typed config, and credentials.
+---
+
+# Connecting to your storage
+
+Switching backends is a configuration change, not a code change. For the
+configuration keys of a specific service, see [Services](/services).
+
+The binding bundles every service — there are no build flags. Once the binding
+is referenced, every backend is available.
+
+## Build an operator
+
+### From a scheme and dictionary
+
+The first argument is the service scheme; configuration follows as a
+`Dictionary<string, string>` whose keys match the service's configuration fields:
+
+```csharp
+using OpenDAL;
+
+using var op = new Operator("s3", new Dictionary<string, string>
+{
+    ["bucket"] = "my-bucket",
+    ["region"] = "us-east-1",
+    ["endpoint"] = "https://s3.amazonaws.com",
+});
+```
+
+A local filesystem operator roots every path under one directory:
+
+```csharp
+using var fs = new Operator("fs", new Dictionary<string, string>
+{
+    ["root"] = "/tmp/opendal",
+});
+```
+
+### From a typed service config
+
+Each service also has a strongly typed config class in `OpenDAL.ServiceConfig`.
+It is converted to the same key/value options internally, so the result is
+identical — pick this form for compile-time-checked, discoverable properties:
+
+```csharp
+using OpenDAL;
+using OpenDAL.ServiceConfig;
+
+using var fs = new Operator(new FsServiceConfig
+{
+    Root = "/tmp/opendal",
+});
+```
+
+Available scheme names and their keys are defined by OpenDAL; see the
+[Scheme reference](https://docs.rs/opendal/latest/opendal/enum.Scheme.html) and
+[Services](/services).
+
+## Credentials
+
+Credentials are just more configuration keys. Set them in the dictionary (or on
+the typed config) — for example S3's `access_key_id` and `secret_access_key`:
+
+```csharp
+using var op = new Operator("s3", new Dictionary<string, string>
+{
+    ["bucket"] = "my-bucket",
+    ["region"] = "us-east-1",
+    ["access_key_id"] = Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID")!,
+    ["secret_access_key"] = Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY")!,
+});
+```
+
+Some services also load credentials from their platform's default sources (for
+S3, the standard AWS environment variables and profiles). See each service's
+page under [Services](/services) for the exact keys and credential behavior.
+
+Avoid hard-coding secrets in source. Read them from the environment or a secret
+manager and pass them in when you build the operator.
+
+## One operator per service and root
+
+An operator maps to one service and one root path. To work with two buckets or
+two roots, build two operators — they are lightweight handles. Use `Duplicate()`
+to create another handle to the same backend configuration. Always dispose
+operators with `using` or an explicit `Dispose()`.
diff --git a/website/docs/20-bindings/dotnet/04-tasks.md b/website/docs/20-bindings/dotnet/04-tasks.md
new file mode 100644
index 000000000..8a6f31017
--- /dev/null
+++ b/website/docs/20-bindings/dotnet/04-tasks.md
@@ -0,0 +1,162 @@
+---
+title: Common tasks
+sidebar_label: Common tasks
+description: Task-oriented recipes for OpenDAL in .NET — read, write, stream, list, delete, copy, and presign.
+---
+
+# Common tasks
+
+Recipes for the things you actually do with storage. Each works on every
+service. They assume an `op` built as in
+[Getting started](./02-getting-started.md).
+
+A few conventions used throughout:
+
+- Paths are relative to the operator's root; a trailing `/` means a directory
+  (`logs/app/` is a directory, `logs/app` is a file).
+- `Read` returns `byte[]`; `Write` takes `byte[]`.
+- Every method has an `…Async` counterpart that returns a `Task` and accepts a
+  `CancellationToken`. The recipes below show the sync form; add `Async`/`await`
+  for the async API.
+- Option types live in `OpenDAL.Options`.
+
+## Read a whole file
+
+```csharp
+using System.Text;
+
+byte[] bytes = op.Read("path/to/file");
+string text = Encoding.UTF8.GetString(bytes);
+```
+
+## Read part of a file
+
+Pass a `ReadOptions` with an offset and length:
+
+```csharp
+using OpenDAL.Options;
+
+byte[] head = op.Read("path/to/file", new ReadOptions { Offset = 0, Length = 1024 });
+```
+
+## Write a whole file
+
+```csharp
+using System.Text;
+
+op.Write("path/to/file", Encoding.UTF8.GetBytes("Hello, World!"));
+```
+
+`WriteOptions` carry content headers, conditions, append mode, and concurrency:
+
+```csharp
+using OpenDAL.Options;
+
+await op.WriteAsync("docs/readme.txt", content, new WriteOptions
+{
+    ContentType = "text/plain",
+    CacheControl = "no-cache",
+    IfNotExists = true,
+});
+```
+
+## Stream a large file
+
+Don't buffer gigabytes in memory — open a `Stream` and read in chunks.
+`OpenReadStream` returns an `OperatorInputStream`:
+
+```csharp
+using var reader = op.OpenReadStream("big.bin");
+using var ms = new MemoryStream();
+reader.CopyTo(ms);
+```
+
+## Stream a large upload
+
+`OpenWriteStream` returns an `OperatorOutputStream`. Write incrementally, then
+dispose to flush and close the native resources deterministically:
+
+```csharp
+using System.Text;
+
+await using (var writer = op.OpenWriteStream("big.bin"))
+{
+    var chunk = Encoding.UTF8.GetBytes("stream-data");
+    await writer.WriteAsync(chunk, 0, chunk.Length);
+    await writer.FlushAsync();
+}
+```
+
+`OpenWriteStream` also accepts a `bufferSize` argument that controls how much is
+buffered before flushing to the backend.
+
+## Check existence and metadata
+
+```csharp
+var meta = op.Stat("path/to/file");
+Console.WriteLine($"{meta.ContentLength} bytes, dir = {meta.IsDir}");
+```
+
+A missing path throws `OpenDALException` with `ErrorCode.NotFound`; see
+[error handling](./05-production.md#error-handling).
+
+## List a directory
+
+`List` returns the entries under a path:
+
+```csharp
+foreach (var entry in op.List("dir/"))
+{
+    Console.WriteLine($"{entry.Path} ({entry.Metadata.Mode})");
+}
+```
+
+## Walk a tree recursively
+
+```csharp
+using OpenDAL.Options;
+
+var entries = op.List("dir/", new ListOptions { Recursive = true });
+```
+
+## Delete a file or a whole tree
+
+```csharp
+op.Delete("path/to/file");   // single path
+op.RemoveAll("dir/");        // a path and everything under it
+```
+
+## Create a directory
+
+```csharp
+op.CreateDir("path/to/dir/");   // the trailing slash is required
+```
+
+## Copy and rename
+
+```csharp
+op.Copy("from.txt", "to.txt");
+op.Rename("old.txt", "new.txt");
+```
+
+Both operate within a single operator and require a service that supports them;
+see [capability checks](./05-production.md#capability-checks).
+
+## Generate a presigned URL
+
+Hand a time-limited URL to a third party so they can access an object without
+your credentials. Presigning is part of the **async** API:
+
+```csharp
+var request = await op.PresignReadAsync("data/file.txt", TimeSpan.FromMinutes(10));
+
+Console.WriteLine(request.Method);
+Console.WriteLine(request.Uri);
+foreach (var header in request.Headers)
+{
+    Console.WriteLine($"{header.Key}: {header.Value}");
+}
+```
+
+The available presign methods are `PresignReadAsync`, `PresignWriteAsync`,
+`PresignStatAsync`, and `PresignDeleteAsync`.
diff --git a/website/docs/20-bindings/dotnet/05-production.md b/website/docs/20-bindings/dotnet/05-production.md
new file mode 100644
index 000000000..f8c200e11
--- /dev/null
+++ b/website/docs/20-bindings/dotnet/05-production.md
@@ -0,0 +1,112 @@
+---
+title: Going to production
+sidebar_label: Going to production
+description: Make OpenDAL robust in .NET — layers, error handling, capability checks, and executor lifetime.
+---
+
+# Going to production
+
+The basics read and write data. Production code also has to survive transient
+failures, bound its resource use, handle errors precisely, and manage native
+resource lifetime.
+
+## Layers
+
+A layer wraps an operator to add cross-cutting behavior without touching your
+storage code. `WithLayer(...)` returns a new operator with the layer applied;
+chain calls to compose several:
+
+```csharp
+using OpenDAL;
+using OpenDAL.Layer;
+
+using var baseOp = new Operator("memory");
+
+using var op = baseOp
+    .WithLayer(new ConcurrentLimitLayer(64))
+    .WithLayer(new RetryLayer
+    {
+        MaxTimes = 5,
+        MinDelay = TimeSpan.FromMilliseconds(100),
+        MaxDelay = TimeSpan.FromSeconds(5),
+        Factor = 2f,
+        Jitter = true,
+    })
+    .WithLayer(new TimeoutLayer
+    {
+        Timeout = TimeSpan.FromSeconds(30),
+        IoTimeout = TimeSpan.FromSeconds(5),
+    });
+```
+
+The .NET binding exposes these layers in `OpenDAL.Layer`:
+
+| Layer | What it does |
+|-------|--------------|
+| `RetryLayer` | Retries transient failures with exponential backoff (`MaxTimes`, `MinDelay`, `MaxDelay`, `Factor`, `Jitter`). |
+| `TimeoutLayer` | Bounds slow calls with a total `Timeout` and a per-I/O `IoTimeout`. |
+| `ConcurrentLimitLayer` | Caps the number of concurrent operations (constructor takes the permit count). |
+| `CapabilityOverrideLayer` | Overrides reported capabilities. |
+
+See [Concepts](../../03-concepts.mdx#layer) for the model.
+
+## Error handling
+
+Most failures surface as `OpenDALException` with a typed `Code` of type
+`ErrorCode`. Match on the code instead of inspecting messages:
+
+```csharp
+using OpenDAL;
+
+try
+{
+    await op.ReadAsync("maybe-missing.txt");
+}
+catch (OpenDALException ex) when (ex.Code == ErrorCode.NotFound)
+{
+    // handle the missing object
+}
+```
+
+Common codes include `NotFound`, `PermissionDenied`, `AlreadyExists`,
+`ConditionNotMatch`, `RateLimited`, and `Unsupported`. Unknown native codes are
+normalized to `ErrorCode.Unexpected`.
+
+## Capability checks {#capability-checks}
+
+Not every service supports every operation. Query what a backend can do through
+`op.Info.Capability` before calling optional operations like `Copy`, `Rename`,
+or presign:
+
+```csharp
+var cap = op.Info.Capability;
+if (cap.PresignRead)
+{
+    var req = await op.PresignReadAsync("a.txt", TimeSpan.FromMinutes(5));
+}
+```
+
+Calling an unsupported operation throws `OpenDALException` with
+`ErrorCode.Unsupported`, so capability checks are an optimization, not a
+requirement for safety. `Info` also exposes the operator's `Scheme`, `Root`, and
+`Name`.
+
+## Executor and lifetime {#executor-and-lifetime}
+
+The binding wraps native handles, so deterministic disposal matters:
+
+- Prefer `using` for `Operator`, `Executor`, and stream instances.
+- Keep an `Executor` alive for the full lifetime of the operations using it.
+- Disposing an `Executor` or `Operator` too early throws
+  `ObjectDisposedException`. For async calls, ensure disposal happens **after**
+  the awaited operations complete.
+- If you do not pass an `Executor`, OpenDAL uses a shared default executor.
+
+See [Getting started — Executors](./02-getting-started.md#executors) for how to
+create and pass an executor.
+
+## Path conventions
+
+- Use backend-native object keys, for example `a/b/c.txt`.
+- For directory-like operations (`CreateDir`, directory `Stat`, listing a
+  directory root) prefer trailing-slash paths such as `logs/`.
diff --git a/website/docs/20-bindings/go.mdx b/website/docs/20-bindings/go.mdx
deleted file mode 100644
index 7fcf59508..000000000
--- a/website/docs/20-bindings/go.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Go
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/go/README.md';
-
-<GitHubReadme basePath="bindings/go/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/go/01-overview.md b/website/docs/20-bindings/go/01-overview.md
new file mode 100644
index 000000000..abe7417be
--- /dev/null
+++ b/website/docs/20-bindings/go/01-overview.md
@@ -0,0 +1,67 @@
+---
+title: Go
+sidebar_label: Overview
+slug: /bindings/go
+description: The OpenDAL Go binding — install it, see what it supports, and where to go next.
+---
+
+# Apache OpenDAL™ Go Binding
+
+A native Go binding for OpenDAL: access S3, GCS, Azure Blob, the local
+filesystem, and [many more services](/services) through one API, with the
+performance of the Rust core underneath. It is built on `opendal-c` via
+[purego](https://github.com/ebitengine/purego) and libffi, so **no CGO** is
+required.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+:::note
+This binding has its own version number, independent of the Rust core. Check
+compatibility against the binding's version, not the core's.
+:::
+
+## Status
+
+Released and usable, published to
+[pkg.go.dev](https://pkg.go.dev/github.com/apache/opendal/bindings/go).
+
+## Capabilities
+
+- **Synchronous API** — every operation is a blocking method that returns a Go
+  `error`; there is no async variant.
+- **Streaming** reads and writes through `io.ReadSeekCloser` and `io.WriteCloser`.
+- **Per-scheme packages** — each service is a small companion module under
+  `github.com/apache/opendal-go-services/<scheme>` that you import alongside the
+  binding.
+- **Presigned URLs** for services that support them, returned as
+  `*net/http.Request`.
+
+## Installation
+
+The binding requires **libffi** to be installed on your system.
+
+```shell
+go get github.com/apache/opendal/bindings/go@latest
+```
+
+Each service lives in its own companion module. Add the schemes you use, for
+example the in-memory service:
+
+```shell
+go get github.com/apache/opendal-go-services/memory
+```
+
+## Useful Links
+
+- **API reference**: [pkg.go.dev](https://pkg.go.dev/github.com/apache/opendal/bindings/go)
+- **Services & configuration**: [/services](/services)
+- **Source & examples**: [`bindings/go/`](https://github.com/apache/opendal/tree/main/bindings/go)
+- **Scheme packages**: [`opendal-go-services`](https://github.com/apache/opendal-go-services)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — a runnable program, then a real backend.
+2. [Connecting to your storage](./03-connecting.md) — build an operator for any service.
+3. [Common tasks](./04-tasks.md) — read, write, stream, list, copy, rename, and presign.
+4. [Going to production](./05-production.md) — retries, timeouts, errors, and capability checks.
diff --git a/website/docs/20-bindings/go/02-getting-started.md b/website/docs/20-bindings/go/02-getting-started.md
new file mode 100644
index 000000000..4177edf25
--- /dev/null
+++ b/website/docs/20-bindings/go/02-getting-started.md
@@ -0,0 +1,101 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Run your first OpenDAL program in Go, then point it at a real storage backend.
+---
+
+# Getting started
+
+## Your first program
+
+This program builds an operator, writes a file, reads it back, inspects its
+metadata, and deletes it. It runs against the in-memory service with no
+credentials, so you can run it right after installing.
+
+The binding requires **libffi** to be installed. Then add the binding and the
+scheme package for the service you want:
+
+```shell
+go get github.com/apache/opendal/bindings/go@latest
+go get github.com/apache/opendal-go-services/memory
+```
+
+```go
+package main
+
+import (
+	"fmt"
+	"log"
+
+	"github.com/apache/opendal-go-services/memory"
+	opendal "github.com/apache/opendal/bindings/go"
+)
+
+func main() {
+	// Configure a service, then build an operator from it.
+	op, err := opendal.NewOperator(memory.Scheme, opendal.OperatorOptions{})
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer op.Close() // Always close the operator to release resources.
+
+	// The same verbs work on every service.
+	if err := op.Write("hello.txt", []byte("Hello, World!")); err != nil {
+		log.Fatal(err)
+	}
+
+	data, err := op.Read("hello.txt")
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Printf("read %d bytes: %s\n", len(data), data)
+
+	meta, err := op.Stat("hello.txt")
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Printf("size = %d bytes\n", meta.ContentLength())
+
+	if err := op.Delete("hello.txt"); err != nil {
+		log.Fatal(err)
+	}
+}
+```
+
+Note that `Write` takes `[]byte` and `Read` returns `[]byte`. Every method
+returns an `error` as its last value — check it instead of expecting a panic.
+
+## Point it at a real backend
+
+Only the service changes; the operations stay identical. To use S3, import its
+scheme package and pass configuration through `OperatorOptions`, a
+`map[string]string` of configuration keys:
+
+```shell
+go get github.com/apache/opendal-go-services/s3
+```
+
+```go
+import (
+	"github.com/apache/opendal-go-services/s3"
+	opendal "github.com/apache/opendal/bindings/go"
+)
+
+op, err := opendal.NewOperator(s3.Scheme, opendal.OperatorOptions{
+	"bucket": "my-bucket",
+	"region": "us-east-1",
+})
+if err != nil {
+	log.Fatal(err)
+}
+defer op.Close()
+
+if err := op.Write("hello.txt", []byte("Hello from S3!")); err != nil {
+	log.Fatal(err)
+}
+```
+
+Each service is a separate `opendal-go-services` module, so add the ones you
+use. The next page, [Connecting to your storage](./03-connecting.md), covers
+configuration and credentials in depth; [Services](/services) lists every
+backend and its configuration keys.
diff --git a/website/docs/20-bindings/go/03-connecting.md b/website/docs/20-bindings/go/03-connecting.md
new file mode 100644
index 000000000..b52881663
--- /dev/null
+++ b/website/docs/20-bindings/go/03-connecting.md
@@ -0,0 +1,109 @@
+---
+title: Connecting to your storage
+sidebar_label: Connecting to your storage
+description: Build an OpenDAL operator in Go for any backend — scheme packages, configuration, and credentials.
+---
+
+# Connecting to your storage
+
+Switching backends is a configuration change, not a code change. For the
+configuration keys of a specific service, see [Services](/services).
+
+## Add the scheme package
+
+Each service is a separate companion module under
+`github.com/apache/opendal-go-services/<scheme>`. Add the ones you use, then
+import the package to get its `Scheme` value:
+
+```shell
+go get github.com/apache/opendal-go-services/s3
+```
+
+```go
+import (
+	"github.com/apache/opendal-go-services/s3"
+	opendal "github.com/apache/opendal/bindings/go"
+)
+```
+
+Forgetting to import the scheme package is the most common reason a service is
+unavailable at build time.
+
+## Build an operator
+
+`NewOperator` takes a `Scheme` and an `OperatorOptions` map of configuration
+keys. The keys are the same as each service's configuration fields:
+
+```go
+op, err := opendal.NewOperator(s3.Scheme, opendal.OperatorOptions{
+	"bucket":   "my-bucket",
+	"region":   "us-east-1",
+	"endpoint": "https://s3.amazonaws.com",
+})
+if err != nil {
+	log.Fatal(err)
+}
+defer op.Close()
+```
+
+A local filesystem operator roots every path under one directory:
+
+```go
+import "github.com/apache/opendal-go-services/fs"
+
+op, err := opendal.NewOperator(fs.Scheme, opendal.OperatorOptions{
+	"root": "/tmp/opendal",
+})
+```
+
+Always `defer op.Close()` after a successful `NewOperator` — the operator holds
+native resources that must be released.
+
+## Credentials
+
+Credentials are just more configuration keys. Set them in `OperatorOptions` —
+for example S3's `access_key_id` and `secret_access_key`:
+
+```go
+op, err := opendal.NewOperator(s3.Scheme, opendal.OperatorOptions{
+	"bucket":            "my-bucket",
+	"region":            "us-east-1",
+	"access_key_id":     "...",
+	"secret_access_key": "...",
+})
+```
+
+Some services also load credentials from their platform's default sources. See
+each service's page under [Services](/services) for the exact keys and
+credential behavior.
+
+Avoid hard-coding secrets in source. Read them from the environment or a secret
+manager and pass them in when you build the operator:
+
+```go
+import "os"
+
+op, err := opendal.NewOperator(s3.Scheme, opendal.OperatorOptions{
+	"bucket":            "my-bucket",
+	"region":            "us-east-1",
+	"access_key_id":     os.Getenv("AWS_ACCESS_KEY_ID"),
+	"secret_access_key": os.Getenv("AWS_SECRET_ACCESS_KEY"),
+})
+```
+
+## Verify the connection
+
+`Check` performs a health check against the backend, so you can fail fast if the
+configuration or credentials are wrong:
+
+```go
+if err := op.Check(); err != nil {
+	log.Fatalf("operator is not reachable: %v", err)
+}
+```
+
+## One operator per service and root
+
+An operator maps to one service and one root path. To work with two buckets or
+two roots, build two operators — each is an independent handle. Remember to
+`Close` each one when you are done with it.
diff --git a/website/docs/20-bindings/go/04-tasks.md b/website/docs/20-bindings/go/04-tasks.md
new file mode 100644
index 000000000..808f1d819
--- /dev/null
+++ b/website/docs/20-bindings/go/04-tasks.md
@@ -0,0 +1,197 @@
+---
+title: Common tasks
+sidebar_label: Common tasks
+description: Task-oriented recipes for OpenDAL in Go — read, write, stream, list, delete, copy, rename, and presign.
+---
+
+# Common tasks
+
+Recipes for the things you actually do with storage. Each works on every
+service. They assume an `op` built as in
+[Getting started](./02-getting-started.md). For full signatures, see the
+[API reference](https://pkg.go.dev/github.com/apache/opendal/bindings/go).
+
+A few conventions used throughout:
+
+- Paths are relative to the operator's root; a trailing `/` means a directory
+  (`logs/app/` is a directory, `logs/app` is a file).
+- `Read` returns `[]byte`; `Write` takes `[]byte`.
+- Every operation returns an `error` as its last value — always check it.
+- Many verbs accept functional options (`opendal.ReadWithRange(...)`,
+  `opendal.WriteWithContentType(...)`, …) as trailing arguments.
+
+## Read a whole file
+
+```go
+data, err := op.Read("path/to/file")
+if err != nil {
+	log.Fatal(err)
+}
+text := string(data)
+```
+
+## Read part of a file
+
+`ReadWithRange(offset, length)` reads `length` bytes starting at `offset`:
+
+```go
+data, err := op.Read("path/to/file", opendal.ReadWithRange(0, 1024))
+```
+
+Use `opendal.ReadWithRangeFrom(offset)` to read from `offset` to the end.
+
+## Stream a large file
+
+Don't load gigabytes into memory — use a `Reader`, which implements
+`io.ReadSeekCloser`, and read in chunks:
+
+```go
+r, err := op.Reader("big.bin")
+if err != nil {
+	log.Fatal(err)
+}
+defer r.Close()
+
+buf := make([]byte, 8192)
+for {
+	n, err := r.Read(buf)
+	if n > 0 {
+		// process buf[:n]
+	}
+	if err == io.EOF {
+		break
+	}
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+```
+
+## Write a whole file
+
+```go
+err := op.Write("path/to/file", []byte("Hello, World!"))
+```
+
+## Stream a large upload
+
+Use a `Writer` for data produced incrementally. It implements `io.WriteCloser`;
+call `Write` repeatedly, then `Close` to commit. Each `Write` call accepts up to
+256KB.
+
+```go
+w, err := op.Writer("big.bin")
+if err != nil {
+	log.Fatal(err)
+}
+if _, err := w.Write(firstChunk); err != nil {
+	log.Fatal(err)
+}
+if _, err := w.Write(secondChunk); err != nil {
+	log.Fatal(err)
+}
+// Close finishes the write; data may be lost if you skip it.
+if err := w.Close(); err != nil {
+	log.Fatal(err)
+}
+```
+
+## Upload concurrently
+
+For large objects on services with multipart support, upload parts in parallel:
+
+```go
+err := op.Write("big.bin", data, opendal.WriteWithConcurrent(8))
+```
+
+## Check existence and metadata
+
+```go
+exists, err := op.IsExist("path/to/file")
+if err != nil {
+	log.Fatal(err)
+}
+if exists {
+	meta, err := op.Stat("path/to/file")
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Printf("%d bytes, dir = %v\n", meta.ContentLength(), meta.IsDir())
+}
+```
+
+## List a directory
+
+`List` returns a `Lister` you iterate with `Next` and `Entry`. Always check
+`Error` after the loop:
+
+```go
+lister, err := op.List("dir/")
+if err != nil {
+	log.Fatal(err)
+}
+defer lister.Close()
+
+for lister.Next() {
+	entry := lister.Entry()
+	fmt.Println(entry.Path())
+}
+if err := lister.Error(); err != nil {
+	log.Fatal(err)
+}
+```
+
+## Walk a tree recursively
+
+```go
+lister, err := op.List("dir/", opendal.ListWithRecursive(true))
+```
+
+The iteration is identical; only the option changes.
+
+## Delete a file or a whole tree
+
+```go
+err := op.Delete("path/to/file")                              // single path
+err = op.Delete("dir/", opendal.DeleteWithRecursive(true))    // path and everything under it
+```
+
+`Delete` succeeds even if the path does not exist.
+
+## Create a directory
+
+```go
+err := op.CreateDir("path/to/dir/") // the trailing slash is required
+```
+
+Creating a directory that already exists succeeds, and creation is recursive
+(like `mkdir -p`).
+
+## Copy and rename
+
+```go
+err := op.Copy("from.txt", "to.txt")
+err = op.Rename("old.txt", "new.txt")
+```
+
+Both operate within a single operator and require a service that supports them;
+see [capability checks](./05-production.md#capability-checks).
+
+## Generate a presigned URL
+
+Hand a time-limited URL to a third party so they can access an object without
+your credentials. The presign methods return a standard `*net/http.Request`:
+
+```go
+import "time"
+
+req, err := op.PresignRead("path/to/file", time.Hour)
+if err != nil {
+	log.Fatal(err)
+}
+fmt.Println(req.Method, req.URL)
+fmt.Println(req.Header)
+```
+
+`PresignWrite`, `PresignStat`, and `PresignDelete` work the same way for the
+other operations.
diff --git a/website/docs/20-bindings/go/05-production.md b/website/docs/20-bindings/go/05-production.md
new file mode 100644
index 000000000..127caf395
--- /dev/null
+++ b/website/docs/20-bindings/go/05-production.md
@@ -0,0 +1,103 @@
+---
+title: Going to production
+sidebar_label: Going to production
+description: Make OpenDAL robust in Go — retries, timeouts, error handling, and capability checks.
+---
+
+# Going to production
+
+The basics read and write data. Production code also has to survive transient
+failures, bound its resource use, handle errors precisely, and adapt to what a
+backend supports.
+
+## Layers
+
+In the Go binding, layers are configured when you build the operator by passing
+options to `NewOperator` after the `OperatorOptions` map. Two layers are
+exposed: retry and timeout.
+
+```go
+import "time"
+
+op, err := opendal.NewOperator(
+	s3.Scheme,
+	opendal.OperatorOptions{"bucket": "my-bucket", "region": "us-east-1"},
+	opendal.WithTimeout(10*time.Second, 60*time.Second),
+	opendal.WithRetry(opendal.RetryMaxTimes(3)),
+)
+if err != nil {
+	log.Fatal(err)
+}
+defer op.Close()
+```
+
+### Retry
+
+`WithRetry` retries operations that fail with temporary errors, using
+exponential backoff. Tune it with these options:
+
+| Option | What it does |
+|--------|--------------|
+| `RetryMaxTimes(n)` | Maximum number of retry attempts. |
+| `RetryFactor(f)` | Backoff multiplier between attempts (must be `>= 1`). |
+| `RetryMinDelay(d)` | Minimum delay before the first retry. |
+| `RetryMaxDelay(d)` | Cap on the delay between retries. |
+| `RetryJitter()` | Adds randomness to the backoff to avoid thundering herds. |
+
+```go
+opendal.WithRetry(
+	opendal.RetryMaxTimes(5),
+	opendal.RetryMinDelay(200*time.Millisecond),
+	opendal.RetryJitter(),
+)
+```
+
+### Timeout
+
+`WithTimeout(timeout, ioTimeout)` bounds slow calls. The first argument applies
+to non-IO operations; the second applies to read, write, list, and streaming IO.
+Pass `WithTimeout` before `WithRetry` so each retry attempt has its own timeout.
+
+## Error handling
+
+Every fallible call returns a Go `error`. When the failure comes from the
+storage layer, the concrete type is `*opendal.Error`. Type-assert it and branch
+on `Code()` instead of parsing messages:
+
+```go
+data, err := op.Read("maybe-missing.txt")
+if err != nil {
+	var oerr *opendal.Error
+	if errors.As(err, &oerr) && oerr.Code() == opendal.CodeNotFound {
+		// handle absence
+	} else {
+		log.Fatal(err)
+	}
+}
+```
+
+Common codes include `CodeNotFound`, `CodePermissioDenied`, `CodeAlreadyExists`,
+`CodeRateLimited`, `CodeConditionNotMatch`, and `CodeUnsupported`. Note that
+`Delete` is idempotent — deleting a missing path succeeds rather than returning
+`CodeNotFound`.
+
+## Capability checks {#capability-checks}
+
+Not every service supports every operation. Query what a backend can do through
+`Info().GetCapability()` before calling optional operations like `Copy`,
+`Rename`, or presign:
+
+```go
+cap := op.Info().GetCapability()
+if cap.Copy() {
+	if err := op.Copy("a.txt", "b.txt"); err != nil {
+		log.Fatal(err)
+	}
+}
+```
+
+Calling an unsupported operation returns an error with `CodeUnsupported`, so
+capability checks are an optimization, not a requirement for safety.
+
+`OperatorInfo` also reports the backend's scheme, root, and name via
+`GetScheme()`, `GetRoot()`, and `GetName()`.
diff --git a/website/docs/20-bindings/haskell.mdx b/website/docs/20-bindings/haskell.mdx
deleted file mode 100644
index 9b4ff6de9..000000000
--- a/website/docs/20-bindings/haskell.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Haskell 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/haskell/README.md';
-
-<GitHubReadme basePath="bindings/haskell/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/haskell/01-overview.md b/website/docs/20-bindings/haskell/01-overview.md
new file mode 100644
index 000000000..6b5a04f9e
--- /dev/null
+++ b/website/docs/20-bindings/haskell/01-overview.md
@@ -0,0 +1,70 @@
+---
+title: Haskell
+sidebar_label: Overview
+slug: /bindings/haskell
+description: The OpenDAL Haskell binding — experimental status, capabilities, and where to start.
+---
+
+# Apache OpenDAL™ Haskell Binding
+
+A Haskell binding for OpenDAL: access S3, GCS, Azure Blob, the local filesystem,
+and [50+ more services](/services) through one API, with the Rust core underneath.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+:::note
+This binding has its own version number, independent of the Rust core. When
+checking for updates or compatibility, always refer to this binding's version
+rather than the core version.
+:::
+
+## Status
+
+**Experimental / WIP.** The binding is unreleased and not yet published to
+Hackage. The API may change without notice. Use it to experiment or contribute,
+but do not rely on it in production.
+
+## Capabilities
+
+- **Blocking IO** — all operations run in `IO`; there is no async API yet.
+- **`OperatorT` monad** — compose operations with `do`-notation; errors surface
+  as `Left OpenDALError` through `ExceptT` under the hood.
+- **Raw `IO` functions** — every operation is also available as a plain `IO`
+  function returning `Either OpenDALError a` for direct use without the monad
+  transformer.
+- **Core operations** — read, write, stat, delete, copy, rename, list (shallow
+  and recursive scan), create directory, remove-all, writer (append and
+  overwrite).
+- **Logging** — optional structured logging via `co-log`.
+- **Services** — whichever services the bundled Rust library is compiled with;
+  the memory service works out of the box for testing.
+
+## Build
+
+The binding is not on Hackage; build from source inside `bindings/haskell/`:
+
+```shell
+# Build the Rust FFI layer and the Haskell library
+cabal build
+
+# Run the test suite
+cabal test
+
+# Generate Haddock documentation
+cabal haddock
+```
+
+Requires a working Rust toolchain (`cargo`) in addition to GHC and `cabal`.
+The build has been tested with GHC 9.4.8.
+
+## Useful Links
+
+- **Haddock reference**: generated locally with `cabal haddock`
+- **Services & configuration**: [/services](/services)
+- **Source**: [`bindings/haskell/`](https://github.com/apache/opendal/tree/main/bindings/haskell)
+- **Contributing**: [`CONTRIBUTING.md`](https://github.com/apache/opendal/blob/main/bindings/haskell/CONTRIBUTING.md)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — build an operator, read and write, handle errors.
diff --git a/website/docs/20-bindings/haskell/02-getting-started.md b/website/docs/20-bindings/haskell/02-getting-started.md
new file mode 100644
index 000000000..7f137e1d5
--- /dev/null
+++ b/website/docs/20-bindings/haskell/02-getting-started.md
@@ -0,0 +1,195 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Build your first OpenDAL program in Haskell — create an operator, read and write, and handle errors.
+---
+
+# Getting started
+
+## Build and install
+
+The binding is not yet published to Hackage. Clone the repository and work from
+`bindings/haskell/`:
+
+```shell
+git clone https://github.com/apache/opendal.git
+cd opendal/bindings/haskell
+cabal build
+```
+
+A working Rust toolchain (`cargo`) is required alongside GHC and `cabal`; the
+custom build step compiles the Rust FFI library automatically.
+
+## Your first program
+
+The snippet below creates an in-memory operator (no credentials needed), writes
+two keys, reads them back, checks existence, inspects metadata, and deletes a
+key.
+
+```haskell
+import OpenDAL
+
+main :: IO ()
+main = do
+  -- newOperator returns Either OpenDALError Operator
+  Right op <- newOperator "memory"
+
+  result <- runOp op $ do
+    writeOp "hello.txt" "Hello, World!"
+    content <- readOp "hello.txt"
+    isExistOp "hello.txt"         -- returns Bool inside the monad
+    meta <- statOp "hello.txt"
+    deleteOp "hello.txt"
+    return (content, mContentLength meta)
+
+  case result of
+    Left err  -> putStrLn $ "Error: " ++ show (errorCode err) ++ " — " ++ message err
+    Right (bytes, len) -> do
+      print bytes    -- "Hello, World!"
+      print len      -- 13
+```
+
+`runOp` executes the `OperatorT` block against the operator and returns
+`IO (Either OpenDALError a)`. Inside the block, operations use `MonadOperation`
+methods and errors abort the block early.
+
+## Using the raw IO API
+
+Every operation is also available as a standalone `IO` function returning
+`Either OpenDALError a`. Use these when you do not want the monad transformer:
+
+```haskell
+import OpenDAL
+
+main :: IO ()
+main = do
+  Right op <- newOperator "memory"
+
+  writeOpRaw op "key1" "value1" >>= \case
+    Left err -> putStrLn $ "write failed: " ++ show err
+    Right () -> return ()
+
+  readOpRaw op "key1" >>= \case
+    Left err    -> putStrLn $ "read failed: " ++ show err
+    Right bytes -> print bytes    -- "value1"
+
+  isExistOpRaw op "key1" >>= \case
+    Left err        -> putStrLn $ "isExist failed: " ++ show err
+    Right doesExist -> print doesExist    -- True
+```
+
+## Handling errors
+
+All errors carry an `ErrorCode` and a human-readable message:
+
+```haskell
+import OpenDAL
+
+main :: IO ()
+main = do
+  Right op <- newOperator "memory"
+  result <- runOp op $ readOp "does-not-exist"
+  case result of
+    Left (OpenDALError code msg) -> do
+      putStrLn $ "Code: " ++ show code    -- NotFound
+      putStrLn $ "Message: " ++ msg
+    Right _ -> putStrLn "unexpected success"
+```
+
+See the `ErrorCode` type for the complete set of error codes. Common ones include
+`NotFound`, `PermissionDenied`, `AlreadyExists`, `Unsupported`, `ConfigInvalid`,
+`RateLimited`, `IsADirectory`, `NotADirectory`, `IsSameFile`, `FFIError`, and
+`Unexpected`.
+
+## Point it at a real backend
+
+Pass a `HashMap` of service-specific config keys to `OperatorConfig`. The
+`OverloadedStrings` extension (enabled by default in the cabal file) lets you
+write the scheme as a string literal:
+
+```haskell
+import Data.HashMap.Strict qualified as HashMap
+import OpenDAL
+
+main :: IO ()
+main = do
+  let cfg = "s3"
+        { ocConfig = HashMap.fromList
+            [ ("bucket", "my-bucket")
+            , ("region", "us-east-1")
+            ]
+        }
+  result <- newOperator cfg
+  case result of
+    Left err -> putStrLn $ "operator creation failed: " ++ show err
+    Right op -> do
+      writeOpRaw op "hello.txt" "Hello from S3!" >>= print
+```
+
+Config keys match the Rust core; see [Services](/services) for the full list of
+keys for each backend.
+
+## Streaming writes
+
+For writing in chunks, use the `Writer` API instead of `writeOpRaw`:
+
+```haskell
+import OpenDAL
+
+main :: IO ()
+main = do
+  Right op <- newOperator "memory"
+  Right writer <- writerOpRaw op "output.bin" defaultWriterOption
+  writerWrite writer "chunk one "   -- returns Either OpenDALError ()
+  writerWrite writer "chunk two"
+  writerClose writer >>= \case      -- returns Either OpenDALError Metadata
+    Left err   -> putStrLn $ "close failed: " ++ show err
+    Right meta -> print (mContentLength meta)   -- 19
+```
+
+Use `appendWriterOption` instead of `defaultWriterOption` to append to an
+existing file (service-dependent; `Unsupported` is returned if the backend does
+not support it).
+
+## Listing entries
+
+`listOpRaw` returns a `Lister`; pull entries one at a time with `nextLister`
+until it returns `Right Nothing`:
+
+```haskell
+import OpenDAL
+
+collectAll :: Lister -> IO [String]
+collectAll lister = go []
+  where
+    go acc = nextLister lister >>= \case
+      Right (Just path) -> go (path : acc)
+      Right Nothing     -> return (reverse acc)
+      Left err          -> fail $ "lister error: " ++ show err
+
+main :: IO ()
+main = do
+  Right op <- newOperator "memory"
+  writeOpRaw op "dir/a.txt" "a" >>= print
+  writeOpRaw op "dir/b.txt" "b" >>= print
+  Right lister <- listOpRaw op "dir/"   -- path must end with /
+  paths <- collectAll lister
+  mapM_ putStrLn paths
+```
+
+`scanOpRaw` works the same way but lists recursively (flat traversal of a
+prefix).
+
+## Enable logging
+
+Pass a `co-log` action in `ocLogAction` to see per-request log messages:
+
+```haskell
+import Colog (simpleMessageAction)
+import OpenDAL
+
+main :: IO ()
+main = do
+  Right op <- newOperator "memory" { ocLogAction = Just simpleMessageAction }
+  writeOpRaw op "k" "v" >>= print
+```
diff --git a/website/docs/20-bindings/java.mdx b/website/docs/20-bindings/java.mdx
deleted file mode 100644
index fd9415b93..000000000
--- a/website/docs/20-bindings/java.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Java
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/java/README.md';
-
-<GitHubReadme basePath="bindings/java/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/java/01-overview.md b/website/docs/20-bindings/java/01-overview.md
new file mode 100644
index 000000000..44b95cf11
--- /dev/null
+++ b/website/docs/20-bindings/java/01-overview.md
@@ -0,0 +1,105 @@
+---
+title: Java
+sidebar_label: Overview
+slug: /bindings/java
+description: The OpenDAL Java binding — install it, see what it supports, and where to go next.
+---
+
+# Apache OpenDAL™ Java Binding
+
+A native Java binding for OpenDAL: access S3, GCS, Azure Blob, HDFS, the local
+filesystem, and [50+ more services](/services) through one API, with the
+performance of the Rust core underneath.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+:::note
+This binding has its own version number, independent of the Rust core. Check
+compatibility against the binding's version, not the core's.
+:::
+
+## Status
+
+Released and stable, published to
+[Maven Central](https://central.sonatype.com/search?q=opendal&smo=true) under
+the `org.apache.opendal` group.
+
+## Capabilities
+
+- **Sync and async** — use [`Operator`] for blocking calls or [`AsyncOperator`]
+  for `CompletableFuture`-based calls, with the same verbs.
+- **Typed or map-based config** — build from a typed `ServiceConfig` builder or
+  a plain `Map<String, String>`.
+- **All services included** — the native library bundles every backend; there
+  are no build flags to enable.
+- **Presigned URLs** for services that support them (async API).
+
+## Installation
+
+The binding ships a platform-specific native library, so you depend on the main
+artifact plus a classifier for your platform. The simplest setup uses an OS
+detector plugin to fill in the classifier automatically.
+
+### Maven
+
+```xml
+<build>
+<extensions>
+  <extension>
+    <groupId>kr.motd.maven</groupId>
+    <artifactId>os-maven-plugin</artifactId>
+    <version>1.7.0</version>
+  </extension>
+</extensions>
+</build>
+
+<dependencies>
+  <dependency>
+    <groupId>org.apache.opendal</groupId>
+    <artifactId>opendal</artifactId>
+    <version>${opendal.version}</version>
+  </dependency>
+  <dependency>
+    <groupId>org.apache.opendal</groupId>
+    <artifactId>opendal</artifactId>
+    <version>${opendal.version}</version>
+    <classifier>${os.detected.classifier}</classifier>
+  </dependency>
+</dependencies>
+```
+
+### Gradle
+
+```groovy
+plugins {
+    id "com.google.osdetector" version "1.7.3"
+}
+
+dependencies {
+    implementation "org.apache.opendal:opendal:$opendalVersion"
+    implementation "org.apache.opendal:opendal:$opendalVersion:$osdetector.classifier"
+}
+```
+
+On musl-based Linux distributions such as Alpine, use the `linux-x86_64-musl` or
+`linux-aarch_64-musl` classifier instead of the detected one.
+
+## Useful Links
+
+- **API reference**: [opendal.apache.org/docs/java](https://opendal.apache.org/docs/java/)
+- **Services & configuration**: [/services](/services)
+- **Upgrade guide**: [`upgrade.md`](https://github.com/apache/opendal/blob/main/bindings/java/upgrade.md)
+- **Source & examples**: [`bindings/java/`](https://github.com/apache/opendal/tree/main/bindings/java)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — a runnable program, then a real backend.
+2. [Connecting to your storage](./03-connecting.md) — build an operator for any service.
+3. [Common tasks](./04-tasks.md) — read, write, stream, list, presign, and more.
+4. [Going to production](./05-production.md) — retries, errors, and capability checks.
+
+[`Operator`]: https://opendal.apache.org/docs/java/org/apache/opendal/Operator.html
+[`AsyncOperator`]: https://opendal.apache.org/docs/java/org/apache/opendal/AsyncOperator.html
+</content>
+</invoke>
diff --git a/website/docs/20-bindings/java/02-getting-started.md b/website/docs/20-bindings/java/02-getting-started.md
new file mode 100644
index 000000000..26339ec84
--- /dev/null
+++ b/website/docs/20-bindings/java/02-getting-started.md
@@ -0,0 +1,97 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Run your first OpenDAL program in Java, then point it at a real storage backend.
+---
+
+# Getting started
+
+## Your first program
+
+This program builds an operator, writes a file, reads it back, inspects its
+metadata, and deletes it. It runs against the in-memory service with no
+credentials, so you can run it right after adding the dependency.
+
+The operator holds a native resource, so close it when you are done — a
+try-with-resources block does this for you:
+
+```java
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.opendal.Metadata;
+import org.apache.opendal.Operator;
+
+public class Main {
+    public static void main(String[] args) {
+        // Configure a service, then build an operator from it.
+        try (Operator op = Operator.of("memory", new HashMap<>())) {
+            // The same verbs work on every service.
+            op.write("hello.txt", "Hello, World!");
+
+            byte[] data = op.read("hello.txt");
+            System.out.println(new String(data));
+
+            Metadata meta = op.stat("hello.txt");
+            System.out.println("size = " + meta.getContentLength() + " bytes");
+
+            op.delete("hello.txt");
+        }
+    }
+}
+```
+
+`write` accepts a `String` or `byte[]`; `read` returns `byte[]`.
+
+## Point it at a real backend
+
+Only the service changes; the operations stay identical. The first argument is
+the scheme, and configuration is a `Map<String, String>`:
+
+```java
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.opendal.Operator;
+
+Map<String, String> conf = new HashMap<>();
+conf.put("bucket", "my-bucket");
+conf.put("region", "us-east-1");
+
+try (Operator op = Operator.of("s3", conf)) {
+    op.write("hello.txt", "Hello from S3!");
+    System.out.println(new String(op.read("hello.txt")));
+}
+```
+
+The native library bundles every service, so there is nothing extra to install.
+The next page, [Connecting to your storage](./03-connecting.md), covers typed
+config builders and credentials in depth; [Services](/services) lists every
+backend and its keys.
+
+## Sync vs async {#sync-vs-async}
+
+`Operator` runs every call synchronously. For non-blocking code, use
+[`AsyncOperator`]: it has the same verbs, but each returns a
+`CompletableFuture`:
+
+```java
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.opendal.AsyncOperator;
+
+Map<String, String> conf = new HashMap<>();
+conf.put("bucket", "my-bucket");
+conf.put("region", "us-east-1");
+
+try (AsyncOperator op = AsyncOperator.of("s3", conf)) {
+    op.write("hello.txt", "Hello, World!")
+        .thenCompose(v -> op.read("hello.txt"))
+        .thenAccept(data -> System.out.println(new String(data)))
+        .join();
+}
+```
+
+Call `op.blocking()` on an `AsyncOperator` to get a synchronous `Operator`
+backed by the same configuration.
+
+[`AsyncOperator`]: https://opendal.apache.org/docs/java/org/apache/opendal/AsyncOperator.html
+</content>
diff --git a/website/docs/20-bindings/java/03-connecting.md b/website/docs/20-bindings/java/03-connecting.md
new file mode 100644
index 000000000..a2fb1cf39
--- /dev/null
+++ b/website/docs/20-bindings/java/03-connecting.md
@@ -0,0 +1,92 @@
+---
+title: Connecting to your storage
+sidebar_label: Connecting to your storage
+description: Build an OpenDAL operator in Java for any backend — schemes, typed config, and credentials.
+---
+
+# Connecting to your storage
+
+Switching backends is a configuration change, not a code change. For the
+configuration keys of a specific service, see [Services](/services).
+
+Unlike the Rust core, the Java native library includes every service — there are
+no build flags. Once the dependency is on the classpath, every backend is
+available.
+
+## Build an operator
+
+### From a scheme and map
+
+The first argument is the service scheme; configuration follows as a
+`Map<String, String>` whose keys match the service's configuration keys:
+
+```java
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.opendal.Operator;
+
+Map<String, String> conf = new HashMap<>();
+conf.put("bucket", "my-bucket");
+conf.put("region", "us-east-1");
+conf.put("endpoint", "https://s3.amazonaws.com");
+
+Operator op = Operator.of("s3", conf);
+```
+
+A local filesystem operator roots every path under one directory:
+
+```java
+Map<String, String> conf = new HashMap<>();
+conf.put("root", "/tmp/opendal");
+
+Operator op = Operator.of("fs", conf);
+```
+
+### From a typed config builder
+
+`ServiceConfig` exposes a typed builder per service, so configuration keys are
+checked at compile time. Pass the built config to `Operator.of`:
+
+```java
+import org.apache.opendal.Operator;
+import org.apache.opendal.ServiceConfig;
+
+Operator op = Operator.of(
+    ServiceConfig.S3.builder()
+        .bucket("my-bucket")
+        .region("us-east-1")
+        .endpoint("https://s3.amazonaws.com")
+        .build());
+```
+
+Use `AsyncOperator.of(...)` with the same arguments for the async API.
+
+## Credentials
+
+Credentials are just more configuration keys. Set them in the map (or on the
+typed builder) — for example S3's `access_key_id` and `secret_access_key`:
+
+```java
+Map<String, String> conf = new HashMap<>();
+conf.put("bucket", "my-bucket");
+conf.put("region", "us-east-1");
+conf.put("access_key_id", System.getenv("AWS_ACCESS_KEY_ID"));
+conf.put("secret_access_key", System.getenv("AWS_SECRET_ACCESS_KEY"));
+
+Operator op = Operator.of("s3", conf);
+```
+
+Some services also load credentials from their platform's default sources (for
+S3, the standard AWS environment variables and profiles). See each service's
+page under [Services](/services) for the exact keys and credential behavior.
+
+Avoid hard-coding secrets in source. Read them from the environment or a secret
+manager and pass them in when you build the operator.
+
+## One operator per service and root
+
+An operator maps to one service and one root path. To work with two buckets or
+two roots, build two operators. Each operator owns a native resource, so close
+it when you are done — a try-with-resources block does this for you, or call
+`close()` explicitly.
+</content>
diff --git a/website/docs/20-bindings/java/04-tasks.md b/website/docs/20-bindings/java/04-tasks.md
new file mode 100644
index 000000000..9a31802e0
--- /dev/null
+++ b/website/docs/20-bindings/java/04-tasks.md
@@ -0,0 +1,151 @@
+---
+title: Common tasks
+sidebar_label: Common tasks
+description: Task-oriented recipes for OpenDAL in Java — read, write, stream, list, delete, copy, and presign.
+---
+
+# Common tasks
+
+Recipes for the things you actually do with storage. Each works on every
+service. They assume an `Operator op` built as in
+[Getting started](./02-getting-started.md). For full signatures, see the
+[API reference](https://opendal.apache.org/docs/java/).
+
+A few conventions used throughout:
+
+- Paths are relative to the operator's root; a trailing `/` means a directory
+  (`logs/app/` is a directory, `logs/app` is a file).
+- `read` returns `byte[]`; `write` takes a `String` or `byte[]`.
+- The synchronous `Operator` is shown here. `AsyncOperator` mirrors these verbs,
+  returning a `CompletableFuture` from each call.
+
+## Read a whole file
+
+```java
+byte[] data = op.read("path/to/file");
+String text = new String(data);
+```
+
+## Read part of a file
+
+`read(path, offset, length)` reads a range:
+
+```java
+byte[] data = op.read("path/to/file", 0, 1024);
+```
+
+## Stream a large file
+
+Don't load gigabytes into memory — read through an `InputStream` in chunks:
+
+```java
+import java.io.InputStream;
+
+try (InputStream in = op.createInputStream("big.bin")) {
+    byte[] buffer = new byte[8192];
+    int n;
+    while ((n = in.read(buffer)) != -1) {
+        // process buffer[0..n]
+    }
+}
+```
+
+## Write a whole file
+
+```java
+op.write("path/to/file", "Hello, World!");
+op.write("path/to/file", new byte[] {0, 1, 2, 3});
+```
+
+## Stream a large upload
+
+Write incrementally through an `OutputStream`; closing it commits the upload:
+
+```java
+import java.io.OutputStream;
+
+try (OutputStream out = op.createOutputStream("big.bin")) {
+    out.write(firstChunk);
+    out.write(secondChunk);
+}
+```
+
+## Check metadata
+
+```java
+import org.apache.opendal.Metadata;
+
+Metadata meta = op.stat("path/to/file");
+System.out.println(meta.getContentLength() + " bytes, dir = " + meta.isDir());
+```
+
+## List a directory
+
+`list` returns the entries under a path:
+
+```java
+import org.apache.opendal.Entry;
+
+for (Entry entry : op.list("dir/")) {
+    System.out.println(entry.getPath() + " " + entry.getMetadata().getMode());
+}
+```
+
+## Walk a tree recursively
+
+Pass a `ListOptions` with `recursive(true)`:
+
+```java
+import org.apache.opendal.Entry;
+import org.apache.opendal.ListOptions;
+
+ListOptions options = ListOptions.builder().recursive(true).build();
+for (Entry entry : op.list("dir/", options)) {
+    System.out.println(entry.getPath());
+}
+```
+
+## Delete a file or a whole tree
+
+```java
+op.delete("path/to/file");   // single path; succeeds even if missing
+op.removeAll("dir/");        // a path and everything under it
+```
+
+## Create a directory
+
+```java
+op.createDir("path/to/dir/");   // the trailing slash is required
+```
+
+## Copy and rename
+
+```java
+op.copy("from.txt", "to.txt");
+op.rename("old.txt", "new.txt");
+```
+
+Both require a service that supports them; see
+[capability checks](./05-production.md#capability-checks).
+
+## Generate a presigned URL
+
+Hand a time-limited URL to a third party so they can access an object without
+your credentials. Presigning is part of the **async** API:
+
+```java
+import java.time.Duration;
+import org.apache.opendal.AsyncOperator;
+import org.apache.opendal.PresignedRequest;
+
+try (AsyncOperator op = AsyncOperator.of("s3", conf)) {
+    PresignedRequest req =
+        op.presignRead("path/to/file", Duration.ofHours(1)).join();
+    System.out.println(req.getMethod() + " " + req.getUri());
+    System.out.println(req.getHeaders());
+}
+```
+
+`presignWrite` and `presignStat` produce presigned requests for the matching
+operations.
+</content>
diff --git a/website/docs/20-bindings/java/05-production.md b/website/docs/20-bindings/java/05-production.md
new file mode 100644
index 000000000..6cf7413ad
--- /dev/null
+++ b/website/docs/20-bindings/java/05-production.md
@@ -0,0 +1,88 @@
+---
+title: Going to production
+sidebar_label: Going to production
+description: Make OpenDAL robust in Java — retries, concurrency limits, error handling, and capability checks.
+---
+
+# Going to production
+
+The basics read and write data. Production code also has to survive transient
+failures, handle errors precisely, and adapt to what a backend supports.
+
+## Layers
+
+A layer wraps an operator to add cross-cutting behavior. `AsyncOperator.layer(...)`
+returns a new operator with the layer applied; the original is left unchanged,
+so close whichever ones you no longer use:
+
+```java
+import org.apache.opendal.AsyncOperator;
+import org.apache.opendal.layer.RetryLayer;
+
+AsyncOperator op = AsyncOperator.of("s3", conf)
+    .layer(RetryLayer.builder().maxTimes(3).build());
+```
+
+The Java binding exposes these layers from `org.apache.opendal.layer`:
+
+| Layer | What it does |
+|-------|--------------|
+| `RetryLayer` | Retries operations that fail with temporary errors, using exponential backoff. Configure with `jitter`, `factor`, `minDelay`, `maxDelay`, and `maxTimes`. |
+| `ConcurrentLimitLayer` | Caps the number of concurrent requests to the given number of permits. |
+| `CapabilityOverrideLayer` | Overrides the capability an operator reports for a service. |
+
+```java
+import org.apache.opendal.AsyncOperator;
+import org.apache.opendal.layer.ConcurrentLimitLayer;
+import org.apache.opendal.layer.RetryLayer;
+
+AsyncOperator op = AsyncOperator.of("s3", conf)
+    .layer(RetryLayer.builder().maxTimes(3).build())
+    .layer(new ConcurrentLimitLayer(16));
+```
+
+To run a layered operator synchronously, call `.blocking()` after applying the
+layers.
+
+## Error handling
+
+OpenDAL throws `OpenDALException`, a `RuntimeException` that carries a typed
+error code. Branch on `getCode()` instead of inspecting messages:
+
+```java
+import org.apache.opendal.OpenDALException;
+
+try {
+    byte[] data = op.read("maybe-missing.txt");
+} catch (OpenDALException e) {
+    if (e.getCode() == OpenDALException.Code.NotFound) {
+        // handle absence
+    } else {
+        throw e;
+    }
+}
+```
+
+Codes include `NotFound`, `PermissionDenied`, `AlreadyExists`,
+`ConditionNotMatch`, `RateLimited`, and `Unsupported`. Note that `delete` is
+idempotent — deleting a missing path succeeds rather than throwing `NotFound`.
+
+With `AsyncOperator`, the `OpenDALException` surfaces as the cause of the
+`CompletionException` thrown by `join()` / `get()`.
+
+## Capability checks {#capability-checks}
+
+Not every service supports every operation. Query what a backend can do through
+`op.info.capability` before calling optional operations like `copy`, `rename`,
+or presign:
+
+```java
+if (op.info.capability.copy) {
+    op.copy("a.txt", "b.txt");
+}
+```
+
+Calling an unsupported operation throws an `OpenDALException` with code
+`Unsupported`, so capability checks are an optimization, not a requirement for
+safety.
+</content>
diff --git a/website/docs/20-bindings/lua.mdx b/website/docs/20-bindings/lua.mdx
deleted file mode 100644
index 6801d452a..000000000
--- a/website/docs/20-bindings/lua.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Lua 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/lua/README.md';
-
-<GitHubReadme basePath="bindings/lua/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/lua/01-overview.md b/website/docs/20-bindings/lua/01-overview.md
new file mode 100644
index 000000000..c6e5cc9dc
--- /dev/null
+++ b/website/docs/20-bindings/lua/01-overview.md
@@ -0,0 +1,73 @@
+---
+title: Lua
+sidebar_label: Overview
+slug: /bindings/lua
+description: The OpenDAL Lua binding — experimental status, what it supports, how to build it, and where to go next.
+---
+
+# Apache OpenDAL™ Lua Binding
+
+A Lua binding for OpenDAL: access the local filesystem, in-memory storage, S3,
+and [50+ more services](/services) through one API, backed by the Rust core.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+:::warning Experimental / WIP
+This binding is **unreleased and experimental**. APIs may change without notice.
+It is not yet published to LuaRocks. Use it for evaluation and contribution, not
+for production workloads.
+:::
+
+## Status
+
+Experimental / work in progress. Not yet published to LuaRocks. The binding
+version is independent of the Rust core version.
+
+## Capabilities
+
+- **Blocking API** — all operations are synchronous from Lua's perspective.
+- **String-based I/O** — `write` takes a Lua string; `read` returns a Lua string.
+- **Core operations** — `read`, `write`, `delete`, `stat`, `rename`,
+  `create_dir`, `is_exist`.
+- **Metadata** — `content_length`, `is_file`, `is_dir` from `stat` results.
+- **Any service** that OpenDAL's Rust core supports, including `fs`, `memory`,
+  S3, GCS, and the full list at [/services](/services).
+
+Not yet available: async API, listing, streaming, presigned URLs, layers.
+
+## Build and install
+
+LuaRocks publishing is planned but not yet done. Build from source:
+
+```bash
+# Step 1: build the native library (default Lua version is 5.2).
+cd bindings/lua
+cargo build --package opendal-lua --release
+
+# Step 2: copy it to your Lua shared-library directory.
+# Adjust the path for your Lua version and OS.
+cp ../../target/release/libopendal_lua.so /usr/lib/lua/5.2/opendal.so
+```
+
+Alternatively, use LuaRocks with the bundled rockspec:
+
+```bash
+cd bindings/lua
+luarocks make
+```
+
+**Lua version**: the binding currently supports Lua 5.2 only.
+The default feature is `lua52`. Other Lua versions (`lua51`, `lua53`, `lua54`,
+`luajit`, `luau`) are not yet wired up as Cargo features in this binding.
+
+## Useful Links
+
+- **User guide**: [opendal.apache.org/docs/bindings/lua](https://opendal.apache.org/docs/bindings/lua)
+- **Services & configuration**: [/services](/services)
+- **Source & examples**: [`bindings/lua/`](https://github.com/apache/opendal/tree/main/bindings/lua)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — create an operator, write, read,
+   and inspect metadata.
diff --git a/website/docs/20-bindings/lua/02-getting-started.md b/website/docs/20-bindings/lua/02-getting-started.md
new file mode 100644
index 000000000..5bc597391
--- /dev/null
+++ b/website/docs/20-bindings/lua/02-getting-started.md
@@ -0,0 +1,123 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Build the OpenDAL Lua binding, then write, read, and inspect files with a real example.
+---
+
+# Getting started
+
+## Prerequisites
+
+Build the native library and install it for your Lua version before running any
+of the examples below. See [Overview](./01-overview.md) for the build steps.
+
+## Your first program
+
+This program creates an operator against the local filesystem, writes a file,
+reads it back, and checks its metadata. No credentials needed.
+
+```lua
+local opendal = require("opendal")
+
+-- Create an operator: first argument is the service scheme,
+-- second is a table of configuration key-value pairs.
+-- Wrap in pcall because errors are raised as Lua errors, not returned.
+local ok, op = pcall(opendal.operator.new, "fs", { root = "/tmp" })
+if not ok then
+    print("failed to create operator:", op)
+    return
+end
+
+-- Write a string to a path (relative to the operator root).
+local ok, err = pcall(function() op:write("hello.txt", "Hello, World!") end)
+if not ok then
+    print("write failed:", err)
+    return
+end
+
+-- Read it back. Returns the content as a Lua string on success.
+local ok, data = pcall(function() return op:read("hello.txt") end)
+if not ok then
+    print("read failed:", data)
+    return
+end
+print("content:", data)
+
+-- Inspect metadata.
+local ok, meta = pcall(function() return op:stat("hello.txt") end)
+if not ok then
+    print("stat failed:", meta)
+    return
+end
+print("size:", meta:content_length())   -- 13
+print("is_file:", meta:is_file())       -- true
+
+-- Clean up.
+op:delete("hello.txt")
+```
+
+## Error handling
+
+All methods raise a Lua error on failure (like `error()`). They do **not**
+return an `(result, error)` tuple. Use `pcall` or `xpcall` to catch errors:
+
+```lua
+local ok, result = pcall(function() return op:read("missing.txt") end)
+if not ok then
+    print("error:", result)   -- result holds the error message
+end
+```
+
+On success, each method returns its value directly:
+- `read` → `string`
+- `write`, `delete`, `create_dir`, `rename` → nothing (void)
+- `stat` → metadata table
+- `is_exist` → `bool`
+- `content_length` → `number`
+- `is_file`, `is_dir` → `bool`
+
+## Use a different service
+
+Only the constructor changes; every method is identical across services.
+
+```lua
+local opendal = require("opendal")
+
+-- In-memory storage — useful for tests; no credentials or paths needed.
+local ok, op = pcall(opendal.operator.new, "memory", {})
+if not ok then
+    print(op)
+    return
+end
+op:write("key", "value")
+print(op:read("key"))   -- "value"
+```
+
+For S3, GCS, Azblob, and every other backend, pass the service scheme and its
+configuration keys. See [/services](/services) for the full list and the
+exact key names each service accepts.
+
+## Available operations
+
+Errors are raised (not returned). Wrap calls in `pcall` when you need to handle failures.
+
+| Method | Signature | Returns on success |
+|--------|-----------|---------|
+| `op:read(path)` | `(string)` | `string` |
+| `op:write(path, bytes)` | `(string, string)` | _(nothing)_ |
+| `op:delete(path)` | `(string)` | _(nothing)_ |
+| `op:stat(path)` | `(string)` | `metadata` |
+| `op:is_exist(path)` | `(string)` | `bool` |
+| `op:rename(src, dst)` | `(string, string)` | _(nothing)_ |
+| `op:create_dir(path)` | `(string)` | _(nothing)_ |
+
+`metadata` returned by `stat` exposes three methods:
+
+| Method | Returns on success |
+|--------|---------|
+| `meta:content_length()` | `number` |
+| `meta:is_file()` | `bool` |
+| `meta:is_dir()` | `bool` |
+
+Paths are relative to the operator's `root`. A trailing `/` denotes a
+directory (required for `create_dir`).
diff --git a/website/docs/20-bindings/nodejs.mdx b/website/docs/20-bindings/nodejs.mdx
deleted file mode 100644
index c55128fb5..000000000
--- a/website/docs/20-bindings/nodejs.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Node.js
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/nodejs/README.md';
-
-<GitHubReadme basePath="bindings/nodejs/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/nodejs/01-overview.md b/website/docs/20-bindings/nodejs/01-overview.md
new file mode 100644
index 000000000..9412a5b3f
--- /dev/null
+++ b/website/docs/20-bindings/nodejs/01-overview.md
@@ -0,0 +1,59 @@
+---
+title: Node.js
+sidebar_label: Overview
+slug: /bindings/nodejs
+description: The OpenDAL Node.js binding — install it, see what it supports, and where to go next.
+---
+
+# Apache OpenDAL™ Node.js Binding
+
+A native Node.js binding for OpenDAL: access S3, GCS, Azure Blob, HDFS, the local
+filesystem, and [50+ more services](/services) through one API, with the
+performance of the Rust core underneath.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+:::note
+This binding has its own version number, independent of the Rust core. Check
+compatibility against the binding's version, not the core's.
+:::
+
+## Status
+
+Released and stable, published to [npm](https://www.npmjs.com/package/opendal).
+
+## Capabilities
+
+- **Promise-based async API** — every operation returns a `Promise`; `await` it.
+- **Synchronous variants** — each verb has a `*Sync` form (`readSync`,
+  `writeSync`, …) for blocking code.
+- **Streaming** reads and writes via Node `Readable`/`Writable` streams for data
+  that does not fit in memory.
+- **All services included** — the package bundles every backend; there are no
+  build flags to enable.
+- **Composable layers** for retry, timeout, logging, throttle, and concurrency
+  limits — see [Going to production](./05-production.md).
+- **Presigned URLs** for services that support them (S3, GCS, Azblob, …).
+
+## Installation
+
+```shell
+npm install opendal
+```
+
+## Useful Links
+
+- **API reference**: [opendal.apache.org/docs/nodejs](https://opendal.apache.org/docs/nodejs/)
+- **Services & configuration**: [/services](/services)
+- **Upgrade guide**: [`upgrade.md`](https://github.com/apache/opendal/blob/main/bindings/nodejs/upgrade.md)
+- **Source & examples**: [`bindings/nodejs/`](https://github.com/apache/opendal/tree/main/bindings/nodejs) · [`bindings/nodejs/examples/`](https://github.com/apache/opendal/tree/main/bindings/nodejs/examples)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — a runnable program, then a real backend.
+2. [Connecting to your storage](./03-connecting.md) — build an operator for any service.
+3. [Common tasks](./04-tasks.md) — read, write, stream, list, copy, and presign.
+4. [Going to production](./05-production.md) — retry, timeouts, errors, observability.
+</content>
+</invoke>
diff --git a/website/docs/20-bindings/nodejs/02-getting-started.md b/website/docs/20-bindings/nodejs/02-getting-started.md
new file mode 100644
index 000000000..248eb8492
--- /dev/null
+++ b/website/docs/20-bindings/nodejs/02-getting-started.md
@@ -0,0 +1,84 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Run your first OpenDAL program in Node.js, then point it at a real storage backend.
+---
+
+# Getting started
+
+## Your first program
+
+This program builds an operator, writes a file, reads it back, inspects its
+metadata, and deletes it. It runs against the in-memory service with no
+credentials, so you can run it right after installing.
+
+```shell
+npm install opendal
+```
+
+```javascript
+import { Operator } from "opendal";
+
+async function main() {
+  // Configure a service, then build an operator from it.
+  const op = new Operator("memory");
+
+  // The same verbs work on every service.
+  await op.write("hello.txt", "Hello, World!");
+
+  const bs = await op.read("hello.txt");        // returns a Buffer
+  console.log(new TextDecoder().decode(bs));
+
+  const meta = await op.stat("hello.txt");
+  console.log(`size = ${meta.contentLength} bytes`);
+
+  await op.delete("hello.txt");
+}
+
+main();
+```
+
+`write` accepts a `string` or a `Buffer`; `read` returns a `Buffer`. Decode it
+with `new TextDecoder().decode(bs)` or `bs.toString()`.
+
+## Point it at a real backend
+
+Only the service changes; the operations stay identical. The scheme is the first
+argument, and configuration is an options object:
+
+```javascript
+import { Operator } from "opendal";
+
+const op = new Operator("s3", {
+  bucket: "my-bucket",
+  region: "us-east-1",
+});
+
+await op.write("hello.txt", "Hello from S3!");
+console.log((await op.read("hello.txt")).toString());
+```
+
+The package bundles every service, so there is nothing extra to install. The next
+page, [Connecting to your storage](./03-connecting.md), covers configuration and
+credentials in depth; [Services](/services) lists every backend and its keys.
+
+## Async and sync {#blocking-vs-async}
+
+OpenDAL's Node.js API is promise-based: every operation returns a `Promise` you
+`await`. For synchronous code, each verb has a `*Sync` variant that blocks and
+returns the value directly:
+
+```javascript
+import { Operator } from "opendal";
+
+const op = new Operator("memory");
+
+op.writeSync("hello.txt", "Hello, World!");
+const bs = op.readSync("hello.txt");
+console.log(bs.toString());
+```
+
+The sync variants cover the same operations (`statSync`, `listSync`,
+`deleteSync`, …). Use the async API in servers and request handlers; reach for
+the sync variants only in scripts and startup code where blocking is acceptable.
+</content>
diff --git a/website/docs/20-bindings/nodejs/03-connecting.md b/website/docs/20-bindings/nodejs/03-connecting.md
new file mode 100644
index 000000000..ddc0a4324
--- /dev/null
+++ b/website/docs/20-bindings/nodejs/03-connecting.md
@@ -0,0 +1,82 @@
+---
+title: Connecting to your storage
+sidebar_label: Connecting to your storage
+description: Build an OpenDAL operator in Node.js for any backend — schemes, configuration, URIs, and credentials.
+---
+
+# Connecting to your storage
+
+Switching backends is a configuration change, not a code change. For the
+configuration keys of a specific service, see [Services](/services).
+
+Unlike the Rust core, the Node.js package includes every service — there are no
+build flags. Once `opendal` is installed, every backend is available.
+
+## Build an operator
+
+The first argument is the service scheme; configuration follows as an options
+object. Option keys are `snake_case`, matching each service's documented keys:
+
+```javascript
+import { Operator } from "opendal";
+
+const op = new Operator("s3", {
+  bucket: "my-bucket",
+  region: "us-east-1",
+  endpoint: "https://s3.amazonaws.com",
+});
+```
+
+A local filesystem operator roots every path under one directory:
+
+```javascript
+const op = new Operator("fs", { root: "/tmp/opendal" });
+```
+
+## From a URI
+
+You can also encode the scheme and configuration in a single URI. Extra options
+override or supplement values from the URI:
+
+```javascript
+import { Operator } from "opendal";
+
+const op = Operator.fromUri("memory://");
+const op2 = Operator.fromUri("s3://my-bucket/", { region: "us-east-1" });
+```
+
+## Credentials
+
+Credentials are just more configuration keys. Set them in the options object —
+for example S3's `access_key_id` and `secret_access_key`:
+
+```javascript
+const op = new Operator("s3", {
+  bucket: "my-bucket",
+  region: "us-east-1",
+  access_key_id: "...",
+  secret_access_key: "...",
+});
+```
+
+Some services also load credentials from their platform's default sources (for
+S3, the standard AWS environment variables and profiles). See each service's
+page under [Services](/services) for the exact keys and credential behavior.
+
+Avoid hard-coding secrets in source. Read them from the environment or a secret
+manager and pass them in when you build the operator:
+
+```javascript
+const op = new Operator("s3", {
+  bucket: "my-bucket",
+  region: "us-east-1",
+  access_key_id: process.env.AWS_ACCESS_KEY_ID,
+  secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,
+});
+```
+
+## One operator per service and root
+
+An operator maps to one service and one root path. To work with two buckets or
+two roots, build two operators — they are lightweight handles.
+</content>
diff --git a/website/docs/20-bindings/nodejs/04-tasks.md b/website/docs/20-bindings/nodejs/04-tasks.md
new file mode 100644
index 000000000..169d9a1ae
--- /dev/null
+++ b/website/docs/20-bindings/nodejs/04-tasks.md
@@ -0,0 +1,166 @@
+---
+title: Common tasks
+sidebar_label: Common tasks
+description: Task-oriented recipes for OpenDAL in Node.js — read, write, stream, list, delete, copy, rename, and presign.
+---
+
+# Common tasks
+
+Recipes for the things you actually do with storage. Each works on every
+service. They assume an `op` built as in
+[Getting started](./02-getting-started.md). For full signatures, see the
+[API reference](https://opendal.apache.org/docs/nodejs/).
+
+A few conventions used throughout:
+
+- Paths are relative to the operator's root; a trailing `/` means a directory
+  (`logs/app/` is a directory, `logs/app` is a file).
+- `read` returns a `Buffer`; `write` takes a `string` or a `Buffer`.
+- Every verb has a `*Sync` variant (`readSync`, `writeSync`, …) for blocking code.
+
+## Read a whole file
+
+```javascript
+const bs = await op.read("path/to/file");   // Buffer
+const text = bs.toString();
+```
+
+## Read part of a file
+
+Pass an `offset` and `size` (both `bigint`):
+
+```javascript
+const bs = await op.read("path/to/file", { offset: 0n, size: 1024n });
+```
+
+## Stream a large file
+
+Don't buffer gigabytes in memory — open a reader and consume it as a Node
+`Readable` stream:
+
+```javascript
+import { pipeline } from "node:stream/promises";
+import { createWriteStream } from "node:fs";
+
+const reader = await op.reader("big.bin");
+const rs = reader.createReadStream();
+await pipeline(rs, createWriteStream("/tmp/big.bin"));
+```
+
+## Write a whole file
+
+```javascript
+await op.write("path/to/file", "Hello, World!");
+await op.write("path/to/file", Buffer.from([0, 1, 2, 3]));
+```
+
+Pass write options as a third argument, for example a content type:
+
+```javascript
+await op.write("path/to/file", data, { contentType: "application/json" });
+```
+
+## Stream a large upload
+
+Open a writer and drive it with a Node `Writable` stream. Closing the stream
+commits the upload:
+
+```javascript
+import { pipeline } from "node:stream/promises";
+import { createReadStream } from "node:fs";
+
+const writer = await op.writer("big.bin");
+const ws = writer.createWriteStream();
+await pipeline(createReadStream("/tmp/big.bin"), ws);
+```
+
+## Upload concurrently
+
+For large objects on services with multipart support, upload parts in parallel:
+
+```javascript
+await op.write("big.bin", data, { concurrent: 8 });
+```
+
+## Check existence and metadata
+
+```javascript
+if (await op.exists("path/to/file")) {
+  const meta = await op.stat("path/to/file");
+  console.log(meta.contentLength, meta.isDirectory());
+}
+```
+
+`Metadata` exposes getters like `contentLength`, `contentType`, `etag`, and
+`lastModified`, plus `isFile()` and `isDirectory()`.
+
+## List a directory
+
+`list` returns an array of entries under a path. The path must end with `/`:
+
+```javascript
+const entries = await op.list("dir/");
+for (const entry of entries) {
+  console.log(entry.path(), entry.metadata().isFile());
+}
+```
+
+For large directories, stream entries with a lister instead of collecting them
+into an array:
+
+```javascript
+const lister = await op.lister("dir/");
+let entry;
+while ((entry = await lister.next()) !== null) {
+  console.log(entry.path());
+}
+```
+
+## Walk a tree recursively
+
+```javascript
+const entries = await op.list("dir/", { recursive: true });
+```
+
+`op.lister("dir/", { recursive: true })` gives the streaming equivalent.
+
+## Delete a file or a whole tree
+
+```javascript
+await op.delete("path/to/file");      // single path; succeeds even if missing
+await op.remove(["a.txt", "b.txt"]);  // a batch of paths
+await op.removeAll("dir/");           // a path and everything under it
+```
+
+`delete` is idempotent — deleting a missing path succeeds rather than failing.
+
+## Create a directory
+
+```javascript
+await op.createDir("path/to/dir/");   // the trailing slash is required
+```
+
+## Copy and rename
+
+```javascript
+await op.copy("from.txt", "to.txt");
+await op.rename("old.txt", "new.txt");
+```
+
+Both require a service that supports them; see
+[capability checks](./05-production.md#capability-checks).
+
+## Generate a presigned URL
+
+Hand a time-limited URL to a third party so they can access an object without
+your credentials. The `expires` argument is in seconds:
+
+```javascript
+const req = await op.presignRead("path/to/file", 3600);  // expires in 3600s
+console.log(req.method, req.url);
+console.log(req.headers);
+```
+
+`presignWrite`, `presignStat`, and `presignDelete` are available too, on
+services that support presigning.
+</content>
diff --git a/website/docs/20-bindings/nodejs/05-production.md b/website/docs/20-bindings/nodejs/05-production.md
new file mode 100644
index 000000000..41c21d36f
--- /dev/null
+++ b/website/docs/20-bindings/nodejs/05-production.md
@@ -0,0 +1,120 @@
+---
+title: Going to production
+sidebar_label: Going to production
+description: Make OpenDAL robust in Node.js — retries, timeouts, throttling, concurrency limits, error handling, and observability.
+---
+
+# Going to production
+
+The basics read and write data. Production code also has to survive transient
+failures, bound its resource use, observe what happens, and react to errors.
+OpenDAL handles the first three with **layers** and gives you **capability
+checks** for the rest.
+
+## Layers
+
+A layer wraps an operator to add cross-cutting behavior without touching your
+storage code. Construct a layer, configure it, then apply it with
+`op.layer(layer.build())`, which returns an operator with the layer applied:
+
+```javascript
+import { Operator, RetryLayer } from "opendal";
+
+const retry = new RetryLayer();
+retry.maxTimes = 3;
+retry.jitter = true;
+
+// layer() returns a NEW operator; capture it. It does not mutate in place.
+const op = new Operator("s3", { bucket: "my-bucket", region: "us-east-1" })
+  .layer(retry.build());
+```
+
+The Node.js binding exposes these layers:
+
+| Layer | What it does |
+|-------|--------------|
+| `RetryLayer` | Retries temporary failures with exponential backoff (`maxTimes`, `jitter`, `factor`, `minDelay`, `maxDelay`). |
+| `TimeoutLayer` | Bounds slow operations (`timeout` for non-IO ops, `ioTimeout` for IO ops; both in ms). |
+| `ConcurrentLimitLayer` | Caps concurrent operations (`new ConcurrentLimitLayer(permits)`, plus `httpPermits`). |
+| `ThrottleLayer` | Rate-limits bandwidth (`new ThrottleLayer(bandwidth, burst)`). |
+| `LoggingLayer` | Logs every operation through Rust's `log` facade. |
+
+## Combining layers
+
+Apply layers one after another. A robust setup wraps logging on the outside and
+retry on the inside, so each retry attempt is logged:
+
+```javascript
+import { Operator, LoggingLayer, TimeoutLayer, RetryLayer } from "opendal";
+
+const logging = new LoggingLayer();
+
+const timeout = new TimeoutLayer();
+timeout.timeout = 30000;    // 30s for non-IO ops (ms)
+timeout.ioTimeout = 10000;  // 10s for IO ops (ms)
+
+const retry = new RetryLayer();
+retry.maxTimes = 3;
+retry.jitter = true;
+
+// Each layer() returns a new operator; chain them. The last applied is the
+// outermost, so logging wraps timeout wraps retry — every retry is logged.
+const op = new Operator("s3", { bucket: "my-bucket", region: "us-east-1" })
+  .layer(retry.build())
+  .layer(timeout.build())
+  .layer(logging.build());
+```
+
+## Observability
+
+`LoggingLayer` emits structured logs through Rust's `log` facade. Enable output
+with the `RUST_LOG` environment variable:
+
+```bash
+# Show debug logs
+RUST_LOG=debug node app.js
+
+# Show only OpenDAL service logs
+RUST_LOG=opendal::services=debug node app.js
+```
+
+## Error handling
+
+Operations reject (async) or throw (sync) with a standard `Error`. The OpenDAL
+error kind appears in `error.message` — branch on it instead of assuming a call
+succeeded:
+
+```javascript
+try {
+  const bs = await op.read("maybe-missing.txt");
+  // ...
+} catch (err) {
+  if (err.message.includes("NotFound")) {
+    // handle absence
+  } else {
+    throw err;
+  }
+}
+```
+
+Common kinds that appear in messages include `NotFound`, `PermissionDenied`,
+`AlreadyExists`, `ConditionNotMatch`, `RateLimited`, and `Unsupported`. Note that
+`delete` is idempotent — deleting a missing path succeeds rather than failing.
+
+## Capability checks {#capability-checks}
+
+Not every service supports every operation. Query what a backend can do with
+`capability()` before calling optional operations like `copy`, `rename`, or
+presign:
+
+```javascript
+const cap = op.capability();
+if (cap.copy) {
+  await op.copy("a.txt", "b.txt");
+}
+```
+
+`Capability` exposes boolean getters such as `read`, `write`, `copy`, `rename`,
+`list`, and `presign`. Calling an unsupported operation throws an error, so
+capability checks are an optimization, not a requirement for safety.
+</content>
diff --git a/website/docs/20-bindings/ocaml.mdx b/website/docs/20-bindings/ocaml.mdx
deleted file mode 100644
index 52be76b65..000000000
--- a/website/docs/20-bindings/ocaml.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: OCaml 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/ocaml/README.md';
-
-<GitHubReadme basePath="bindings/ocaml/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/ocaml/01-overview.md b/website/docs/20-bindings/ocaml/01-overview.md
new file mode 100644
index 000000000..5e16a8e0b
--- /dev/null
+++ b/website/docs/20-bindings/ocaml/01-overview.md
@@ -0,0 +1,76 @@
+---
+title: OCaml
+sidebar_label: Overview
+slug: /bindings/ocaml
+description: The OpenDAL OCaml binding — experimental WIP, build it from source, and see what it exposes.
+---
+
+# Apache OpenDAL™ OCaml Binding
+
+An OCaml binding for OpenDAL: access S3, GCS, Azure Blob, the local
+filesystem, and [50+ more services](/services) through one API, backed by the
+Rust core.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, and operation are the same four ideas in every language.
+
+:::warning Experimental / Work in Progress
+This binding is **unreleased and experimental**. APIs may change without notice.
+It is not published to opam. Build from source only.
+:::
+
+## Status
+
+Unreleased — work in progress. OCaml 4.10 or later (OCaml 5 not yet supported)
+due to an upstream constraint in `ocaml-rs`.
+
+## Capabilities
+
+- **Blocking API** — all operations are synchronous; no async runtime required.
+- **Full result-type error handling** — every fallible function returns
+  `('a, string) result`; no exceptions for storage errors.
+- **Streaming readers and writers** — `Operator.reader` / `Operator.writer` for
+  data that does not fit in memory.
+- **Directory listing** — batch (`Operator.list`) and streaming
+  (`Operator.lister` + `Operator.Lister.next`) variants.
+- **Capability introspection** — check what a backend supports at runtime via
+  `Operator.info` and the `Operator.Capability` module.
+- **Services**: every backend the Rust core supports is available; the binding
+  is built against the full Rust crate.
+
+## Building from source
+
+The binding has no opam release yet. Install the toolchain, then build with dune:
+
+```bash
+# 1. Install opam (if not already installed)
+bash -c "sh <(curl -fsSL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)"
+
+# 2. Create a 4.x switch (OCaml 5 is not supported)
+opam switch create opendal-ocaml4.14 ocaml-base-compiler.4.14.0
+eval $(opam env)
+
+# 3. Install development tools (optional but recommended)
+opam install -y utop odoc ounit2 ocaml-lsp-server ocamlformat ocamlformat-rpc
+
+# 4. Build
+cd bindings/ocaml
+dune build
+
+# 5. Run tests
+dune test
+```
+
+You also need a working Rust toolchain (stable) for the `build.rs` step that
+compiles the Rust core into a native library.
+
+## Useful Links
+
+- **Source**: [`bindings/ocaml/`](https://github.com/apache/opendal/tree/main/bindings/ocaml)
+- **Services & configuration**: [/services](/services)
+- **OCaml module reference**: `lib/operator.mli` in the source tree
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — create an operator, read, write,
+   and handle errors.
diff --git a/website/docs/20-bindings/ocaml/02-getting-started.md b/website/docs/20-bindings/ocaml/02-getting-started.md
new file mode 100644
index 000000000..37bd5237b
--- /dev/null
+++ b/website/docs/20-bindings/ocaml/02-getting-started.md
@@ -0,0 +1,175 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Create an operator, read and write files, and handle errors in the OCaml binding.
+---
+
+# Getting started
+
+:::warning Experimental / Work in Progress
+This binding is unreleased and experimental. Build from source before following
+this guide. See [Overview](./01-overview.md) for the build steps.
+:::
+
+## Your first program
+
+The example below uses the `fs` (local filesystem) service, which requires no
+credentials. Open the project in utop or add it to a dune executable that
+depends on the `opendal` library.
+
+```ocaml
+open Opendal
+
+(* Create an operator for the local filesystem, rooted at /tmp *)
+let () =
+  match Operator.new_operator "fs" [ ("root", "/tmp") ] with
+  | Error err -> Printf.eprintf "Failed to create operator: %s\n" err
+  | Ok op ->
+
+  (* Write a file *)
+  (match Operator.write op "hello.txt" (Bytes.of_string "Hello, World!") with
+  | Error err -> Printf.eprintf "Write failed: %s\n" err
+  | Ok () ->
+
+  (* Read it back — result is a char array *)
+  (match Operator.read op "hello.txt" with
+  | Error err -> Printf.eprintf "Read failed: %s\n" err
+  | Ok content ->
+      let text = content |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string in
+      print_endline text;   (* Hello, World! *)
+
+  (* Inspect metadata *)
+  (match Operator.stat op "hello.txt" with
+  | Error err -> Printf.eprintf "Stat failed: %s\n" err
+  | Ok meta ->
+      Printf.printf "size = %Ld bytes\n" (Operator.Metadata.content_length meta);
+      Printf.printf "is_file = %b\n" (Operator.Metadata.is_file meta));
+
+  (* Delete the file *)
+  (match Operator.delete op "hello.txt" with
+  | Error err -> Printf.eprintf "Delete failed: %s\n" err
+  | Ok () -> print_endline "Deleted.")))
+```
+
+`Operator.read` returns `(char array, string) result`. Convert to `Bytes` with
+`Array.to_seq |> Bytes.of_seq` and then to `string` with `Bytes.to_string`.
+
+## Point it at a real backend
+
+Only the scheme and config map change; every operation stays the same:
+
+```ocaml
+open Opendal
+
+let () =
+  let config =
+    [ ("bucket", "my-bucket")
+    ; ("region", "us-east-1")
+    ; ("access_key_id", "...")
+    ; ("secret_access_key", "...")
+    ]
+  in
+  match Operator.new_operator "s3" config with
+  | Error err -> Printf.eprintf "Failed: %s\n" err
+  | Ok op ->
+      ignore (Operator.write op "hello.txt" (Bytes.of_string "Hello from S3!"))
+```
+
+See [Services](/services) for every backend and its configuration keys.
+
+## Error handling
+
+Every fallible function returns `('a, string) result`. The idiomatic pattern is
+`match`:
+
+```ocaml
+match Operator.read op "maybe-missing.txt" with
+| Ok content ->
+    (* use content *)
+    let text = content |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string in
+    print_endline text
+| Error msg ->
+    (* msg is a string description of the error *)
+    Printf.eprintf "Error: %s\n" msg
+```
+
+You can also use `Result.get_ok` to raise an exception on error (useful in
+tests and scripts):
+
+```ocaml
+let content = Operator.read op "file.txt" |> Result.get_ok in
+```
+
+## Streaming reads and writes
+
+For large files, avoid loading everything into memory:
+
+```ocaml
+(* Streaming read — read 1 KiB starting at byte offset 0 *)
+match Operator.reader op "big.bin" with
+| Error err -> Printf.eprintf "Reader failed: %s\n" err
+| Ok r ->
+    let buf = Bytes.create 1024 in
+    (match Operator.Reader.pread r buf 0L with
+    | Ok n -> Printf.printf "Read %d bytes\n" n
+    | Error err -> Printf.eprintf "pread failed: %s\n" err)
+```
+
+```ocaml
+(* Streaming write — write in chunks, then close to commit *)
+match Operator.writer op "big.bin" with
+| Error err -> Printf.eprintf "Writer failed: %s\n" err
+| Ok w ->
+    ignore (Operator.Writer.write w (Bytes.of_string "first chunk\n"));
+    ignore (Operator.Writer.write w (Bytes.of_string "second chunk\n"));
+    (match Operator.Writer.close w with
+    | Ok _meta -> print_endline "Upload complete"
+    | Error err -> Printf.eprintf "Close failed: %s\n" err)
+```
+
+`Writer.close` must be called to commit the upload; it returns metadata for the
+written object.
+
+## Listing a directory
+
+`list` loads all entries into an array; `lister` streams them one at a time:
+
+```ocaml
+(* Batch listing *)
+match Operator.list op "data/" with
+| Error err -> Printf.eprintf "List failed: %s\n" err
+| Ok entries ->
+    Array.iter
+      (fun entry -> Printf.printf "%s\n" (Operator.Entry.name entry))
+      entries
+```
+
+```ocaml
+(* Streaming listing — memory-efficient for large directories *)
+match Operator.lister op "data/" with
+| Error err -> Printf.eprintf "Lister failed: %s\n" err
+| Ok lst ->
+    let rec loop () =
+      match Operator.Lister.next lst with
+      | Ok (Some entry) ->
+          Printf.printf "%s\n" (Operator.Entry.name entry);
+          loop ()
+      | Ok None -> ()   (* end of listing *)
+      | Error err -> Printf.eprintf "Next failed: %s\n" err
+    in
+    loop ()
+```
+
+## Checking capabilities
+
+Not every service supports every operation. Query capabilities before calling
+optional operations like `copy` or `rename`:
+
+```ocaml
+let info = Operator.info op in
+let cap  = Operator.OperatorInfo.capability info in
+if Operator.Capability.copy cap then
+  ignore (Operator.copy op "source.txt" "backup.txt")
+else
+  print_endline "copy not supported by this backend"
+```
diff --git a/website/docs/20-bindings/php.mdx b/website/docs/20-bindings/php.mdx
deleted file mode 100644
index f66190dea..000000000
--- a/website/docs/20-bindings/php.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: PHP 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/php/README.md';
-
-<GitHubReadme basePath="bindings/php/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/php/01-overview.md b/website/docs/20-bindings/php/01-overview.md
new file mode 100644
index 000000000..0755017b9
--- /dev/null
+++ b/website/docs/20-bindings/php/01-overview.md
@@ -0,0 +1,102 @@
+---
+title: PHP
+sidebar_label: Overview
+slug: /bindings/php
+description: The OpenDAL PHP binding — experimental extension built from Rust source, how to install it, and where to go next.
+---
+
+# Apache OpenDAL™ PHP Binding
+
+A PHP extension for OpenDAL: access S3, GCS, Azure Blob, the local
+filesystem, and [50+ more services](/services) through one synchronous API,
+with the performance of the Rust core underneath.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, and operation are the same four ideas in every language.
+
+## Status
+
+**Experimental / work in progress.** The binding is unreleased and has no
+published package. It is built from source as a native PHP extension using
+[ext-php-rs](https://github.com/davidcole1340/ext-php-rs). The API may change
+without notice. It is not ready for production use.
+
+## Capabilities
+
+- **Synchronous API** — all calls block; there is no async variant.
+- **String and binary writes** — `write` for UTF-8 strings, `write_binary` for
+  arbitrary byte arrays.
+- **Metadata** — `stat` returns content length, content type, ETag, MD5, and
+  entry mode (file or directory).
+- **Directory operations** — `create_dir` creates directories recursively.
+- **All services** that OpenDAL supports are available as long as they are
+  enabled at compile time; see [Services](/services).
+
+## Building and installing the extension
+
+The extension cannot be installed with `pecl` or through Composer. You must
+build it from the repository source.
+
+**Requirements**
+
+- PHP 8.1+
+- Rust and Cargo — see [rust-lang.org/tools/install](https://www.rust-lang.org/tools/install)
+
+**Step 1 — Clone the repository**
+
+```bash
+git clone https://github.com/apache/opendal.git
+```
+
+**Step 2 — Build the extension**
+
+```bash
+cd opendal/bindings/php
+cargo build
+```
+
+Add `--release` for production builds.
+
+**Step 3 — Enable the extension**
+
+Copy the compiled library to PHP's extension directory and add the `extension=`
+directive to `php.ini`:
+
+```bash
+# Linux
+cp target/debug/libopendal_php.so $(php -r "echo ini_get('extension_dir');")/libopendal_php.so
+echo "extension=libopendal_php.so" >> $(php -r "echo php_ini_loaded_file();")
+
+# macOS
+cp target/debug/libopendal_php.dylib $(php -r "echo ini_get('extension_dir');")/libopendal_php.dylib
+echo "extension=libopendal_php.dylib" >> $(php -r "echo php_ini_loaded_file();")
+
+# Windows
+cp target/debug/libopendal_php.dll $(php -r "echo ini_get('extension_dir');")/libopendal_php.dll
+echo "extension=libopendal_php.dll" >> $(php -r "echo php_ini_loaded_file();")
+```
+
+Alternatively, use `cargo-php` to build and install in one step:
+
+```bash
+cargo install cargo-php
+cd opendal/bindings/php
+cargo php install
+```
+
+**Verify the installation**
+
+```bash
+php -m | grep opendal-php
+```
+
+## Useful Links
+
+- **Source & examples**: [`bindings/php/`](https://github.com/apache/opendal/tree/main/bindings/php)
+- **Services & configuration**: [/services](/services)
+- **ext-php-rs**: [github.com/davidcole1340/ext-php-rs](https://github.com/davidcole1340/ext-php-rs)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — construct an operator, then write,
+   read, inspect, and delete a file.
diff --git a/website/docs/20-bindings/php/02-getting-started.md b/website/docs/20-bindings/php/02-getting-started.md
new file mode 100644
index 000000000..9cdee3704
--- /dev/null
+++ b/website/docs/20-bindings/php/02-getting-started.md
@@ -0,0 +1,120 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Construct an OpenDAL operator in PHP, then write, read, inspect, and delete a file.
+---
+
+# Getting started
+
+This page assumes the `opendal-php` extension is installed and enabled. If it
+is not, follow the [build and install steps](./01-overview.md#building-and-installing-the-extension) first.
+
+## Your first program
+
+The example below builds an operator against the in-memory service — no
+credentials or configuration needed — then writes a file, reads it back,
+inspects its metadata, and deletes it.
+
+```php
+use OpenDAL\Operator;
+
+$op = new Operator("memory", []);
+
+// Write a UTF-8 string.
+$op->write("hello.txt", "Hello, World!");
+
+// Read it back. Returns a string (binary-safe).
+$content = $op->read("hello.txt");
+echo $content; // Hello, World!
+
+// Inspect metadata.
+$meta = $op->stat("hello.txt");
+echo $meta->content_length; // 13
+
+// Delete. Succeeds even if the path does not exist.
+$op->delete("hello.txt");
+```
+
+## Point it at a real backend
+
+Only the first two arguments to `Operator` change; the operations are identical
+on every service.
+
+```php
+use OpenDAL\Operator;
+
+// Local filesystem rooted at /tmp
+$op = new Operator("fs", ["root" => "/tmp"]);
+
+$op->write("hello.txt", "Hello from the filesystem!");
+echo $op->read("hello.txt");
+```
+
+The first argument is the service scheme (`"fs"`, `"s3"`, `"memory"`, etc.).
+The second argument is an associative array of service-specific configuration
+keys. See [Services](/services) for the full list of schemes and their keys.
+
+## Check existence before reading
+
+`is_exist` returns `1` if the path exists and `0` otherwise — both are PHP **int**,
+not bool. The same applies to `mode->is_file` and `mode->is_dir`. Using them
+in an `if` or ternary works because PHP treats `1` as truthy and `0` as falsy,
+but `=== true` will always be `false`. Prefer `=== 1` for strict checks.
+
+```php
+if ($op->is_exist("hello.txt")) {
+    $meta = $op->stat("hello.txt");
+    echo $meta->content_length . " bytes\n";
+    // is_file returns 1 (int) for files, 0 for directories/other
+    echo $meta->mode->is_file ? "file\n" : "directory\n";
+}
+
+// Strict comparison — use === 1, not === true
+if ($op->is_exist("hello.txt") === 1) {
+    // ...
+}
+```
+
+## Write binary data
+
+`write` requires valid UTF-8. For arbitrary bytes, convert with `unpack` first
+and use `write_binary`:
+
+```php
+$bytes = unpack("C*", $binaryContent); // array of unsigned byte values
+$op->write_binary("data.bin", $bytes);
+
+$content = $op->read("data.bin");      // returns a binary-safe string
+```
+
+## Create a directory
+
+The trailing slash is required; omitting it produces a `NotADirectory` error.
+Creation is always recursive (equivalent to `mkdir -p`).
+
+```php
+$op->create_dir("logs/app/");
+```
+
+## Error handling
+
+All methods throw a standard `Exception` on failure. The message comes from
+the OpenDAL core and describes what went wrong (service error, path not found,
+invalid configuration, and so on).
+
+```php
+use OpenDAL\Operator;
+
+try {
+    $op = new Operator("fs", ["root" => "/tmp"]);
+    $content = $op->read("no-such-file.txt");
+} catch (\Exception $e) {
+    echo "Error: " . $e->getMessage() . "\n";
+}
+```
+
+Common error causes:
+- Passing an unknown scheme to the constructor.
+- Missing required configuration keys (e.g. `"root"` for the `fs` service).
+- Reading a path that does not exist.
+- Passing non-UTF-8 content to `write` (use `write_binary` instead).
diff --git a/website/docs/20-bindings/python.mdx b/website/docs/20-bindings/python.mdx
deleted file mode 100644
index a81da0e9d..000000000
--- a/website/docs/20-bindings/python.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Python
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/python/README.md';
-
-<GitHubReadme basePath="bindings/python/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/python/01-overview.md b/website/docs/20-bindings/python/01-overview.md
new file mode 100644
index 000000000..6874e3f14
--- /dev/null
+++ b/website/docs/20-bindings/python/01-overview.md
@@ -0,0 +1,56 @@
+---
+title: Python
+sidebar_label: Overview
+slug: /bindings/python
+description: The OpenDAL Python binding — install it, see what it supports, and where to go next.
+---
+
+# Apache OpenDAL™ Python Binding
+
+A native Python binding for OpenDAL: access S3, GCS, Azure Blob, HDFS, the local
+filesystem, and [50+ more services](/services) through one API, with the
+performance of the Rust core underneath.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+:::note
+This binding has its own version number, independent of the Rust core. Check
+compatibility against the binding's version, not the core's.
+:::
+
+## Status
+
+Released and stable, published to [PyPI](https://pypi.org/project/opendal/).
+
+## Capabilities
+
+- **Sync and async** — use [`Operator`] or [`AsyncOperator`] with the same verbs.
+- **File-like objects** — `op.open(path, "rb")` returns a familiar
+  read/write/seek file you can stream through.
+- **All services included** — the wheel bundles every backend; there are no
+  build flags to enable.
+- **Presigned URLs** for services that support them (async API).
+
+## Installation
+
+```shell
+pip install opendal
+```
+
+## Useful Links
+
+- **API reference**: [opendal.apache.org/docs/python](https://opendal.apache.org/docs/python/)
+- **Services & configuration**: [/services](/services)
+- **Upgrade guide**: [`upgrade.md`](https://github.com/apache/opendal/blob/main/bindings/python/upgrade.md)
+- **Source & examples**: [`bindings/python/`](https://github.com/apache/opendal/tree/main/bindings/python)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — a runnable program, then a real backend.
+2. [Connecting to your storage](./03-connecting.md) — build an operator for any service.
+3. [Common tasks](./04-tasks.md) — read, write, stream, list, presign, and more.
+4. [Going to production](./05-production.md) — retries, errors, and capability checks.
+
+[`Operator`]: https://opendal.apache.org/docs/python/opendal.html#opendal.Operator
+[`AsyncOperator`]: https://opendal.apache.org/docs/python/opendal.html#opendal.AsyncOperator
diff --git a/website/docs/20-bindings/python/02-getting-started.md b/website/docs/20-bindings/python/02-getting-started.md
new file mode 100644
index 000000000..a6e454adf
--- /dev/null
+++ b/website/docs/20-bindings/python/02-getting-started.md
@@ -0,0 +1,78 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Run your first OpenDAL program in Python, then point it at a real storage backend.
+---
+
+# Getting started
+
+## Your first program
+
+This program builds an operator, writes a file, reads it back, inspects its
+metadata, and deletes it. It runs against the in-memory service with no
+credentials, so you can run it right after installing.
+
+```shell
+pip install opendal
+```
+
+```python
+import opendal
+
+# Configure a service, then build an operator from it.
+op = opendal.Operator("memory")
+
+# The same verbs work on every service.
+op.write("hello.txt", b"Hello, World!")
+
+data = op.read("hello.txt")        # returns bytes
+print(data.decode())
+
+meta = op.stat("hello.txt")
+print(f"size = {meta.content_length} bytes")
+
+op.delete("hello.txt")
+```
+
+Note that `write` takes `bytes` and `read` returns `bytes`.
+
+## Point it at a real backend
+
+Only the service changes; the operations stay identical. The scheme is the first
+argument, and configuration is passed as keyword arguments:
+
+```python
+import opendal
+
+op = opendal.Operator("s3", bucket="my-bucket", region="us-east-1")
+
+op.write("hello.txt", b"Hello from S3!")
+print(op.read("hello.txt"))
+```
+
+The wheel bundles every service, so there is nothing extra to install. The next
+page, [Connecting to your storage](./03-connecting.md), covers configuration and
+credentials in depth; [Services](/services) lists every backend and its keys.
+
+## Async usage {#blocking-vs-async}
+
+OpenDAL has a first-class async API. Use [`AsyncOperator`] with the same verbs,
+awaiting each call:
+
+```python
+import asyncio
+import opendal
+
+async def main():
+    op = opendal.AsyncOperator("s3", bucket="my-bucket", region="us-east-1")
+    await op.write("hello.txt", b"Hello, World!")
+    data = await op.read("hello.txt")
+    print(data.decode())
+
+asyncio.run(main())
+```
+
+Both operators expose the same operations. Convert between them with
+`op.to_async_operator()` and `op.to_operator()` when you need the other form.
+
+[`AsyncOperator`]: https://opendal.apache.org/docs/python/opendal.html#opendal.AsyncOperator
diff --git a/website/docs/20-bindings/python/03-connecting.md b/website/docs/20-bindings/python/03-connecting.md
new file mode 100644
index 000000000..41eda9cc9
--- /dev/null
+++ b/website/docs/20-bindings/python/03-connecting.md
@@ -0,0 +1,77 @@
+---
+title: Connecting to your storage
+sidebar_label: Connecting to your storage
+description: Build an OpenDAL operator in Python for any backend — schemes, configuration, and credentials.
+---
+
+# Connecting to your storage
+
+Switching backends is a configuration change, not a code change. For the
+configuration keys of a specific service, see [Services](/services).
+
+Unlike the Rust core, the Python wheel includes every service — there are no
+build flags. Once `opendal` is installed, every backend is available.
+
+## Build an operator
+
+The first argument is the service scheme; configuration follows as keyword
+arguments:
+
+```python
+import opendal
+
+op = opendal.Operator(
+    "s3",
+    bucket="my-bucket",
+    region="us-east-1",
+    endpoint="https://s3.amazonaws.com",
+)
+```
+
+A local filesystem operator roots every path under one directory:
+
+```python
+op = opendal.Operator("fs", root="/tmp/opendal")
+```
+
+Use `opendal.AsyncOperator` with the same arguments for the async API.
+
+## Credentials
+
+Credentials are just more configuration keys. Set them as keyword arguments — for
+example S3's `access_key_id` and `secret_access_key`:
+
+```python
+op = opendal.Operator(
+    "s3",
+    bucket="my-bucket",
+    region="us-east-1",
+    access_key_id="...",
+    secret_access_key="...",
+)
+```
+
+Some services also load credentials from their platform's default sources (for
+S3, the standard AWS environment variables and profiles). See each service's
+page under [Services](/services) for the exact keys and credential behavior.
+
+Avoid hard-coding secrets in source. Read them from the environment or a secret
+manager and pass them in when you build the operator:
+
+```python
+import os
+import opendal
+
+op = opendal.Operator(
+    "s3",
+    bucket="my-bucket",
+    region="us-east-1",
+    access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
+    secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
+)
+```
+
+## One operator per service and root
+
+An operator maps to one service and one root path. To work with two buckets or
+two roots, build two operators — they are lightweight and safe to share.
diff --git a/website/docs/20-bindings/python/04-tasks.md b/website/docs/20-bindings/python/04-tasks.md
new file mode 100644
index 000000000..a60d4a1ce
--- /dev/null
+++ b/website/docs/20-bindings/python/04-tasks.md
@@ -0,0 +1,131 @@
+---
+title: Common tasks
+sidebar_label: Common tasks
+description: Task-oriented recipes for OpenDAL in Python — read, write, stream, list, delete, copy, and presign.
+---
+
+# Common tasks
+
+Recipes for the things you actually do with storage. Each works on every
+service. They assume an `op = opendal.Operator(...)` built as in
+[Getting started](./02-getting-started.md). For full signatures, see the
+[API reference](https://opendal.apache.org/docs/python/).
+
+A few conventions used throughout:
+
+- Paths are relative to the operator's root; a trailing `/` means a directory
+  (`logs/app/` is a directory, `logs/app` is a file).
+- `read` returns `bytes`; `write` takes `bytes`.
+- The async API mirrors these — use `opendal.AsyncOperator` and `await` each call.
+
+## Read a whole file
+
+```python
+data = op.read("path/to/file")     # bytes
+text = data.decode()
+```
+
+## Read part of a file
+
+```python
+data = op.read("path/to/file", offset=0, size=1024)
+```
+
+## Stream a large file
+
+Don't load gigabytes into memory — open a file-like object and read in chunks:
+
+```python
+with op.open("big.bin", "rb") as f:
+    while chunk := f.read(8192):
+        ...  # process chunk (bytes)
+```
+
+## Write a whole file
+
+```python
+op.write("path/to/file", b"Hello, World!")
+```
+
+## Stream a large upload
+
+Open the file in write mode and write incrementally:
+
+```python
+with op.open("big.bin", "wb") as f:
+    f.write(first_chunk)
+    f.write(second_chunk)
+```
+
+## Upload concurrently
+
+For large objects on services with multipart support, upload parts in parallel:
+
+```python
+op.write("big.bin", data, concurrent=8)
+```
+
+## Check existence and metadata
+
+```python
+if op.exists("path/to/file"):
+    meta = op.stat("path/to/file")
+    print(meta.content_length, meta.is_dir)
+```
+
+## List a directory
+
+`list` yields the entries under a path:
+
+```python
+for entry in op.list("dir/"):
+    print(entry.path, entry.metadata.mode)
+```
+
+## Walk a tree recursively
+
+```python
+for entry in op.list("dir/", recursive=True):
+    print(entry.path)
+```
+
+## Delete a file or a whole tree
+
+```python
+op.delete("path/to/file")    # single path; succeeds even if missing
+op.remove_all("dir/")        # a path and everything under it
+```
+
+## Create a directory
+
+```python
+op.create_dir("path/to/dir/")   # the trailing slash is required
+```
+
+## Copy and rename
+
+```python
+op.copy("from.txt", "to.txt")
+op.rename("old.txt", "new.txt")
+```
+
+Both require a service that supports them; see
+[capability checks](./05-production.md#capability-checks).
+
+## Generate a presigned URL
+
+Hand a time-limited URL to a third party so they can access an object without
+your credentials. Presigning is part of the **async** API:
+
+```python
+import asyncio
+import opendal
+
+async def main():
+    op = opendal.AsyncOperator("s3", bucket="my-bucket", region="us-east-1")
+    req = await op.presign_read("path/to/file", 3600)  # expires in 3600s
+    print(req.method, req.url)
+    print(req.headers)
+
+asyncio.run(main())
+```
diff --git a/website/docs/20-bindings/python/05-production.md b/website/docs/20-bindings/python/05-production.md
new file mode 100644
index 000000000..1e0556fd7
--- /dev/null
+++ b/website/docs/20-bindings/python/05-production.md
@@ -0,0 +1,76 @@
+---
+title: Going to production
+sidebar_label: Going to production
+description: Make OpenDAL robust in Python — retries, concurrency limits, error handling, and capability checks.
+---
+
+# Going to production
+
+The basics read and write data. Production code also has to survive transient
+failures, handle errors precisely, and adapt to what a backend supports.
+
+## Layers
+
+A layer wraps an operator to add cross-cutting behavior. `op.layer(...)` returns
+a new operator with the layer applied:
+
+```python
+import opendal
+from opendal.layers import RetryLayer
+
+op = opendal.Operator("s3", bucket="my-bucket", region="us-east-1")
+op = op.layer(RetryLayer(max_times=3))
+```
+
+The Python binding exposes these layers:
+
+| Layer | What it does |
+|-------|--------------|
+| `RetryLayer` | Retries operations that fail with temporary errors, using exponential backoff. |
+| `ConcurrentLimitLayer` | Caps the number of concurrent requests. |
+| `MimeGuessLayer` | Sets `Content-Type` from the file extension. |
+
+```python
+from opendal.layers import ConcurrentLimitLayer, RetryLayer
+
+op = (
+    opendal.Operator("s3", bucket="my-bucket", region="us-east-1")
+    .layer(RetryLayer(max_times=3))
+    .layer(ConcurrentLimitLayer(16))
+)
+```
+
+## Error handling
+
+OpenDAL raises typed exceptions from `opendal.exceptions`. Catch the specific
+class instead of inspecting messages:
+
+```python
+import opendal
+from opendal.exceptions import NotFound
+
+try:
+    data = op.read("maybe-missing.txt")
+except NotFound:
+    data = None
+```
+
+Available exceptions include `NotFound`, `PermissionDenied`, `AlreadyExists`,
+`ConditionNotMatch`, `RateLimited`, and `Unsupported`, all subclasses of
+`opendal.exceptions.Error`. Note that `delete` is idempotent — deleting a missing
+path succeeds rather than raising `NotFound`.
+
+## Capability checks {#capability-checks}
+
+Not every service supports every operation. Query what a backend can do with
+`capability()` before calling optional operations like `copy`, `rename`, or
+presign:
+
+```python
+cap = op.capability()
+if cap.copy:
+    op.copy("a.txt", "b.txt")
+```
+
+Calling an unsupported operation raises `opendal.exceptions.Unsupported`, so
+capability checks are an optimization, not a requirement for safety.
diff --git a/website/docs/20-bindings/ruby.mdx b/website/docs/20-bindings/ruby.mdx
deleted file mode 100644
index 64ccc80c0..000000000
--- a/website/docs/20-bindings/ruby.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Ruby
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/ruby/README.md';
-
-<GitHubReadme basePath="bindings/ruby/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/ruby/01-overview.md b/website/docs/20-bindings/ruby/01-overview.md
new file mode 100644
index 000000000..d276b3ae7
--- /dev/null
+++ b/website/docs/20-bindings/ruby/01-overview.md
@@ -0,0 +1,62 @@
+---
+title: Ruby
+sidebar_label: Overview
+slug: /bindings/ruby
+description: The OpenDAL Ruby binding — install it, see what it supports, and where to go next.
+---
+
+# Apache OpenDAL™ Ruby Binding
+
+A native Ruby binding for OpenDAL: access S3, GCS, Azure Blob, the local
+filesystem, and [50+ more services](/services) through one API, with the
+performance of the Rust core underneath.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+:::note
+This binding has its own version number, independent of the Rust core. Check
+compatibility against the binding's version, not the core's.
+:::
+
+## Status
+
+Released and published to [RubyGems](https://rubygems.org/gems/opendal).
+
+## Capabilities
+
+- **Synchronous** — every operation runs on the calling thread; there is no
+  async API.
+- **One API for every service** — the gem bundles all backends, selected by a
+  scheme string at construction time.
+- **File-like `IO`** — `op.open(path, "r")` returns a familiar
+  read/write/seek/`readline` object you can stream through.
+- **Middlewares** (OpenDAL layers) for retry, concurrency limits, throttling,
+  and timeouts — see [Going to production](./05-production.md).
+
+## Installation
+
+```shell
+gem install opendal
+```
+
+Or add it to your `Gemfile`:
+
+```ruby
+# Gemfile
+gem "opendal"
+```
+
+## Useful Links
+
+- **API reference**: [opendal.apache.org/docs/ruby](https://opendal.apache.org/docs/ruby/)
+- **Services & configuration**: [/services](/services)
+- **Rust core docs**: [docs.rs/opendal](https://docs.rs/opendal/latest/opendal/index.html)
+- **Source & examples**: [`bindings/ruby/`](https://github.com/apache/opendal/tree/main/bindings/ruby)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — a runnable program, then a real backend.
+2. [Connecting to your storage](./03-connecting.md) — build an operator for any service.
+3. [Common tasks](./04-tasks.md) — read, write, stream, list, copy, and rename.
+4. [Going to production](./05-production.md) — middlewares, errors, and capability checks.
diff --git a/website/docs/20-bindings/ruby/02-getting-started.md b/website/docs/20-bindings/ruby/02-getting-started.md
new file mode 100644
index 000000000..3c3a4a001
--- /dev/null
+++ b/website/docs/20-bindings/ruby/02-getting-started.md
@@ -0,0 +1,64 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Run your first OpenDAL program in Ruby, then point it at a real storage backend.
+---
+
+# Getting started
+
+## Your first program
+
+This program builds an operator, writes a file, reads it back, inspects its
+metadata, and deletes it. It runs against the in-memory service with no
+credentials, so you can run it right after installing.
+
+```shell
+gem install opendal
+```
+
+```ruby
+require "opendal"
+
+# Configure a service, then build an operator from it.
+op = OpenDal::Operator.new("memory", {})
+
+# The same verbs work on every service.
+op.write("hello.txt", "Hello, World!")
+
+data = op.read("hello.txt")        # returns a String
+puts data
+
+meta = op.stat("hello.txt")
+puts "size = #{meta.content_length} bytes"
+
+op.delete("hello.txt")
+```
+
+`write` takes a `String`, and `read` returns a `String`.
+
+## Point it at a real backend
+
+Only the service changes; the operations stay identical. The scheme is the first
+argument, and configuration is a `Hash` of string keys and values:
+
+```ruby
+require "opendal"
+
+op = OpenDal::Operator.new("s3", {
+  "bucket" => "my-bucket",
+  "region" => "us-east-1",
+})
+
+op.write("hello.txt", "Hello from S3!")
+puts op.read("hello.txt")
+```
+
+The gem bundles every service, so there is nothing extra to install. The next
+page, [Connecting to your storage](./03-connecting.md), covers configuration and
+credentials in depth; [Services](/services) lists every backend and its keys.
+
+## Synchronous by design
+
+The Ruby binding is synchronous: every operation runs on the calling thread and
+returns when it completes. There is no async operator and nothing to `await` —
+call the methods directly as shown above.
diff --git a/website/docs/20-bindings/ruby/03-connecting.md b/website/docs/20-bindings/ruby/03-connecting.md
new file mode 100644
index 000000000..7727e7360
--- /dev/null
+++ b/website/docs/20-bindings/ruby/03-connecting.md
@@ -0,0 +1,75 @@
+---
+title: Connecting to your storage
+sidebar_label: Connecting to your storage
+description: Build an OpenDAL operator in Ruby for any backend — schemes, configuration, and credentials.
+---
+
+# Connecting to your storage
+
+Switching backends is a configuration change, not a code change. For the
+configuration keys of a specific service, see [Services](/services).
+
+Unlike the Rust core, the Ruby gem includes every service — there are no build
+flags. Once `opendal` is installed, every backend is available.
+
+## Build an operator
+
+The first argument is the service scheme; configuration follows as a `Hash` with
+string keys and string values:
+
+```ruby
+require "opendal"
+
+op = OpenDal::Operator.new("s3", {
+  "bucket" => "my-bucket",
+  "region" => "us-east-1",
+  "endpoint" => "https://s3.amazonaws.com",
+})
+```
+
+A local filesystem operator roots every path under one directory:
+
+```ruby
+op = OpenDal::Operator.new("fs", {"root" => "/tmp/opendal"})
+```
+
+When a service needs no extra configuration, pass an empty hash:
+
+```ruby
+op = OpenDal::Operator.new("memory", {})
+```
+
+## Credentials
+
+Credentials are just more configuration keys. Set them as entries in the hash —
+for example S3's `access_key_id` and `secret_access_key`:
+
+```ruby
+op = OpenDal::Operator.new("s3", {
+  "bucket" => "my-bucket",
+  "region" => "us-east-1",
+  "access_key_id" => "...",
+  "secret_access_key" => "...",
+})
+```
+
+Some services also load credentials from their platform's default sources. See
+each service's page under [Services](/services) for the exact keys and credential
+behavior.
+
+Avoid hard-coding secrets in source. Read them from the environment or a secret
+manager and pass them in when you build the operator:
+
+```ruby
+op = OpenDal::Operator.new("s3", {
+  "bucket" => "my-bucket",
+  "region" => "us-east-1",
+  "access_key_id" => ENV.fetch("AWS_ACCESS_KEY_ID"),
+  "secret_access_key" => ENV.fetch("AWS_SECRET_ACCESS_KEY"),
+})
+```
+
+## One operator per service and root
+
+An operator maps to one service and one root path. To work with two buckets or
+two roots, build two operators — they are lightweight handles.
diff --git a/website/docs/20-bindings/ruby/04-tasks.md b/website/docs/20-bindings/ruby/04-tasks.md
new file mode 100644
index 000000000..d6bd4f576
--- /dev/null
+++ b/website/docs/20-bindings/ruby/04-tasks.md
@@ -0,0 +1,130 @@
+---
+title: Common tasks
+sidebar_label: Common tasks
+description: Task-oriented recipes for OpenDAL in Ruby — read, write, stream, list, delete, copy, and rename.
+---
+
+# Common tasks
+
+Recipes for the things you actually do with storage. Each works on every
+service. They assume an `op = OpenDal::Operator.new(...)` built as in
+[Getting started](./02-getting-started.md). For full signatures, see the
+[API reference](https://opendal.apache.org/docs/ruby/).
+
+A few conventions used throughout:
+
+- Paths are relative to the operator's root; a trailing `/` means a directory
+  (`logs/app/` is a directory, `logs/app` is a file).
+- `read` returns a `String`; `write` takes a `String`.
+- For large data, open an `OpenDal::IO` with `op.open` and stream in chunks.
+
+## Read a whole file
+
+```ruby
+data = op.read("path/to/file")     # String
+```
+
+## Stream a large file
+
+Don't load gigabytes into memory — open the file and read in chunks:
+
+```ruby
+io = op.open("big.bin", "rb")
+while (chunk = io.read(8192))
+  # process chunk (String)
+end
+io.close
+```
+
+`OpenDal::IO` also supports `readline`, `readlines`, `seek`, `tell`, `rewind`,
+and `eof?` for line- and position-oriented reads.
+
+## Write a whole file
+
+```ruby
+op.write("path/to/file", "Hello, World!")
+```
+
+## Stream a large upload
+
+Open the file in write mode and write incrementally, then `close` to commit:
+
+```ruby
+io = op.open("big.bin", "wb")
+io.write(first_chunk)
+io.write(second_chunk)
+io.close
+```
+
+Writing does not support `seek`; an `OpenDal::IO` is either a reader or a writer.
+
+## Check existence and metadata
+
+```ruby
+if op.exist?("path/to/file")
+  meta = op.stat("path/to/file")
+  puts "#{meta.content_length} bytes, dir = #{meta.dir?}"
+end
+```
+
+`Metadata` also exposes `content_type`, `content_md5`, `etag`,
+`content_disposition`, and the `file?` / `dir?` helpers.
+
+## List a directory
+
+`list` returns a `Lister`, which is `Enumerable` and yields `Entry` objects:
+
+```ruby
+op.list("dir/").each do |entry|
+  puts "#{entry.path} (#{entry.metadata.mode})"
+end
+```
+
+Because it is `Enumerable`, the usual methods work too:
+
+```ruby
+paths = op.list("dir/").map(&:path)
+```
+
+## Walk a tree recursively
+
+```ruby
+op.list("dir/", recursive: true).each do |entry|
+  puts entry.path
+end
+```
+
+`list` also accepts `limit:` (per-request max results) and `start_after:` (the
+key to start listing from).
+
+## Delete a file or a whole tree
+
+```ruby
+op.delete("path/to/file")     # single path
+op.remove_all("dir/")         # a path and everything under it
+```
+
+## Create a directory
+
+```ruby
+op.create_dir("path/to/dir/")   # the trailing slash is required
+```
+
+## Copy and rename
+
+```ruby
+op.copy("from.txt", "to.txt")
+op.rename("old.txt", "new.txt")
+```
+
+Both require a service that supports them; see
+[capability checks](./05-production.md#capability-checks).
+
+## Inspect the operator
+
+```ruby
+info = op.info
+puts info.scheme    # e.g. "s3"
+puts info.root      # e.g. "/"
+puts info.name      # e.g. the bucket or container name
+```
diff --git a/website/docs/20-bindings/ruby/05-production.md b/website/docs/20-bindings/ruby/05-production.md
new file mode 100644
index 000000000..a8d0920c4
--- /dev/null
+++ b/website/docs/20-bindings/ruby/05-production.md
@@ -0,0 +1,73 @@
+---
+title: Going to production
+sidebar_label: Going to production
+description: Make OpenDAL robust in Ruby — middlewares, error handling, and capability checks.
+---
+
+# Going to production
+
+The basics read and write data. Production code also has to survive transient
+failures, bound its resource use, handle errors precisely, and adapt to what a
+backend supports.
+
+## Middlewares
+
+A middleware wraps an operator to add cross-cutting behavior — these are
+OpenDAL's [layers](../../03-concepts.mdx#layer), exposed in Ruby as
+`OpenDal::Middleware`. Apply one with `op.middleware(...)`, which returns a new
+operator with the middleware applied:
+
+```ruby
+require "opendal"
+
+op = OpenDal::Operator.new("s3", {"bucket" => "my-bucket", "region" => "us-east-1"})
+
+op = op.middleware(OpenDal::Middleware::ConcurrentLimit.new(5))
+op = op.middleware(OpenDal::Middleware::Retry.new)
+op = op.middleware(OpenDal::Middleware::Timeout.new(10, 5))
+```
+
+The Ruby binding exposes these middlewares:
+
+| Middleware | What it does |
+|------------|--------------|
+| `OpenDal::Middleware::Retry` | Retries operations that fail with temporary errors. `new` takes no arguments. |
+| `OpenDal::Middleware::ConcurrentLimit` | Caps the number of concurrent requests. `new(permits)`. |
+| `OpenDal::Middleware::Throttle` | Limits bandwidth. `new(bandwidth, burst)`. |
+| `OpenDal::Middleware::Timeout` | Aborts slow operations. `new(timeout, io_timeout)`, in seconds. |
+
+## Error handling
+
+Failed operations raise `RuntimeError` with a message describing what went
+wrong. Rescue it around calls that may fail, for example reading a path that may
+not exist:
+
+```ruby
+begin
+  data = op.read("maybe-missing.txt")
+rescue RuntimeError => e
+  warn "read failed: #{e.message}"
+  data = nil
+end
+```
+
+Invalid arguments raise the standard Ruby exceptions: `op.open` with a bad mode
+raises `ArgumentError`, and reading past the end with `readline` raises
+`EOFError`.
+
+## Capability checks {#capability-checks}
+
+Not every service supports every operation. Query what a backend can do with
+`capability` before calling optional operations like `copy` or `rename`:
+
+```ruby
+cap = op.capability
+if cap.copy
+  op.copy("a.txt", "b.txt")
+end
+```
+
+`Capability` exposes a boolean (or value) per operation, including `read`,
+`write`, `stat`, `delete`, `create_dir`, `copy`, `rename`, `list`, and the
+`list_with_recursive` / `write_can_append` style fine-grained flags. The same
+capability is available from `op.info.capability`.
diff --git a/website/docs/20-bindings/swift.mdx b/website/docs/20-bindings/swift.mdx
deleted file mode 100644
index 2f565a848..000000000
--- a/website/docs/20-bindings/swift.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Swift 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/swift/README.md';
-
-<GitHubReadme basePath="bindings/swift/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/swift/01-overview.md b/website/docs/20-bindings/swift/01-overview.md
new file mode 100644
index 000000000..bb8a1787e
--- /dev/null
+++ b/website/docs/20-bindings/swift/01-overview.md
@@ -0,0 +1,89 @@
+---
+title: Swift
+sidebar_label: Overview
+slug: /bindings/swift
+description: The OpenDAL Swift binding — experimental status, what it supports, how to build it, and where to go next.
+---
+
+# Apache OpenDAL™ Swift Binding
+
+A Swift binding for OpenDAL: access storage services through one API, backed
+by the Rust core.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, and operation are the same ideas in every language.
+
+:::warning Experimental / WIP
+This binding is unreleased and under active development. The API surface is
+minimal, breaking changes may happen without notice, and it is **not suitable
+for production use**. Contributions welcome.
+:::
+
+## Status
+
+**Experimental — unreleased.** Only blocking read and write are implemented.
+The binding depends on the C binding and must be built from source; it is not
+published to any package registry.
+
+## Capabilities
+
+| Capability | Status |
+|---|---|
+| Blocking read | Supported |
+| Blocking write | Supported |
+| Async read/write | Not yet |
+| Stat / metadata | Not yet |
+| List | Not yet |
+| Delete | Not yet |
+| Presigned URLs | Not yet |
+
+The `Operator` class is the single entry point. It accepts a service scheme
+string and a dictionary of string options, matching the same model as every
+other OpenDAL binding. See [Services](/services) for the full list of backends
+and their configuration keys.
+
+## Installation
+
+The package is **not published to any registry**. You must build the C
+dependency and reference the package by local path.
+
+### 1. Build the C dependency
+
+```shell
+cd bindings/swift
+make build-c
+```
+
+### 2. Add a local path dependency
+
+In your project's `Package.swift`:
+
+```swift
+// swift-tools-version:5.7
+import PackageDescription
+
+let package = Package(
+    name: "MyTool",
+    dependencies: [
+        .package(path: "/path/to/opendal/bindings/swift/OpenDAL"),
+    ],
+    targets: [
+        .target(name: "MyTool", dependencies: [
+            .product(name: "OpenDAL", package: "OpenDAL"),
+        ]),
+    ]
+)
+```
+
+Replace `/path/to/opendal` with the absolute path to your local checkout.
+
+## Useful Links
+
+- **User guide**: [opendal.apache.org/docs/bindings/swift](https://opendal.apache.org/docs/bindings/swift)
+- **Services & configuration**: [/services](/services)
+- **Source**: [`bindings/swift/`](https://github.com/apache/opendal/tree/main/bindings/swift)
+- **Contributing**: [`bindings/swift/CONTRIBUTING.md`](https://github.com/apache/opendal/blob/main/bindings/swift/CONTRIBUTING.md)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — create an operator, write data, read it back.
diff --git a/website/docs/20-bindings/swift/02-getting-started.md b/website/docs/20-bindings/swift/02-getting-started.md
new file mode 100644
index 000000000..83a19b295
--- /dev/null
+++ b/website/docs/20-bindings/swift/02-getting-started.md
@@ -0,0 +1,101 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Create an OpenDAL Swift operator, write data, and read it back.
+---
+
+# Getting started
+
+:::warning Experimental
+The Swift binding is unreleased and experimental. Build from source before
+following this guide — see [Overview](./01-overview.md) for build steps.
+:::
+
+## Your first program
+
+This program creates an in-memory operator, writes bytes to a path, and reads
+them back. No credentials or network access needed.
+
+```swift
+import OpenDAL
+
+// Create an operator backed by the in-memory service.
+let op = try Operator(scheme: "memory")
+
+// Write bytes to a path.
+var data = Data([1, 2, 3, 4])
+try op.blockingWrite(&data, to: "/hello")
+
+// Read them back.
+let result = try op.blockingRead("/hello")
+print(result) // 4 bytes
+```
+
+`Operator.init(scheme:options:)` throws `OperatorError` on failure. Both
+`blockingWrite` and `blockingRead` also throw `OperatorError`; wrap calls in
+`do/catch` for production code.
+
+## Passing options
+
+Service configuration is passed as a `[String: String]` dictionary. For
+example, to pin a root path on the memory service:
+
+```swift
+import OpenDAL
+
+let op = try Operator(
+    scheme: "memory",
+    options: ["root": "/myroot"]
+)
+
+var data = Data("hello".utf8)
+try op.blockingWrite(&data, to: "file.txt")
+
+let bytes = try op.blockingRead("file.txt")
+print(String(data: bytes, encoding: .utf8)!) // hello
+```
+
+## Pointing at a real backend
+
+Only the scheme and options change; the write/read calls are identical. The
+keys for each service are listed on [/services](/services).
+
+```swift
+import OpenDAL
+
+// S3 example — fill in real values.
+let op = try Operator(
+    scheme: "s3",
+    options: [
+        "bucket": "my-bucket",
+        "region": "us-east-1",
+        "access_key_id": "...",
+        "secret_access_key": "...",
+    ]
+)
+
+var payload = Data("hello from S3".utf8)
+try op.blockingWrite(&payload, to: "hello.txt")
+```
+
+## Error handling
+
+Both `Operator.init` and the blocking operations throw `OperatorError`:
+
+```swift
+import OpenDAL
+
+do {
+    let op = try Operator(scheme: "memory")
+    let _ = try op.blockingRead("/does-not-exist")
+} catch let err as OperatorError {
+    print("OpenDAL operation failed: \(err)")
+} catch {
+    print("unexpected error: \(error)")
+}
+```
+
+> **Note:** `OperatorError` has `code` (UInt32) and `message` (Data) fields
+> internally, but they are not declared `public` and cannot be accessed from
+> consuming packages. Until they are exposed, use the error's string
+> representation via `\(err)` or `err.localizedDescription`.
diff --git a/website/docs/20-bindings/zig.mdx b/website/docs/20-bindings/zig.mdx
deleted file mode 100644
index 06de565bb..000000000
--- a/website/docs/20-bindings/zig.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Zig 🚧
----
-
-import GitHubReadme from '@site/components/GitHubReadme';
-import Content from '../../../bindings/zig/README.md';
-
-<GitHubReadme basePath="bindings/zig/">
-  <Content components={{ h1: 'h2' }} />
-</GitHubReadme>
diff --git a/website/docs/20-bindings/zig/01-overview.md b/website/docs/20-bindings/zig/01-overview.md
new file mode 100644
index 000000000..49acedd31
--- /dev/null
+++ b/website/docs/20-bindings/zig/01-overview.md
@@ -0,0 +1,104 @@
+---
+title: Zig
+sidebar_label: Overview
+slug: /bindings/zig
+description: The OpenDAL Zig binding — early-stage, built from source, wraps the C API to expose blocking storage operations.
+---
+
+# Apache OpenDAL™ Zig Binding
+
+A Zig binding for OpenDAL, giving Zig programs blocking access to S3, GCS, Azure Blob,
+the local filesystem, and [50+ more services](/services) through one API backed by the
+Rust core.
+
+New to the model behind the API? Read [Concepts](../../03-concepts.mdx) first —
+service, operator, layer, operation are the same four ideas in every language.
+
+:::caution Early-stage / Experimental
+This binding is **early-stage and a work in progress**. The API is incomplete,
+there are no pre-built packages, and breaking changes may occur. It must be built
+from source. It is not yet published to any package registry.
+:::
+
+## Status
+
+Unreleased and experimental (version `0.0.1`). The Zig binding wraps the OpenDAL C
+binding (`libopendal_c`) using Zig's `translate-c` mechanism, then exposes a thin
+idiomatic Zig layer on top. Only synchronous (blocking) operations are implemented;
+an async interface using coroutines is noted as WIP in the source.
+
+Requires Zig 0.14.0 or higher.
+
+## Capabilities
+
+Operations available through `Operator`:
+
+- **write** — write bytes to a path
+- **read** — read bytes from a path
+- **stat** — fetch metadata (size, type, content-type, etag, last-modified)
+- **delete** — remove a path
+- **exists** — check whether a path exists
+- **list** — iterate directory entries (returns a `Lister` yielding `Entry` values)
+- **createDir** — create a directory path
+- **rename** — rename/move a path (supported only on backends that expose this capability)
+- **copy** — copy a path (supported only on backends that expose this capability)
+
+All operations return Zig errors (`OpendalError`) rather than raw C error codes.
+
+`info()` / `OperatorInfo` exist in the source but are currently broken (a compile error in `src/opendal.zig` references an undefined variable) and cannot be used until the bug is fixed.
+
+The binding also exposes the raw C API under the `opendal.c` namespace for cases where
+the thin layer does not yet cover what you need.
+
+## Building
+
+Requirements: Zig 0.14.0 or higher, a C/C++ compiler, CMake, and a Rust toolchain.
+
+```shell
+# From bindings/zig/
+
+# 1. Build the underlying C library (runs cmake + make under bindings/c)
+zig build libopendal_c
+
+# 2. Build and run the unit tests
+zig build test --summary all
+```
+
+The C library lands in `bindings/c/target/debug/` (or `release/` with
+`-Doptimize=ReleaseSafe`). The header is at `bindings/c/include/opendal.h`.
+
+To build in release mode:
+
+```shell
+zig build libopendal_c -Doptimize=ReleaseSafe
+zig build test -Doptimize=ReleaseSafe --summary all
+```
+
+## Using the module in your project
+
+Add the module to your `build.zig`:
+
+```zig
+const opendal_dep = b.dependency("opendal", .{
+    .target = target,
+    .optimize = optimize,
+});
+exe.root_module.addImport("opendal", opendal_dep.module("opendal"));
+```
+
+Then import it:
+
+```zig
+const opendal = @import("opendal");
+```
+
+## Useful Links
+
+- **Source**: [`bindings/zig/`](https://github.com/apache/opendal/tree/main/bindings/zig)
+- **Services & configuration**: [/services](/services)
+- **C binding (underlying layer)**: [`bindings/c/`](https://github.com/apache/opendal/tree/main/bindings/c)
+- **Contributing**: [`bindings/zig/CONTRIBUTING.md`](https://github.com/apache/opendal/blob/main/bindings/zig/CONTRIBUTING.md)
+
+## Next steps
+
+1. [Getting started](./02-getting-started.md) — build an operator, write and read data, inspect metadata, and list a directory.
diff --git a/website/docs/20-bindings/zig/02-getting-started.md b/website/docs/20-bindings/zig/02-getting-started.md
new file mode 100644
index 000000000..c72376de0
--- /dev/null
+++ b/website/docs/20-bindings/zig/02-getting-started.md
@@ -0,0 +1,131 @@
+---
+title: Getting started
+sidebar_label: Getting started
+description: Build an operator, write and read data, inspect metadata, and list a directory with the OpenDAL Zig binding.
+---
+
+# Getting started
+
+Before running anything, make sure you have built the binding:
+
+```shell
+# From bindings/zig/
+zig build libopendal_c
+```
+
+## Your first program
+
+The example below initializes an operator backed by the in-memory service, writes a
+file, reads it back, checks its metadata, and deletes it. It needs no credentials and
+no external service.
+
+```zig
+const std = @import("std");
+const opendal = @import("opendal");
+
+pub fn main() !void {
+    // Initialize an operator for the "memory" backend with no options.
+    var op = try opendal.Operator.init("memory", null);
+    defer op.deinit();
+
+    // Write bytes to a path.
+    try op.write("/hello.txt", "Hello, World!");
+
+    // Read them back.
+    const data = try op.read("/hello.txt");
+    std.debug.print("read: {s}\n", .{data});
+
+    // Inspect metadata.
+    var meta = try op.stat("/hello.txt");
+    defer meta.deinit();
+    std.debug.print("size: {d} bytes\n", .{meta.contentLength()});
+    std.debug.print("is file: {}\n", .{meta.isFile()});
+
+    // Check existence.
+    std.debug.print("exists: {}\n", .{try op.exists("/hello.txt")});
+
+    // Delete.
+    try op.delete("/hello.txt");
+    std.debug.print("deleted, exists: {}\n", .{try op.exists("/hello.txt")});
+}
+```
+
+`Operator.init` takes the service scheme as a string and an optional pointer to an
+`opendal_operator_options` struct for service-specific configuration. Passing `null`
+uses defaults.
+
+## Listing a directory
+
+`list` returns a `Lister`. Call `next` in a loop until it returns `null`.
+
+```zig
+var op = try opendal.Operator.init("memory", null);
+defer op.deinit();
+
+try op.createDir("/testdir/");
+try op.write("/testdir/a.txt", "aaa");
+try op.write("/testdir/b.txt", "bbb");
+
+var lister = try op.list("/testdir/");
+defer lister.deinit();
+
+while (try lister.next()) |entry| {
+    defer entry.deinit(); // Entry owns the underlying C allocation; free it after use.
+    std.debug.print("name={s}  path={s}\n", .{ entry.name(), entry.path() });
+}
+```
+
+## Passing service options
+
+Service-specific configuration is set through the C API's `opendal_operator_options`.
+The Zig binding re-exports the raw C namespace as `opendal.c`:
+
+```zig
+const c = opendal.c;
+
+const options = c.opendal_operator_options_new();
+defer c.opendal_operator_options_free(options);
+c.opendal_operator_options_set(options, "root", "/myroot");
+
+// opendal_operator_options_new() returns [*c]..., but Operator.init expects ?*.
+// @ptrCast converts the many-item C pointer to a single-item optional pointer.
+var op = try opendal.Operator.init("memory", @ptrCast(options));
+defer op.deinit();
+```
+
+## Error handling
+
+Every operation returns a Zig error union. The binding maps C error codes to named
+Zig errors in the `OpendalError` set. The table below shows common variants;
+the full set (including `error.IsDirectory`, `error.IsNotDirectory`, `error.RateLimited`,
+`error.IsSameFile`, `error.ConditionNotMatch`, `error.RangeNotSatisfied`, and others)
+is defined in `src/opendal.zig`.
+
+| Zig error | Meaning |
+|---|---|
+| `error.NotFound` | Path does not exist |
+| `error.PermissionDenied` | Access denied |
+| `error.AlreadyExists` | Path already exists |
+| `error.Unsupported` | Backend does not support the operation |
+| `error.ConfigInvalid` | Operator configuration is invalid |
+| `error.Unexpected` | Unclassified error from the backend |
+
+Handle them with a standard `catch` or `try`:
+
+```zig
+const data = op.read("/missing.txt") catch |err| {
+    if (err == error.NotFound) {
+        std.debug.print("file not found\n", .{});
+        return;
+    }
+    return err;
+};
+```
+
+## Running the binding's own tests
+
+```shell
+zig build test --summary all
+```
+
+All tests run against the in-memory backend; no credentials are required.
diff --git a/website/docs/sidebars.js b/website/docs/sidebars.js
index 7704508ad..bfa5dbd22 100644
--- a/website/docs/sidebars.js
+++ b/website/docs/sidebars.js
@@ -19,9 +19,57 @@
 
 // @ts-check
 
+// A per-language guide: a collapsible category whose title links to the
+// language's Overview, with the remaining guide pages as children. `dir` is the
+// doc id prefix (e.g. 'core' or 'bindings/python'); `pages` lists the non-
+// overview pages that language actually ships (omit those it doesn't have).
+const langGuide = (label, dir, pages, icon) => ({
+  type: 'category',
+  label,
+  // sidebar-lang-<key> drives the brand logo shown before the label (see
+  // custom.css). Defaults to the last path segment of the doc id prefix.
+  className: `sidebar-lang sidebar-lang-${icon ?? dir.split('/').pop()}`,
+  link: { type: 'doc', id: `${dir}/overview` },
+  collapsed: true,
+  items: pages.map((p) => `${dir}/${p}`),
+});
+
+// The full task-centric guide; thin/experimental bindings ship a subset.
+const FULL = ['getting-started', 'connecting', 'tasks', 'production'];
+const MIN = ['getting-started'];
+
 /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
 const sidebars = {
-  docs: [{ type: 'autogenerated', dirName: '.' }],
+  docs: [
+    // Concepts: read these first, regardless of language.
+    'overview',
+    'vision',
+    'concepts',
+    // Divider between the concept docs and the per-language guides.
+    {
+      type: 'html',
+      value: '<div class="sidebar-section-label">Languages</div>',
+      defaultStyle: false,
+    },
+    // Languages, flat. Rust (the core) leads; the rest are alphabetical.
+    langGuide('Rust', 'core', FULL, 'rust'),
+    langGuide('C', 'bindings/c', MIN),
+    langGuide('C++', 'bindings/cpp', MIN),
+    langGuide('D', 'bindings/d', MIN),
+    langGuide('Dart', 'bindings/dart', MIN),
+    langGuide('.NET', 'bindings/dotnet', FULL),
+    langGuide('Go', 'bindings/go', FULL),
+    langGuide('Haskell', 'bindings/haskell', MIN),
+    langGuide('Java', 'bindings/java', FULL),
+    langGuide('Lua', 'bindings/lua', MIN),
+    langGuide('Node.js', 'bindings/nodejs', FULL),
+    langGuide('OCaml', 'bindings/ocaml', MIN),
+    langGuide('PHP', 'bindings/php', MIN),
+    langGuide('Python', 'bindings/python', FULL),
+    langGuide('Ruby', 'bindings/ruby', FULL),
+    langGuide('Swift', 'bindings/swift', MIN),
+    langGuide('Zig', 'bindings/zig', MIN),
+  ],
 };
 
 module.exports = sidebars;
diff --git a/website/src/css/custom.css b/website/src/css/custom.css
index fc4c8f192..1246dd09e 100644
--- a/website/src/css/custom.css
+++ b/website/src/css/custom.css
@@ -431,6 +431,55 @@ pre[class*="language-"] {
   filter: brightness(0) invert(1) opacity(0.7);
 }
 
+/* ---- 7b. Docs sidebar section label -------------------------------------- */
+/* A divider + eyebrow label that separates the concept docs from the
+   per-language guides in the docs sidebar. */
+.sidebar-section-label {
+  margin: var(--odl-space-4, 1rem) 0 var(--odl-space-1, 0.25rem);
+  padding: var(--odl-space-3, 0.75rem) 0.75rem 0;
+  border-top: 1px solid var(--ifm-toc-border-color);
+  font-family: var(--odl-font-mono);
+  font-size: var(--odl-text-xs);
+  font-weight: 600;
+  letter-spacing: var(--odl-tracking-label);
+  text-transform: uppercase;
+  color: var(--odl-fg-muted);
+}
+
... 37 lines suppressed ...