diff --git a/.github/PULL_REQUEST_TEMPLATE/add_library.md b/.github/PULL_REQUEST_TEMPLATE/add_library.md new file mode 100644 index 00000000..681f07fe --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/add_library.md @@ -0,0 +1,133 @@ + + +### What are you adding? + + + +- [ ] Client library → `_data/libraries/clients/` +- [ ] Integration (tool, framework, or AI/agent library) → `_data/libraries/integrations/` + +### Entry details + +- **Name:** +- **Repository:** +- **License (SPDX):** + +--- + +## How entries work + +Each entry is a single flat JSON file under `_data/libraries/`, in one of two +group directories, and registered by path in `_data/libraries/manifest.json`: + +- Client libraries → `_data/libraries/clients/` (appear on the **Clients** page) +- Integrations → `_data/libraries/integrations/` (appear on the **Integrations** page) + +The directory decides which page shows the entry and which fields it needs. +Integrations are the tools, frameworks, and AI libraries that work with Valkey: + +- Tools and frameworks: CLI and GUI apps, deployment and operations tooling, + proxies and gateways, frameworks and ORMs, queues and background jobs, + data-movement connectors, and observability or testing tools. +- AI and agent libraries: AI memory platforms, agent frameworks, vector stores, + RAG and retrieval, and inference or serving engines. + +Name the file after your entry, lowercase and hyphenated (e.g. +`valkey-glide-python.json`). +CI checks every entry with `build/validate-libraries.py`, against +`_data/libraries/schema.json`, the manifest, and the SPDX license list. To run +the check locally, use `./build/validate-libraries.py` with +[uv](https://docs.astral.sh/uv/) installed. The README's "Checking catalog +entries" section shows how to run it without uv. No other repositories are +needed. + +## Fields + +Entries are flat JSON objects, apart from a client's `features`. Use only the +fields below. + +**Required for all entries:** + +- `name` (string) — official name. +- `description` (string) — one or two factual, vendor-neutral sentences. The maximum length is set in `_data/libraries/schema.json`. +- `license` (string) — SPDX identifier, e.g. `"Apache-2.0"`, `"MIT"`, `"BSD-3-Clause"`. Must be an OSI-approved open-source license. +- `repository` (string) — public source-code repository URL. Entries whose repository is under `https://github.com/valkey-io/` are shown as maintained by Valkey. +- `documentation` (string) — a link to a page that shows how to use the project with Valkey. For a page on valkey.io, use a root-relative path such as `/topics/cli/`; for any other site, an `https://` URL. Link the first of these that exists: a Valkey page in the project's docs, a Valkey example, a getting-started guide, or the README section that shows basic usage. For a library that speaks the Redis protocol, a page that shows the connection setup is enough. A homepage, a docs index, or a directory listing isn't. Link GitHub files at a tag or commit (`/blob/v1.2.3/…`), not a branch. + +**Required for clients:** + +- `language` (string) — primary language. Integrations that are imported into code, such as ORM adapters, SDK wrappers and agent frameworks, set it too. Standalone tools leave it out. If a library has a separate package for each language, add one entry per language. The allowed languages are in `_data/libraries/schema.json`. If yours isn't there, add it to the schema in your PR. +- `features` (object of true/false flags) — one flag for each feature key listed under `features` in `_data/libraries/schema.json`. Set a flag to `true` if the client supports the feature, otherwise `false`. Include them all. + +The client example below shows the format. +The label and definition of each feature, as shown on the site, are in `featureDescriptions` in `_data/libraries/metadata.json`. +A feature that every listed client supports, or that none does, isn't shown on cards or in the legend, because it doesn't tell clients apart. +So a feature you set can appear or disappear as other clients are added or removed. + +**Optional fields:** + +- `tags` (array of strings) — category tags that integrations use for filtering and discovery. Client entries have no tags. The allowed tags are in `_data/libraries/schema.json`. Use one of those where it fits. If none does, propose a new tag by adding it to the schema in your PR. +- `installCommand` (string) — primary install command, e.g. `"pip install valkey-glide"`. + +## Example entries + +Client (`_data/libraries/clients/valkey-glide-python.json`): + +```json +{ + "name": "Valkey GLIDE Python", + "language": "Python", + "license": "Apache-2.0", + "description": "Designed for reliability, optimized performance, and high-availability. GLIDE is a multi-language client with one shared Rust core and per-language bindings.", + "repository": "https://github.com/valkey-io/valkey-glide/tree/main/python", + "documentation": "https://glide.valkey.io/getting-started/quickstart/?lang=python", + "installCommand": "pip install valkey-glide", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": true, + "latency": false, + "az": true, + "csc": false, + "capa": false, + "pool": false + } +} +``` + +Integration (`_data/libraries/integrations/valkey-cli.json`): + +```json +{ + "name": "valkey-cli", + "language": "C", + "license": "BSD-3-Clause", + "description": "The interactive terminal client shipped with valkey-server, with cluster-aware routing, --scan, --bigkeys and latency diagnostics built in.", + "repository": "https://github.com/valkey-io/valkey", + "documentation": "/topics/cli/", + "tags": ["CLI & GUI"] +} +``` + +## Register in the manifest + +Add the file's path (relative to `_data/libraries/`) to the matching array in +`_data/libraries/manifest.json` — `clients` or `integrations`. List it exactly +once; the page sorts cards by name, so the position doesn't matter. A file not +in the manifest will not appear on the site. + +--- + +### Submission checklist + +- [ ] Valid JSON, placed in `clients/` or `integrations/`, and registered once in `_data/libraries/manifest.json`. +- [ ] All required fields present: `name`, `description`, `license`, `repository`, `documentation` — plus `language` and `features` for clients, with every flag. +- [ ] Tested compatible with Valkey 7.2 or above. +- [ ] OSI-approved open-source license. +- [ ] Repository is publicly accessible, active within the last 6 months, and has a README with basic usage. +- [ ] `documentation` opens on a page that shows how to use the project with Valkey, not a homepage or an index. +- [ ] Description is factual, vendor-neutral and within the length limit in `_data/libraries/schema.json`. Every `features` flag and compatibility claim is accurate. +- [ ] Commits are signed per the DCO using `--signoff`. + +By submitting this pull request, I confirm that my contribution is made under the terms of the BSD-3-Clause License. diff --git a/.github/workflows/validate-libraries.yml b/.github/workflows/validate-libraries.yml new file mode 100644 index 00000000..2fbff789 --- /dev/null +++ b/.github/workflows/validate-libraries.yml @@ -0,0 +1,37 @@ +name: Validate library catalog + +on: + pull_request: + branches: ["main"] + paths: + - '_data/libraries/**' + - 'build/validate-libraries.py' + - '.github/workflows/validate-libraries.yml' + workflow_call: + inputs: + ref: + description: The Git ref to validate. Leave it empty to validate the commit that triggered the run. + type: string + required: false + default: '' + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ inputs.ref }} + + - name: Install uv + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + with: + # The script's header pins its dependencies, so it is the cache key. + cache-dependency-glob: build/validate-libraries.py + + - name: Validate catalog entries + run: ./build/validate-libraries.py diff --git a/.github/workflows/zola-deploy.yml b/.github/workflows/zola-deploy.yml index d39216de..dba6400f 100644 --- a/.github/workflows/zola-deploy.yml +++ b/.github/workflows/zola-deploy.yml @@ -17,8 +17,11 @@ permissions: jobs: build: runs-on: ubuntu-latest + outputs: + commit: ${{ steps.checkout.outputs.commit }} steps: - name: Checkout + id: checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: ref: ${{ github.event_name != 'pull_request' && 'main' || '' }} @@ -54,11 +57,10 @@ jobs: repository: valkey-io/valkey-json path: valkey-json - - name: Init commands, topics and clients + - name: Init commands and topics run: | cd website - ./build/init-topics-and-clients.sh ../valkey-doc/topics \ - ../valkey-doc/clients + ./build/init-topics.sh ../valkey-doc/topics ./build/init-commands.sh ../valkey-doc/commands \ ../valkey/src/commands ../valkey-bloom/src/commands \ ../valkey-json/src/commands ../valkey-search/src/commands @@ -76,13 +78,22 @@ jobs: with: path: ./website/public + validate: + needs: build + if: github.event_name != 'pull_request' + uses: ./.github/workflows/validate-libraries.yml + with: + ref: ${{ needs.build.outputs.commit }} + permissions: + contents: read + deploy: if: github.event_name != 'pull_request' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - needs: build + needs: [build, validate] steps: - name: Deploy to GitHub Pages id: deployment diff --git a/.gitignore b/.gitignore index e9aeccba..dd673b2d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ build-command-json build-json-command-json build-search-command-json build-topics -build-clients content/commands/* !content/commands/_index.md content/topics/* diff --git a/README.md b/README.md index 3712f3b9..fc63930c 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,34 @@ Open your browser to `http://127.0.0.1:1111/` Zola will automatically rebuild on changes to the template or any content stored in this repo. Changes to external content (command reference, documentation topics) require a restart of the Zola server process (`ctrl-c` then `zola serve` again, a browser refresh may also be needed). +### Checking catalog entries + +The clients and integrations entries in `_data/libraries/` are checked by `build/validate-libraries.py`, against the rules in `_data/libraries/schema.json` and the SPDX license list. +CI runs it on pull requests that change the catalog and before every deploy. +To run it locally with [uv](https://docs.astral.sh/uv/), which installs the dependencies pinned in the script's `# /// script` header: + +```shell +./build/validate-libraries.py +``` + +Without uv, install the dependencies listed in that header yourself, then run `python3 build/validate-libraries.py` with the Python version the header requires. + +The script downloads the SPDX license list, so it needs network access. + ## Building additional content -**By default, the site will build without documentation topics, command reference, or the clients page.** -The content for documentation topics and the clients page are stored within the `valkey-io/valkey-doc` repo. +**By default, the site will build without documentation topics or the command reference.** +The content for documentation topics is stored within the `valkey-io/valkey-doc` repo. The content for the command reference page is in the `valkey-io/valkey` repo. +The clients and integrations pages (`/clients/`, `/integrations/`) are built from `_data/libraries/` in this repo, so they need no other repo. If you want to build the site with this content, you'll need to have a local copy of `valkey-io/valkey-doc` and `valkey-io/valkey` _outside_ of this repo. -Then follow the instructions to [build the documentation topics and clients](#building-the-documentation-topics-and-clients-page) and/or [build the command reference](#building-the-command-reference). +Then follow the instructions to [build the documentation topics](#building-the-documentation-topics) and/or [build the command reference](#building-the-command-reference). The instructions show how to use scripts that create symbolic links to the `valkey-io/valkey-doc` and `valkey-io/valkey` repos as well as create a series of empty stub files that tell Zola to create pages. -### Building the documentation topics and clients page +### Building the documentation topics -Documentation 'topics' (i.e. `/topics/keyspace/`, `/topics/encryption/`, `/topics/transactions/`) and the client libraries' data (i.e. `/client-page-clients/nodejs/valkey-glide`, `/client-page-clients/python/valkey-py`) sources content from `valkey-io/valkey-doc`. +Documentation 'topics' (i.e. `/topics/keyspace/`, `/topics/encryption/`, `/topics/transactions/`) source content from `valkey-io/valkey-doc`. ```mermaid flowchart TD @@ -52,12 +67,12 @@ First, stop the `zola serve` process if you're running it. From the root directory of this repo run: ```shell -# You should only need to run this once or when you add a new topic/client. -./build/init-topics-and-clients.sh ../valkey-doc/topics ../valkey-doc/clients +# You should only need to run this once or when you add a new topic. +./build/init-topics.sh ../valkey-doc/topics ``` Then, restart Zola. -Point your browser at `http://127.0.0.1:1111/topics/` and you should see the fully populated list of topics and clients. +Point your browser at `http://127.0.0.1:1111/topics/` and you should see the fully populated list of topics. All files created in this process are ignored by git. Commit your changes to your local copy of `valkey-io/valkey-doc`. diff --git a/_data/lang_colors.toml b/_data/lang_colors.toml new file mode 100644 index 00000000..393bdf86 --- /dev/null +++ b/_data/lang_colors.toml @@ -0,0 +1,60 @@ +# Language chip colors for the client/integration catalog (see templates/libraries.html). +# +# Colors are the per-language "color" values from GitHub Linguist's +# lib/linguist/languages.yml, which is MIT-licensed data (colors, not logos), so +# there is no trademark exposure — unlike bundling brand logos. See: +# https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml +# https://github.com/github-linguist/linguist/blob/main/LICENSE (MIT) +# +# Each entry is keyed by the language slug the template computes from the +# `language` field (lowercased; `#` -> `-sharp`, `.` and spaces -> `-`), e.g. +# "C#" -> "c-sharp", "Node.js" -> "node-js". +# +# Fields: +# color — chip background (Linguist hex). +# fg — chip text/monogram color; use "#000" on light chips, "#fff" on dark. +# mono — 1-2 char monogram shown in the badge. +# +# Note: Node.js is a runtime, not a Linguist language, so it has no Linguist +# color of its own; we reuse the JavaScript color (#f1e05a) for the "node-js" +# slug since Node.js clients are JavaScript/TypeScript. + +[python] +color = "#3572A5" +fg = "#fff" +mono = "Py" + +[node-js] +color = "#f1e05a" +fg = "#000" +mono = "Js" + +[java] +color = "#b07219" +fg = "#fff" +mono = "Jv" + +[c-sharp] +color = "#178600" +fg = "#fff" +mono = "C#" + +[go] +color = "#00ADD8" +fg = "#fff" +mono = "Go" + +[php] +color = "#4F5D95" +fg = "#fff" +mono = "PHP" + +[swift] +color = "#F05138" +fg = "#fff" +mono = "Sw" + +[ruby] +color = "#701516" +fg = "#fff" +mono = "Rb" diff --git a/_data/libraries/clients/iovalkey.json b/_data/libraries/clients/iovalkey.json new file mode 100644 index 00000000..c43d8d87 --- /dev/null +++ b/_data/libraries/clients/iovalkey.json @@ -0,0 +1,20 @@ +{ + "name": "iovalkey", + "language": "Node.js", + "license": "MIT", + "description": "A friendly fork of a widely used Node.js client, tracking Valkey. Promise-based API, Lua scripting and cluster support.", + "repository": "https://github.com/valkey-io/iovalkey", + "documentation": "https://github.com/valkey-io/iovalkey/blob/v0.4.0/README.md#quick-start", + "installCommand": "npm install iovalkey", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": false, + "latency": false, + "az": false, + "csc": false, + "capa": false, + "pool": true + } +} diff --git a/_data/libraries/clients/phpredis.json b/_data/libraries/clients/phpredis.json new file mode 100644 index 00000000..50c4cd7b --- /dev/null +++ b/_data/libraries/clients/phpredis.json @@ -0,0 +1,20 @@ +{ + "name": "phpredis", + "language": "PHP", + "license": "PHP-3.01", + "description": "A client implemented as a PHP extension in C. Supports clustering, Sentinel, client-side sharding and storing PHP sessions.", + "repository": "https://github.com/phpredis/phpredis", + "documentation": "https://github.com/phpredis/phpredis/blob/6.3.0/README.md#connect-open", + "installCommand": "pie install phpredis/phpredis", + "features": { + "replica": true, + "backoff": true, + "pubsub": false, + "scan": false, + "latency": false, + "az": false, + "csc": false, + "capa": false, + "pool": true + } +} diff --git a/_data/libraries/clients/predis.json b/_data/libraries/clients/predis.json new file mode 100644 index 00000000..12360ee6 --- /dev/null +++ b/_data/libraries/clients/predis.json @@ -0,0 +1,20 @@ +{ + "name": "predis", + "language": "PHP", + "license": "MIT", + "description": "A pure-PHP client with no extension to compile. Supports clustering, replication and transparent key prefixing.", + "repository": "https://github.com/predis/predis", + "documentation": "https://github.com/predis/predis/blob/v3.6.1/README.md#connecting-to-redis", + "installCommand": "composer require predis/predis", + "features": { + "replica": true, + "backoff": true, + "pubsub": false, + "scan": false, + "latency": false, + "az": false, + "csc": false, + "capa": false, + "pool": false + } +} diff --git a/_data/libraries/clients/redisson.json b/_data/libraries/clients/redisson.json new file mode 100644 index 00000000..d8599dfb --- /dev/null +++ b/_data/libraries/clients/redisson.json @@ -0,0 +1,20 @@ +{ + "name": "redisson", + "language": "Java", + "license": "Apache-2.0", + "description": "A Java client that exposes Valkey as familiar JDK structures — Map, Lock, Queue, ExecutorService — plus client-side caching.", + "repository": "https://github.com/redisson/redisson", + "documentation": "https://redisson.pro/docs/getting-started/", + "installCommand": "implementation 'org.redisson:redisson:4.+'", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": true, + "latency": false, + "az": false, + "csc": true, + "capa": true, + "pool": true + } +} diff --git a/_data/libraries/clients/valkey-glide-csharp.json b/_data/libraries/clients/valkey-glide-csharp.json new file mode 100644 index 00000000..86018a3f --- /dev/null +++ b/_data/libraries/clients/valkey-glide-csharp.json @@ -0,0 +1,20 @@ +{ + "name": "Valkey GLIDE C#", + "language": "C#", + "license": "Apache-2.0", + "description": "Designed for reliability, optimized performance, and high-availability. The .NET client over the shared Rust core, with StackExchange.Redis API compatibility.", + "repository": "https://github.com/valkey-io/valkey-glide-csharp", + "documentation": "https://glide.valkey.io/getting-started/quickstart/?lang=c%23", + "installCommand": "dotnet add package Valkey.Glide", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": true, + "latency": false, + "az": false, + "csc": false, + "capa": false, + "pool": true + } +} diff --git a/_data/libraries/clients/valkey-glide-go.json b/_data/libraries/clients/valkey-glide-go.json new file mode 100644 index 00000000..09dc5f8c --- /dev/null +++ b/_data/libraries/clients/valkey-glide-go.json @@ -0,0 +1,20 @@ +{ + "name": "Valkey GLIDE Go", + "language": "Go", + "license": "Apache-2.0", + "description": "Designed for reliability, optimized performance, and high-availability. GLIDE is a multi-language client with one shared Rust core and per-language bindings.", + "repository": "https://github.com/valkey-io/valkey-glide/tree/main/go", + "documentation": "https://glide.valkey.io/getting-started/quickstart/?lang=go", + "installCommand": "go get github.com/valkey-io/valkey-glide/go/v2", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": true, + "latency": false, + "az": true, + "csc": false, + "capa": false, + "pool": false + } +} diff --git a/_data/libraries/clients/valkey-glide-java.json b/_data/libraries/clients/valkey-glide-java.json new file mode 100644 index 00000000..493beb1a --- /dev/null +++ b/_data/libraries/clients/valkey-glide-java.json @@ -0,0 +1,20 @@ +{ + "name": "Valkey GLIDE Java", + "language": "Java", + "license": "Apache-2.0", + "description": "Designed for reliability, optimized performance, and high-availability. Pick the classifier that matches your platform: osx-aarch_64, linux-aarch_64 or linux-x86_64.", + "repository": "https://github.com/valkey-io/valkey-glide/tree/main/java", + "documentation": "https://glide.valkey.io/getting-started/quickstart/?lang=java", + "installCommand": "implementation 'io.valkey:valkey-glide:2.+'", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": true, + "latency": false, + "az": true, + "csc": false, + "capa": false, + "pool": false + } +} diff --git a/_data/libraries/clients/valkey-glide-node-js.json b/_data/libraries/clients/valkey-glide-node-js.json new file mode 100644 index 00000000..51ab86ea --- /dev/null +++ b/_data/libraries/clients/valkey-glide-node-js.json @@ -0,0 +1,20 @@ +{ + "name": "Valkey GLIDE Node.js", + "language": "Node.js", + "license": "Apache-2.0", + "description": "Designed for reliability, optimized performance, and high-availability. GLIDE is a multi-language client with one shared Rust core and per-language bindings.", + "repository": "https://github.com/valkey-io/valkey-glide/tree/main/node", + "documentation": "https://glide.valkey.io/getting-started/quickstart/?lang=node", + "installCommand": "npm install @valkey/valkey-glide", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": true, + "latency": false, + "az": true, + "csc": false, + "capa": false, + "pool": false + } +} diff --git a/_data/libraries/clients/valkey-glide-php.json b/_data/libraries/clients/valkey-glide-php.json new file mode 100644 index 00000000..d0866945 --- /dev/null +++ b/_data/libraries/clients/valkey-glide-php.json @@ -0,0 +1,20 @@ +{ + "name": "Valkey GLIDE PHP", + "language": "PHP", + "license": "Apache-2.0", + "description": "Designed for reliability, optimized performance, and high-availability. GLIDE is a multi-language client with one shared Rust core and per-language bindings.", + "repository": "https://github.com/valkey-io/valkey-glide-php", + "documentation": "https://glide.valkey.io/getting-started/quickstart/?lang=php", + "installCommand": "pie install valkey-io/valkey-glide-php", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": true, + "latency": false, + "az": true, + "csc": false, + "capa": false, + "pool": false + } +} diff --git a/_data/libraries/clients/valkey-glide-python.json b/_data/libraries/clients/valkey-glide-python.json new file mode 100644 index 00000000..1f01e4c8 --- /dev/null +++ b/_data/libraries/clients/valkey-glide-python.json @@ -0,0 +1,20 @@ +{ + "name": "Valkey GLIDE Python", + "language": "Python", + "license": "Apache-2.0", + "description": "Designed for reliability, optimized performance, and high-availability. GLIDE is a multi-language client with one shared Rust core and per-language bindings.", + "repository": "https://github.com/valkey-io/valkey-glide/tree/main/python", + "documentation": "https://glide.valkey.io/getting-started/quickstart/?lang=python", + "installCommand": "pip install valkey-glide", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": true, + "latency": false, + "az": true, + "csc": false, + "capa": false, + "pool": false + } +} diff --git a/_data/libraries/clients/valkey-glide-ruby.json b/_data/libraries/clients/valkey-glide-ruby.json new file mode 100644 index 00000000..10ee458a --- /dev/null +++ b/_data/libraries/clients/valkey-glide-ruby.json @@ -0,0 +1,20 @@ +{ + "name": "Valkey GLIDE Ruby", + "language": "Ruby", + "license": "Apache-2.0", + "description": "Designed for reliability, optimized performance, and high-availability. A synchronous Ruby client over the shared Rust core, prebuilt for Linux and Apple silicon macOS.", + "repository": "https://github.com/valkey-io/valkey-glide-ruby", + "documentation": "https://glide.valkey.io/getting-started/quickstart/?lang=ruby", + "installCommand": "gem install valkey-glide-rb", + "features": { + "replica": true, + "backoff": true, + "pubsub": false, + "scan": false, + "latency": false, + "az": true, + "csc": false, + "capa": false, + "pool": false + } +} diff --git a/_data/libraries/clients/valkey-go.json b/_data/libraries/clients/valkey-go.json new file mode 100644 index 00000000..ad4d2635 --- /dev/null +++ b/_data/libraries/clients/valkey-go.json @@ -0,0 +1,20 @@ +{ + "name": "valkey-go", + "language": "Go", + "license": "Apache-2.0", + "description": "A fast Go client with automatic pipelining, server-assisted client-side caching and generic-typed command building.", + "repository": "https://github.com/valkey-io/valkey-go", + "documentation": "https://pkg.go.dev/github.com/valkey-io/valkey-go#readme-getting-started", + "installCommand": "go get github.com/valkey-io/valkey-go", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": false, + "latency": false, + "az": true, + "csc": true, + "capa": true, + "pool": true + } +} diff --git a/_data/libraries/clients/valkey-java.json b/_data/libraries/clients/valkey-java.json new file mode 100644 index 00000000..25d74d3b --- /dev/null +++ b/_data/libraries/clients/valkey-java.json @@ -0,0 +1,20 @@ +{ + "name": "valkey-java", + "language": "Java", + "license": "MIT", + "description": "The Jedis-derived Java client maintained under valkey-io. Blocking API, connection pooling and cluster support.", + "repository": "https://github.com/valkey-io/valkey-java", + "documentation": "https://github.com/valkey-io/valkey-java/blob/v5.5.0/README.md#getting-started", + "installCommand": "implementation 'io.valkey:valkey-java:5.+'", + "features": { + "replica": false, + "backoff": true, + "pubsub": false, + "scan": false, + "latency": false, + "az": false, + "csc": false, + "capa": true, + "pool": true + } +} diff --git a/_data/libraries/clients/valkey-py.json b/_data/libraries/clients/valkey-py.json new file mode 100644 index 00000000..71904f7e --- /dev/null +++ b/_data/libraries/clients/valkey-py.json @@ -0,0 +1,20 @@ +{ + "name": "valkey-py", + "language": "Python", + "license": "MIT", + "description": "The community fork of the long-standing Python client, maintained by the Valkey project. Familiar synchronous and asyncio APIs.", + "repository": "https://github.com/valkey-io/valkey-py", + "documentation": "https://valkey-py.readthedocs.io/en/latest/", + "installCommand": "pip install valkey", + "features": { + "replica": true, + "backoff": true, + "pubsub": true, + "scan": false, + "latency": false, + "az": false, + "csc": true, + "capa": false, + "pool": true + } +} diff --git a/_data/libraries/clients/valkey-swift.json b/_data/libraries/clients/valkey-swift.json new file mode 100644 index 00000000..a8af0ba3 --- /dev/null +++ b/_data/libraries/clients/valkey-swift.json @@ -0,0 +1,20 @@ +{ + "name": "valkey-swift", + "language": "Swift", + "license": "Apache-2.0", + "description": "A Swift client built on Swift Concurrency and NIO, for server-side Swift and Apple platforms.", + "repository": "https://github.com/valkey-io/valkey-swift", + "documentation": "https://swiftpackageindex.com/valkey-io/valkey-swift/documentation/valkey/getting-started", + "installCommand": "swift package add-dependency https://github.com/valkey-io/valkey-swift", + "features": { + "replica": true, + "backoff": true, + "pubsub": false, + "scan": false, + "latency": false, + "az": false, + "csc": false, + "capa": true, + "pool": true + } +} diff --git a/_data/libraries/integrations/agno.json b/_data/libraries/integrations/agno.json new file mode 100644 index 00000000..cc52edaf --- /dev/null +++ b/_data/libraries/integrations/agno.json @@ -0,0 +1,12 @@ +{ + "name": "Agno", + "language": "Python", + "license": "Apache-2.0", + "description": "Agent framework with Valkey as a session and state store, plus a Valkey vector store (requires valkey-bundle) supporting vector and keyword search.", + "repository": "https://github.com/agno-agi/agno", + "documentation": "https://docs.agno.com/database/providers/valkey/overview", + "installCommand": "pip install 'agno[valkey]'", + "tags": [ + "Agent frameworks" + ] +} diff --git a/_data/libraries/integrations/cocoindex.json b/_data/libraries/integrations/cocoindex.json new file mode 100644 index 00000000..22141d5a --- /dev/null +++ b/_data/libraries/integrations/cocoindex.json @@ -0,0 +1,12 @@ +{ + "name": "CocoIndex", + "language": "Python", + "license": "Apache-2.0", + "description": "Data transformation framework for AI that builds and incrementally updates indexes as your sources change.", + "repository": "https://github.com/cocoindex-io/cocoindex", + "documentation": "https://cocoindex.io/docs/connectors/valkey/", + "installCommand": "pip install 'cocoindex[valkey]'", + "tags": [ + "RAG & retrieval" + ] +} diff --git a/_data/libraries/integrations/cognee-valkey-adapter.json b/_data/libraries/integrations/cognee-valkey-adapter.json new file mode 100644 index 00000000..bc66ca15 --- /dev/null +++ b/_data/libraries/integrations/cognee-valkey-adapter.json @@ -0,0 +1,12 @@ +{ + "name": "Cognee Valkey Adapter", + "language": "Python", + "license": "Apache-2.0", + "description": "Cognee is an AI memory engine building knowledge graphs from your data, which has an adapter for storing embeddings in Valkey (requires valkey-bundle).", + "repository": "https://github.com/topoteretes/cognee-community", + "documentation": "https://github.com/topoteretes/cognee-community/blob/25980633a5d047e6e41b1f65e5607a44ca0dd7a2/packages/vector/valkey/examples/example.py", + "installCommand": "pip install cognee-community-vector-adapter-valkey", + "tags": [ + "Agent memory" + ] +} diff --git a/_data/libraries/integrations/huey.json b/_data/libraries/integrations/huey.json new file mode 100644 index 00000000..71aaf80e --- /dev/null +++ b/_data/libraries/integrations/huey.json @@ -0,0 +1,12 @@ +{ + "name": "huey", + "language": "Python", + "license": "MIT", + "description": "Python task queue with periodic tasks and retries, with a Valkey backend built on the official Valkey GLIDE client.", + "repository": "https://github.com/coleifer/huey", + "documentation": "https://huey.readthedocs.io/en/latest/recipes.html#high-availability-redis-sentinel-valkey-and-cluster", + "installCommand": "pip install huey valkey-glide-sync", + "tags": [ + "Queues & background jobs" + ] +} diff --git a/_data/libraries/integrations/langbot.json b/_data/libraries/integrations/langbot.json new file mode 100644 index 00000000..26a4e17c --- /dev/null +++ b/_data/libraries/integrations/langbot.json @@ -0,0 +1,12 @@ +{ + "name": "LangBot", + "language": "Python", + "license": "Apache-2.0", + "description": "Chat-bot platform whose knowledge base can use a Valkey vector store, built on the official Valkey GLIDE client (requires valkey-bundle; not available on Windows).", + "repository": "https://github.com/langbot-app/LangBot", + "documentation": "https://github.com/langbot-app/LangBot/blob/v4.10.11/docs/VALKEY_SEARCH_INTEGRATION.md", + "installCommand": "pip install langbot", + "tags": [ + "Agent frameworks" + ] +} diff --git a/_data/libraries/integrations/langflow.json b/_data/libraries/integrations/langflow.json new file mode 100644 index 00000000..fc072596 --- /dev/null +++ b/_data/libraries/integrations/langflow.json @@ -0,0 +1,12 @@ +{ + "name": "Langflow", + "language": "Python", + "license": "MIT", + "description": "Visual builder for agent and RAG flows, with Valkey vector store (requires valkey-bundle) and chat memory components shipped as extensions.", + "repository": "https://github.com/langflow-ai/langflow", + "documentation": "https://docs.langflow.org/bundles-valkey", + "installCommand": "pip install \"langflow[bundles]\"", + "tags": [ + "RAG & retrieval" + ] +} diff --git a/_data/libraries/integrations/litellm.json b/_data/libraries/integrations/litellm.json new file mode 100644 index 00000000..39704966 --- /dev/null +++ b/_data/libraries/integrations/litellm.json @@ -0,0 +1,12 @@ +{ + "name": "LiteLLM", + "language": "Python", + "license": "MIT", + "description": "Unified gateway for 100+ LLM APIs, with a Valkey-backed semantic cache for prompt and response reuse (requires valkey-bundle).", + "repository": "https://github.com/BerriAI/litellm", + "documentation": "https://docs.litellm.ai/docs/proxy/caching_semantic#valkey", + "installCommand": "pip install litellm 'redis<6'", + "tags": [ + "Inference & serving" + ] +} diff --git a/_data/libraries/integrations/praisonai.json b/_data/libraries/integrations/praisonai.json new file mode 100644 index 00000000..aafd6f6c --- /dev/null +++ b/_data/libraries/integrations/praisonai.json @@ -0,0 +1,12 @@ +{ + "name": "PraisonAI", + "language": "Python", + "license": "MIT", + "description": "Multi-agent framework for building production agent teams, with pluggable retrieval (requires valkey-bundle), and storage.", + "repository": "https://github.com/MervinPraison/PraisonAI", + "documentation": "https://praison.ai/docs/features/persistence#url-schemes-per-store-kind", + "installCommand": "pip install 'praisonai[valkey]'", + "tags": [ + "Agent frameworks" + ] +} diff --git a/_data/libraries/integrations/redsync.json b/_data/libraries/integrations/redsync.json new file mode 100644 index 00000000..07c1ebd4 --- /dev/null +++ b/_data/libraries/integrations/redsync.json @@ -0,0 +1,12 @@ +{ + "name": "Redsync", + "language": "Go", + "license": "BSD-3-Clause", + "description": "Distributed mutual-exclusion lock for Go, coordinating a quorum across several independent nodes.", + "repository": "https://github.com/go-redsync/redsync", + "documentation": "https://github.com/go-redsync/redsync/blob/v4.18.0/examples/valkego/main.go", + "installCommand": "go get github.com/go-redsync/redsync/v4", + "tags": [ + "Frameworks & ORMs" + ] +} diff --git a/_data/libraries/integrations/rq.json b/_data/libraries/integrations/rq.json new file mode 100644 index 00000000..7fc84fd1 --- /dev/null +++ b/_data/libraries/integrations/rq.json @@ -0,0 +1,12 @@ +{ + "name": "Rq", + "language": "Python", + "license": "BSD-2-Clause", + "description": "Minimalistic Python task queue that supports Valkey.", + "repository": "https://github.com/rq/rq", + "documentation": "https://python-rq.org/", + "installCommand": "pip install rq", + "tags": [ + "Queues & background jobs" + ] +} diff --git a/_data/libraries/integrations/sidekiq.json b/_data/libraries/integrations/sidekiq.json new file mode 100644 index 00000000..10ee73ad --- /dev/null +++ b/_data/libraries/integrations/sidekiq.json @@ -0,0 +1,12 @@ +{ + "name": "Sidekiq", + "language": "Ruby", + "license": "LGPL-3.0-only", + "description": "Background job processing for Ruby applications, with maintained support for Valkey 7.2 and above.", + "repository": "https://github.com/sidekiq/sidekiq", + "documentation": "https://github.com/sidekiq/sidekiq/wiki/Using-Redis/b685f8063a1147679a5221f675433d40588bdaff", + "installCommand": "gem install sidekiq", + "tags": [ + "Queues & background jobs" + ] +} diff --git a/_data/libraries/integrations/spring-data-valkey.json b/_data/libraries/integrations/spring-data-valkey.json new file mode 100644 index 00000000..710fc03d --- /dev/null +++ b/_data/libraries/integrations/spring-data-valkey.json @@ -0,0 +1,12 @@ +{ + "name": "Spring Data Valkey", + "language": "Java", + "license": "Apache-2.0", + "description": "Spring Data module for Valkey — templates, repositories and Spring Cache backing for JVM applications.", + "repository": "https://github.com/valkey-io/spring-data-valkey", + "documentation": "https://spring.valkey.io/valkey/getting-started/", + "tags": [ + "Frameworks & ORMs" + ], + "installCommand": "implementation 'io.valkey.springframework.boot:spring-boot-starter-data-valkey:2.0.1'" +} diff --git a/_data/libraries/integrations/valkey-admin.json b/_data/libraries/integrations/valkey-admin.json new file mode 100644 index 00000000..01e71832 --- /dev/null +++ b/_data/libraries/integrations/valkey-admin.json @@ -0,0 +1,11 @@ +{ + "name": "Valkey Admin", + "language": "TypeScript", + "license": "Apache-2.0", + "description": "Administration console for standalone and clustered deployments: browse and edit keys, inspect slots and replication, watch live metrics and run commands from a single web interface.", + "repository": "https://github.com/valkey-io/valkey-admin", + "documentation": "https://valkey-admin.valkey.io/deployment/docker/", + "tags": [ + "CLI & GUI" + ] +} diff --git a/_data/libraries/integrations/valkey-cli.json b/_data/libraries/integrations/valkey-cli.json new file mode 100644 index 00000000..dea216ad --- /dev/null +++ b/_data/libraries/integrations/valkey-cli.json @@ -0,0 +1,11 @@ +{ + "name": "valkey-cli", + "language": "C", + "license": "BSD-3-Clause", + "description": "The interactive terminal client shipped with valkey-server, with cluster-aware routing, --scan, --bigkeys and latency diagnostics built in.", + "repository": "https://github.com/valkey-io/valkey", + "documentation": "/topics/cli/", + "tags": [ + "CLI & GUI" + ] +} diff --git a/_data/libraries/integrations/valkey-container.json b/_data/libraries/integrations/valkey-container.json new file mode 100644 index 00000000..0f7226b8 --- /dev/null +++ b/_data/libraries/integrations/valkey-container.json @@ -0,0 +1,11 @@ +{ + "name": "valkey-container", + "language": "Shell", + "license": "BSD-3-Clause", + "description": "Sources for the official Valkey container images.", + "repository": "https://github.com/valkey-io/valkey-container", + "documentation": "https://hub.docker.com/r/valkey/valkey", + "tags": [ + "Deploy & operate" + ] +} diff --git a/_data/libraries/integrations/valkey-helm.json b/_data/libraries/integrations/valkey-helm.json new file mode 100644 index 00000000..2323a889 --- /dev/null +++ b/_data/libraries/integrations/valkey-helm.json @@ -0,0 +1,10 @@ +{ + "name": "valkey-helm", + "license": "BSD-3-Clause", + "description": "Official Helm chart for installing Valkey on Kubernetes.", + "repository": "https://github.com/valkey-io/valkey-helm", + "documentation": "https://github.com/valkey-io/valkey-helm/blob/valkey-0.12.0/README.md#tldr", + "tags": [ + "Deploy & operate" + ] +} diff --git a/_data/libraries/integrations/valkey-integration-for-haystack.json b/_data/libraries/integrations/valkey-integration-for-haystack.json new file mode 100644 index 00000000..cf0b7b2a --- /dev/null +++ b/_data/libraries/integrations/valkey-integration-for-haystack.json @@ -0,0 +1,12 @@ +{ + "name": "Valkey Integration for Haystack", + "language": "Python", + "license": "Apache-2.0", + "description": "Document and embedding store for Haystack RAG pipelines, backed by Valkey (requires valkey-bundle).", + "repository": "https://github.com/deepset-ai/haystack-core-integrations", + "documentation": "https://haystack.deepset.ai/integrations/valkey", + "installCommand": "pip install valkey-haystack", + "tags": [ + "RAG & retrieval" + ] +} diff --git a/_data/libraries/integrations/valkey-mcp-server.json b/_data/libraries/integrations/valkey-mcp-server.json new file mode 100644 index 00000000..56e9fcd0 --- /dev/null +++ b/_data/libraries/integrations/valkey-mcp-server.json @@ -0,0 +1,12 @@ +{ + "name": "Valkey MCP Server", + "language": "Python", + "license": "Apache-2.0", + "description": "Model Context Protocol server exposing Valkey to AI agents through search, JSON, and tiered read/write/admin command tools (requires valkey-bundle).", + "repository": "https://github.com/awslabs/mcp/tree/main/src/valkey-mcp-server", + "documentation": "https://awslabs.github.io/mcp/servers/valkey-mcp-server", + "installCommand": "uvx awslabs.valkey-mcp-server@latest", + "tags": [ + "Agent frameworks" + ] +} diff --git a/_data/libraries/integrations/valkey-message-queue-starter.json b/_data/libraries/integrations/valkey-message-queue-starter.json new file mode 100644 index 00000000..b57fa377 --- /dev/null +++ b/_data/libraries/integrations/valkey-message-queue-starter.json @@ -0,0 +1,12 @@ +{ + "name": "Valkey Message Queue Starter", + "language": "TypeScript", + "license": "MIT", + "description": "Next.js starter template demonstrating a reliable message queue built on Valkey Streams, using consumer groups and pending-entry acknowledgement.", + "repository": "https://github.com/vercel/examples/tree/main/solutions/aws-message-queue-elasticache", + "documentation": "https://vercel.com/templates/next.js/valkey-message-queue-with-elasticache-and-next-js", + "installCommand": "pnpm create next-app --example https://github.com/vercel/examples/tree/main/solutions/aws-message-queue-elasticache", + "tags": [ + "Deploy & operate" + ] +} diff --git a/_data/libraries/integrations/valkey-operator.json b/_data/libraries/integrations/valkey-operator.json new file mode 100644 index 00000000..9c072686 --- /dev/null +++ b/_data/libraries/integrations/valkey-operator.json @@ -0,0 +1,11 @@ +{ + "name": "valkey-operator", + "language": "Go", + "license": "Apache-2.0", + "description": "Kubernetes operator for deploying Valkey clusters and managing their lifecycle. Work in progress.", + "repository": "https://github.com/valkey-io/valkey-operator", + "documentation": "https://github.com/valkey-io/valkey-operator/blob/v0.7.1/docs/quickstart.md", + "tags": [ + "Deploy & operate" + ] +} diff --git a/_data/libraries/integrations/valkey-skills.json b/_data/libraries/integrations/valkey-skills.json new file mode 100644 index 00000000..6039af26 --- /dev/null +++ b/_data/libraries/integrations/valkey-skills.json @@ -0,0 +1,10 @@ +{ + "name": "valkey-skills", + "license": "BSD-3-Clause", + "description": "Reusable, domain-specific skills that teach coding agents how to work with Valkey correctly.", + "repository": "https://github.com/valkey-io/valkey-skills", + "documentation": "https://github.com/valkey-io/valkey-skills/blob/c7f563970ae4702d1c83c9da27ac8cdbca15fa7b/README.md#install", + "tags": [ + "Agent frameworks" + ] +} diff --git a/_data/libraries/integrations/vllm-semantic-router.json b/_data/libraries/integrations/vllm-semantic-router.json new file mode 100644 index 00000000..22355e0c --- /dev/null +++ b/_data/libraries/integrations/vllm-semantic-router.json @@ -0,0 +1,11 @@ +{ + "name": "vLLM Semantic Router", + "language": "Go", + "license": "Apache-2.0", + "description": "Mixture-of-Models router for LLM inference, with a Valkey-backed semantic cache, conversation memory and vector store (requires valkey-bundle).", + "repository": "https://github.com/vllm-project/semantic-router", + "documentation": "https://vllm-sr.ai/docs/installation/valkey-memory/", + "tags": [ + "Inference & serving" + ] +} diff --git a/_data/libraries/manifest.json b/_data/libraries/manifest.json new file mode 100644 index 00000000..2eef599b --- /dev/null +++ b/_data/libraries/manifest.json @@ -0,0 +1,43 @@ +{ + "clients": [ + "clients/valkey-glide-python.json", + "clients/valkey-py.json", + "clients/valkey-glide-node-js.json", + "clients/iovalkey.json", + "clients/valkey-glide-java.json", + "clients/valkey-java.json", + "clients/redisson.json", + "clients/valkey-glide-go.json", + "clients/valkey-go.json", + "clients/phpredis.json", + "clients/predis.json", + "clients/valkey-swift.json", + "clients/valkey-glide-csharp.json", + "clients/valkey-glide-php.json", + "clients/valkey-glide-ruby.json" + ], + "integrations": [ + "integrations/valkey-admin.json", + "integrations/valkey-cli.json", + "integrations/valkey-operator.json", + "integrations/valkey-helm.json", + "integrations/valkey-container.json", + "integrations/spring-data-valkey.json", + "integrations/redsync.json", + "integrations/rq.json", + "integrations/huey.json", + "integrations/sidekiq.json", + "integrations/cocoindex.json", + "integrations/valkey-integration-for-haystack.json", + "integrations/cognee-valkey-adapter.json", + "integrations/praisonai.json", + "integrations/valkey-skills.json", + "integrations/agno.json", + "integrations/langbot.json", + "integrations/langflow.json", + "integrations/litellm.json", + "integrations/valkey-mcp-server.json", + "integrations/valkey-message-queue-starter.json", + "integrations/vllm-semantic-router.json" + ] +} diff --git a/_data/libraries/metadata.json b/_data/libraries/metadata.json new file mode 100644 index 00000000..b8ca79e9 --- /dev/null +++ b/_data/libraries/metadata.json @@ -0,0 +1,49 @@ +{ + "featureDescriptions": [ + { + "key": "replica", + "label": "Replica reads", + "definition": "Route read commands to replicas instead of the primary, spreading read load across the topology." + }, + { + "key": "backoff", + "label": "Retry & backoff", + "definition": "Back off progressively instead of hammering a server that is overloaded or restarting, avoiding connection storms." + }, + { + "key": "pubsub", + "label": "Pub/Sub", + "definition": "Re-subscribe to channels and patterns automatically after a reconnect or topology change, so subscribers never go silently deaf." + }, + { + "key": "scan", + "label": "SCAN family", + "definition": "Iterate keys consistently across every shard, tracking slot migrations that happen mid-scan." + }, + { + "key": "latency", + "label": "Low-latency", + "definition": "Send reads to whichever replica is currently answering fastest, measured by the client itself." + }, + { + "key": "az", + "label": "AZ affinity", + "definition": "Prefer replicas in the client’s own availability zone to cut cross-AZ latency and transfer cost." + }, + { + "key": "csc", + "label": "Client-side caching", + "definition": "Keep hot values in the application process and let the server invalidate them, removing a round trip entirely." + }, + { + "key": "capa", + "label": "Advanced capabilities", + "definition": "Announce client capabilities so the server can hand off slot redirections cleanly during upgrades and failover." + }, + { + "key": "pool", + "label": "Connection pooling", + "definition": "Reuse a pool of established connections rather than dialing one per command." + } + ] +} diff --git a/_data/libraries/schema.json b/_data/libraries/schema.json new file mode 100644 index 00000000..39156ad8 --- /dev/null +++ b/_data/libraries/schema.json @@ -0,0 +1,180 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Valkey clients and integrations catalog", + "description": "Rules for the files in `_data/libraries/`. `build/validate-libraries.py` checks each entry file listed under `clients` in `manifest.json` against `$defs/client` and each listed under `integrations` against `$defs/integration`. It checks `manifest.json` against `$defs/manifest` and `metadata.json` against `$defs/metadata`. It then runs the checks that span files or need the SPDX license list.", + "$comment": "The script prints a rule's description when that rule fails, so each description is on the subschema that fails. Each `not` rule repeats `\"type\": \"string\"` so that a value of the wrong type is reported once, by `type`. The URL patterns' character classes allow whitespace, and in Python `$` also matches before a final newline, so a `not` rule rejects whitespace in URLs. The fields that clients and integrations share are each defined once in `$defs` and referenced with `$ref`.", + "$defs": { + "client": { + "description": "A client entry, stored in `clients/`, is a JSON object that describes one client library.", + "type": "object", + "additionalProperties": false, + "required": ["name", "description", "license", "repository", "documentation", "language", "features"], + "properties": { + "name": { "$ref": "#/$defs/name" }, + "description": { "$ref": "#/$defs/description" }, + "license": { "$ref": "#/$defs/license" }, + "repository": { "$ref": "#/$defs/repository" }, + "documentation": { "$ref": "#/$defs/documentation" }, + "language": { "$ref": "#/$defs/language" }, + "installCommand": { "$ref": "#/$defs/installCommand" }, + "tags": { + "description": "Only integration entries, in `integrations/`, have `tags`.", + "not": {} + }, + "features": { + "description": "An object of client feature flags with exactly the keys `replica`, `backoff`, `pubsub`, `scan`, `latency`, `az`, `csc`, `capa` and `pool`, each set to `true` or `false`.", + "type": "object", + "additionalProperties": false, + "required": ["replica", "backoff", "pubsub", "scan", "latency", "az", "csc", "capa", "pool"], + "properties": { + "replica": { "description": "Set to `true` if the client supports this feature, otherwise `false`.", "type": "boolean" }, + "backoff": { "description": "Set to `true` if the client supports this feature, otherwise `false`.", "type": "boolean" }, + "pubsub": { "description": "Set to `true` if the client supports this feature, otherwise `false`.", "type": "boolean" }, + "scan": { "description": "Set to `true` if the client supports this feature, otherwise `false`.", "type": "boolean" }, + "latency": { "description": "Set to `true` if the client supports this feature, otherwise `false`.", "type": "boolean" }, + "az": { "description": "Set to `true` if the client supports this feature, otherwise `false`.", "type": "boolean" }, + "csc": { "description": "Set to `true` if the client supports this feature, otherwise `false`.", "type": "boolean" }, + "capa": { "description": "Set to `true` if the client supports this feature, otherwise `false`.", "type": "boolean" }, + "pool": { "description": "Set to `true` if the client supports this feature, otherwise `false`.", "type": "boolean" } + } + } + } + }, + "integration": { + "description": "An integration entry, stored in `integrations/`, is a JSON object that describes one tool, framework or AI library.", + "type": "object", + "additionalProperties": false, + "required": ["name", "description", "license", "repository", "documentation"], + "properties": { + "name": { "$ref": "#/$defs/name" }, + "description": { "$ref": "#/$defs/description" }, + "license": { "$ref": "#/$defs/license" }, + "repository": { "$ref": "#/$defs/repository" }, + "documentation": { "$ref": "#/$defs/documentation" }, + "language": { "$ref": "#/$defs/language" }, + "installCommand": { "$ref": "#/$defs/installCommand" }, + "tags": { + "$comment": "The tag set is fixed on purpose: adding a tag is a deliberate change to this schema.", + "description": "A non-empty list of distinct tags.", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "description": "Integration tags must be one of `CLI & GUI`, `Deploy & operate`, `Frameworks & ORMs`, `Queues & background jobs`, `RAG & retrieval`, `Agent memory`, `Agent frameworks` or `Inference & serving`.", + "enum": ["CLI & GUI", "Deploy & operate", "Frameworks & ORMs", "Queues & background jobs", "RAG & retrieval", "Agent memory", "Agent frameworks", "Inference & serving"] + } + }, + "features": { + "description": "Only client entries, in `clients/`, have `features`.", + "not": {} + } + } + }, + "name": { + "description": "The project's official name. It must not be blank or have spaces at either end.", + "type": "string", + "pattern": "\\S", + "not": { "type": "string", "pattern": "^\\s|\\s$" } + }, + "description": { + "description": "One or two factual, vendor-neutral sentences of at most 200 characters. The text must not have spaces at either end.", + "type": "string", + "maxLength": 200, + "pattern": "\\S", + "not": { "type": "string", "pattern": "^\\s|\\s$" } + }, + "license": { + "description": "An SPDX license expression made of OSI-approved licenses, such as `MIT` or `MIT OR Apache-2.0`. It must not be blank or have spaces at either end.", + "type": "string", + "pattern": "\\S", + "not": { "type": "string", "pattern": "^\\s|\\s$" } + }, + "repository": { + "$comment": "The Valkey chip is derived from this URL, so dot segments are rejected: `https://github.com/valkey-io/x/tree/a/../../../rq/rq` would otherwise give a third-party repository the chip.", + "description": "The public source repository, as an `https://` URL with a lowercase host and no port or user name. Its path must not contain spaces, `?`, `#`, `\\`, `%` or a `.` or `..` segment.", + "type": "string", + "pattern": "^https://[a-z0-9-]+(\\.[a-z0-9-]+)+(/[^?#\\\\%]*)?$", + "not": { "type": "string", "pattern": "\\s|/\\.\\.?(/|$)" }, + "if": { "pattern": "^https://(www\\.)?github\\.com(/|$)" }, + "then": { + "description": "A GitHub repository URL is `https://github.com/OWNER/REPO`, or `https://github.com/OWNER/REPO/tree/REF/PATH` for a project in a subdirectory. It has no trailing slash and no `.git` suffix.", + "pattern": "^https://github\\.com/[A-Za-z0-9-]+/[A-Za-z0-9._-]+(?=3.10" +# dependencies = ["jsonschema==4.26.0", "license-expression==30.4.4"] +# /// +"""Check the catalog files in `_data/libraries/` against `schema.json` and the SPDX License List. + +Run it as `./build/validate-libraries.py`, or as `python3 build/validate-libraries.py` after +installing the dependencies in the `# /// script` header. +""" + +import http.client +import json +import pathlib +import re +import sys +import time +import urllib.request + +import jsonschema +from license_expression import LicenseSymbol, Licensing + +SPDX_VERSION = "3.29.0" +SPDX_URL = f"https://raw.githubusercontent.com/spdx/license-list-data/v{SPDX_VERSION}/json" +DOWNLOAD_ATTEMPTS = 3 + +ROOT = pathlib.Path(__file__).resolve().parent.parent +LIBRARIES = ROOT / "_data" / "libraries" +# Each manifest group, and the directory of the same name, holds entries of one `$defs` kind. +DEFINITION_FOR_GROUP = {"clients": "client", "integrations": "integration"} +GROUPS = list(DEFINITION_FOR_GROUP) +FILE_NAME = r"[a-z0-9]+(-[a-z0-9]+)*\.json" + +# `load_json` returns this for a file it has already reported. It isn't `None`, +# because a file that contains JSON `null` parses to `None` and must still +# reach the schema check. +INVALID = object() + + +def repo_path(path): + """Return `path` relative to the repository root, for messages.""" + return path.relative_to(ROOT).as_posix() + + +def reject_duplicate_keys(pairs): + """Build a JSON object, raising `ValueError` if a key appears more than once.""" + keys = [key for key, _ in pairs] + duplicates = sorted({key for key in keys if keys.count(key) > 1}) + if duplicates: + # Reviewers read the diff, and with a repeated key the site silently uses the last value. + verb = "appears" if len(duplicates) == 1 else "appear" + raise ValueError(f"{', '.join(repr(key) for key in duplicates)} {verb} more than once") + return dict(pairs) + + +def load_json(path, problems): + """Return the parsed file, or INVALID after recording why it can't be read.""" + try: + return json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=reject_duplicate_keys) + except ValueError as error: + problems.append(f"{repo_path(path)}: is not valid JSON: {error}") + return INVALID + + +def schema_problems(path, data, schema, definition): + """Return one line per way `data` breaks `schema["$defs"][definition]`.""" + lines = [] + # The root keeps `$defs`, so the definition's `$ref`s to the shared fields resolve. + validator = jsonschema.Draft202012Validator({"$defs": schema["$defs"], "$ref": f"#/$defs/{definition}"}) + for error in sorted(validator.iter_errors(data), key=lambda e: e.json_path): + field = error.json_path.removeprefix("$").removeprefix(".") + where = f"{repo_path(path)}: {field}" if field else repo_path(path) + description = error.schema.get("description") + # For missing and unknown fields, jsonschema's own wording is already + # plain. For every other failure it would quote the raw rule, so the + # rule's description is printed instead. + if error.validator in ("required", "additionalProperties") or description is None: + line = f"{where}: {error.message}" + else: + got = json.dumps(error.instance, ensure_ascii=False) + if len(got) > 80: + got = got[:77] + "..." + if error.validator == "maxLength": + got += f" ({len(error.instance)} characters)" + line = f"{where}: {description} Got: {got}" + # A blank string breaks both its `pattern` and its `not` rule. They share + # one description, so the same line would print twice. + if line not in lines: + lines.append(line) + return lines + + +def download_spdx_list(name): + """Download and parse one SPDX list file. Try again if a network or HTTP error stops the download.""" + for attempt in range(1, DOWNLOAD_ATTEMPTS + 1): + try: + with urllib.request.urlopen(f"{SPDX_URL}/{name}", timeout=60) as response: + return json.load(response) + # Network errors, HTTP errors and timeouts are `OSError`. A body cut short + # raises `IncompleteRead`, an `HTTPException`. + except (OSError, http.client.HTTPException): + if attempt == DOWNLOAD_ATTEMPTS: + raise + time.sleep(2 * attempt) + + +def license_problems(expression, licensing, licenses, exceptions): + """Return the ways an SPDX expression breaks the catalog's license rules.""" + # On some malformed input, license-expression raises errors other than its + # own `ExpressionError`. A trailing `MIT OR` raises `AttributeError`. + try: + info = licensing.validate(expression, strict=True) + if info.errors: + return [f"'{expression}' is not a valid SPDX license expression: {'; '.join(info.errors)}"] + symbols = licensing.license_symbols(licensing.parse(expression), decompose=True) + except Exception: + return [f"'{expression}' is not a valid SPDX license expression"] + + lines = [] + # The card shows the expression as written, so it must use SPDX's spelling. + if info.normalized_expression != expression: + lines.append(f"write '{expression}' as `{info.normalized_expression}`") + for symbol in symbols: + if symbol.is_exception: + if exceptions[symbol.key]["isDeprecatedLicenseId"]: + lines.append(f"'{symbol.key}' is a deprecated SPDX exception") + continue + details = licenses[symbol.key] + if details["isDeprecatedLicenseId"]: + lines.append(f"'{symbol.key}' is a deprecated SPDX license ID. Use its current ID from https://spdx.org/licenses/") + if not details.get("isOsiApproved", False): + lines.append(f"'{symbol.key}' is not an OSI-approved license") + return lines + + +def print_problems(problems): + """Print each problem on its own line, then how many there were.""" + print("\n".join(problems), file=sys.stderr) + noun = "problem" if len(problems) == 1 else "problems" + print(f"{len(problems)} {noun} found in {repo_path(LIBRARIES)}.", file=sys.stderr) + + +def main(): + schema_path = LIBRARIES / "schema.json" + schema = json.loads(schema_path.read_text(encoding="utf-8")) + jsonschema.Draft202012Validator.check_schema(schema) + + try: + licenses = {item["licenseId"]: item for item in download_spdx_list("licenses.json")["licenses"]} + exceptions = {item["licenseExceptionId"]: item for item in download_spdx_list("exceptions.json")["exceptions"]} + except (OSError, http.client.HTTPException) as error: + print( + f"Could not download the SPDX License List {SPDX_VERSION} from {SPDX_URL} " + f"in {DOWNLOAD_ATTEMPTS} attempts: {error}", + file=sys.stderr, + ) + return 1 + # Only SPDX IDs are known symbols, so unknown IDs and `LicenseRef-*` are rejected. + licensing = Licensing( + [LicenseSymbol(key) for key in licenses] + + [LicenseSymbol(key, is_exception=True) for key in exceptions] + ) + + problems = [] + + # `manifest.json` must be valid JSON and match its schema. + manifest_path = LIBRARIES / "manifest.json" + manifest = load_json(manifest_path, problems) + if manifest is not INVALID: + problems += schema_problems(manifest_path, manifest, schema, "manifest") + if problems: + # Without a usable manifest there is no list of entries to check. + print_problems(problems) + return 1 + + # Each listed path must appear once, have the form `GROUP/NAME.json` and point at an existing file. + entries = [] # (group, path) + seen = set() + for group in GROUPS: + where = f"{repo_path(manifest_path)}: {group}" + for rel in manifest[group]: + if rel in seen: + problems.append(f"{where}: '{rel}' is listed more than once") + continue + seen.add(rel) + # Checking the whole path rules out subdirectories, other + # extensions and aliases such as `clients/./x.json`. + if not re.fullmatch(f"{group}/{FILE_NAME}", rel): + problems.append( + f"{where}: '{rel}' must be `{group}/NAME.json`. `NAME` is lowercase letters " + "and digits joined by single hyphens, such as `valkey-glide-python`." + ) + continue + path = LIBRARIES / rel + if not path.is_file(): + problems.append(f"{where}: '{rel}' does not exist") + continue + entries.append((group, path)) + + # Every file in the group directories must be listed in the manifest. + for group in GROUPS: + for path in sorted((LIBRARIES / group).rglob("*")): + # Finder adds `.DS_Store` files to folders, and `.gitignore` keeps them + # out of the repo. + if not path.is_file() or path.name == ".DS_Store": + continue + if path.relative_to(LIBRARIES).as_posix() not in seen: + problems.append(f"{repo_path(path)}: is not listed in {repo_path(manifest_path)}") + + # Check each listed entry: the schema first, then the rules that need other files or the SPDX list. + names = {} + for group, path in entries: + entry = load_json(path, problems) + if entry is INVALID: + continue + problems += schema_problems(path, entry, schema, DEFINITION_FOR_GROUP[group]) + if not isinstance(entry, dict): + continue + + name = entry.get("name") + if isinstance(name, str) and name.strip(): + key = name.casefold() + if key in names: + problems.append( + f"{repo_path(path)}: name: '{name}' is already used by {repo_path(names[key])}. Names must be unique, ignoring case." + ) + else: + names[key] = path + + expression = entry.get("license") + if isinstance(expression, str) and expression.strip(): + for line in license_problems(expression, licensing, licenses, exceptions): + problems.append(f"{repo_path(path)}: license: {line}") + + # metadata.json must describe exactly the feature keys the schema defines. + metadata_path = LIBRARIES / "metadata.json" + metadata = load_json(metadata_path, problems) + if metadata is not INVALID: + metadata_problems = schema_problems(metadata_path, metadata, schema, "metadata") + problems += metadata_problems + if not metadata_problems: + feature_keys = list(schema["$defs"]["client"]["properties"]["features"]["properties"]) + where = f"{repo_path(metadata_path)}: featureDescriptions" + described = [item["key"] for item in metadata["featureDescriptions"]] + for key in sorted({key for key in described if described.count(key) > 1}): + problems.append(f"{where}: '{key}' is listed more than once") + for key in feature_keys: + if key not in described: + problems.append(f"{where}: has no entry for the feature key `{key}`") + for key in sorted(set(described) - set(feature_keys)): + problems.append(f"{where}: '{key}' is not a feature key in {repo_path(schema_path)}") + + if problems: + print_problems(problems) + return 1 + print(f"{repo_path(LIBRARIES)}: {len(entries)} entries pass {repo_path(schema_path)} and SPDX License List {SPDX_VERSION} checks.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/config.toml b/config.toml index 709358a3..68709386 100644 --- a/config.toml +++ b/config.toml @@ -31,7 +31,6 @@ command_json_path = "../build-command-json/" command_bloom_json_path = "../build-bloom-command-json/" command_json_json_path = "../build-json-command-json/" command_search_json_path = "../build-search-command-json/" -client_json_path = "../build-clients/" doc_topic_path = "../build-topics/" review_list = [ diff --git a/content/_index.md b/content/_index.md index 015f93b0..2630dcc7 100644 --- a/content/_index.md +++ b/content/_index.md @@ -38,7 +38,7 @@ title = "Clients" description = "Official Valkey client libraries include support for:" features = ["Python", "Java", "Go", "Node.js", "PHP", "C#", "Ruby"] button_text = "Learn More" -button_url = "/clients" +button_url = "/clients/" [[extra.download_ctas]] text= "Get Valkey" diff --git a/content/clients/_index.md b/content/clients/_index.md index 8f840967..59dd2f25 100644 --- a/content/clients/_index.md +++ b/content/clients/_index.md @@ -1,64 +1,8 @@ +++ -title = "Client Libraries" -template = "client-list.html" -[extra] -recommended_clients_paths = [ - "/python/valkey-GLIDE.json", - "/python/valkey-py.json", - "/node.js/valkey-GLIDE.json", - "/node.js/iovalkey.json", - "/java/valkey-GLIDE.json", - "/java/valkey-java.json", - "/java/redisson.json", - "/go/valkey-GLIDE.json", - "/go/valkey-go.json", - "/php/valkey-GLIDE.json", - "/php/phpredis.json", - "/php/predis.json", - "/swift/valkey-swift.json", - "/csharp/valkey-GLIDE.json", - "/ruby/valkey-GLIDE.json" - ] - -client_fields =[ - "name", - "language", - "read_from_replica", - "smart_backoff_to_prevent_connection_storm", - "pubsub_state_restoration", - "cluster_scan", - "latency_based_read_from_replica", - "AZ_based_read_from_replica", - "client_side_caching", - "client_capa_redirect", - "persistent_connection_pool" - ] +title = "Clients" +template = "libraries.html" +render = true +[extra] +kind = "clients" +++ - -This page offers an overview of recommended Valkey clients for various programming languages. A table of advanced features supported by the respective clients is provided, highlighting the unique advantages of one client over another. - -This page includes only clients which are regularly tested and recommended. However, it's important to note that other clients that support Redis OSS version 7.2 are compatible with Valkey 7.2 and above. To add your client to the list, please refer to [this link.](https://github.com/valkey-io/valkey-doc/blob/main/clients/README.md) - - - -Advanced Features Overview ------ - -1. **Read from Replica** - The ability to read data from a replica node, which can be useful for load balancing and reducing the load on the primary node. This feature is particularly important in read-heavy applications. - -2. **Smart Backoff to Prevent Connection Storm** - A strategy used to prevent connection storms by progressively updating the wait time between retries when attempting to reconnect to a Valkey server. This helps to reduce the load on the server during topology updates, periods of high demand or network instability. - -3. **PubSub State Restoration** - The ability to restore the state of Pub/Sub (publish/subscribe) channels after a client reconnects. This feature ensures that clients can continue receiving messages after disconnections or topology updates such as adding or removing shards, for both legacy Pub/Sub and sharded Pub/Sub. The client will automatically resubscribe the connections to the new node. The advantage is that the application code is simplified, and doesn’t have to take care of resubscribing to new nodes during reconnects. - -4. **Cluster Scan** - This feature ensures that the user experience and guarantees for scanning a cluster are identical to those for scanning a single node. The SCAN function operates as a cursor-based iterator. With each command, the server provides an updated cursor, which must be used as the cursor argument in subsequent calls. A complete iteration with SCAN retrieves all elements present in the collection from start to finish. If an element exists in the collection at the beginning and remains until the end of the iteration, SCAN will return it. Conversely, any element removed before the iteration begins and not re-added during the process will not be returned by SCAN. A client supporting this feature ensures the scan iterator remains valid even during failovers or cluster scaling (in or out) during the SCAN operation. - -5. **Latency-Based Read from Replica** - This feature enables reading data from the nearest replica, i.e., the replica that offers the best latency. It supports complex deployments where replicas are distributed across various distances, including different geographical regions, to ensure data is read from the closest replica, thereby minimizing latency. - -6. **AZ-Based Read from Replica** - This feature enables reading data from replicas within the same Availability Zone (AZ). When running Valkey in a cloud environment across multiple AZs, it is preferable to keep traffic localized within an AZ to reduce costs and latency. By reading from replicas in the same AZ as the client, you can optimize performance and minimize cross-AZ data transfer charges. For more detailed information about this feature and its implementation, please refer to [this link.](https://github.com/valkey-io/valkey/pull/700) - -7. **Client Side Caching** - Valkey client-side caching is a feature that allows clients to cache the results of Valkey queries on the client-side, reducing the need for frequent communication with the Valkey server. This can significantly improve application performance by lowering latency, reducing the network usage and cost and reducing the load on the Valkey server. - -8. **`CLIENT CAPA redirect` Support** - The `CLIENT CAPA redirect` feature was introduced in Valkey 8 to facilitate seamless upgrades without causing errors in standalone mode. When enabled, this feature allows the replica to redirect data access commands (both read and write operations) to the primary instance. This ensures uninterrupted service during the upgrade process. For more detailed information about this feature, please refer to [this link.](https://github.com/valkey-io/valkey/pull/325) - -9. **Persistent Connection Pool** - This feature enables the Valkey client to maintain a pool of persistent connections to the Valkey server, improving performance and reducing overhead. Instead of establishing a new connection for each request, the client can reuse existing connections from the pool, minimizing the time and resources required for connection setup. diff --git a/content/docs/index.md b/content/docs/index.md index 96632ddf..2086a7b7 100644 --- a/content/docs/index.md +++ b/content/docs/index.md @@ -9,6 +9,7 @@ body_class= "blocks-page" * [Command Reference](/commands/) A categorized listing of all Valkey commands * [Documentation by topic](/topics/) In-depth documentation covering a wide variety of operational and usage subjects -* [Client Libraries](/clients/) An overview of recommended Valkey clients and their features. +* [Clients](/clients/) Recommended Valkey client libraries for every major language. +* [Integrations](/integrations/) Tools, frameworks, and AI integrations that work with Valkey. * [Valkey Admin](http://valkey-admin.valkey.io/) An intuitive visual interface to monitor, manage, and interact with your Valkey deployments. diff --git a/content/integrations/_index.md b/content/integrations/_index.md new file mode 100644 index 00000000..fa722e51 --- /dev/null +++ b/content/integrations/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Integrations" +template = "libraries.html" +render = true + +[extra] +kind = "integrations" ++++ diff --git a/sass/_catalog.scss b/sass/_catalog.scss new file mode 100644 index 00000000..b18cedaa --- /dev/null +++ b/sass/_catalog.scss @@ -0,0 +1,980 @@ +// +// Clients + Integrations catalog pages +// +// Shared styles for the catalog served at /clients/ and /integrations/. Both +// use the same compact header, toolbar, filter-pill row, and card grid; the +// only visual differences are the facet (languages vs. tags) and the +// advanced-feature list/legend on the clients view. Reuses the site's +// colors where one matches exactly, its typography mixins, and `respond-min`. +// +// The site sets `html { font-size: 10px }`, so in this file 1rem = 10px. +// +// This file is part of the sitewide stylesheet, so every rule is scoped under +// `.catalog` or uses a `catalog-` class name. Generic names such as `.toolbar`, +// `.chip` or `.copy-btn` must not style or hide elements on other pages. +// +// Progressive enhancement: without JS, all cards show and JS-only controls are +// hidden. A synchronous head snippet adds `js` to before first paint; +// rules gated behind `.js` then reveal the JS-only controls. +// + +// --------------------------------------------------------------------------- +// Catalog-local design tokens +// --------------------------------------------------------------------------- + +$cat-bg: $background-darker; // page ground +$cat-surface: $white; // cards, controls +$cat-fg: #1a2026; // wordmark ink +$cat-muted: $text-light; // secondary text +$cat-border: #e2e8f0; // hairlines +$cat-accent: #6983ff; // valkey mark periwinkle + +$cat-accent-soft: rgba($cat-accent, 0.14); +$cat-fg-soft: rgba($cat-fg, 0.06); + +$cat-radius: 10px; +$cat-radius-lg: 16px; + +// --------------------------------------------------------------------------- +// Layout wrapper +// --------------------------------------------------------------------------- + +.catalog { + max-width: var(--max-width); + margin: 0 auto; + padding: 10px 20px 0; + @include sans-serif; + color: $cat-fg; + background: $cat-surface; + line-height: 1.55; + overflow-x: hidden; + + a, + a:visited { + color: $cat-fg; + } +} + +// --------------------------------------------------------------------------- +// Compact header +// --------------------------------------------------------------------------- +// +// A small heading, one lede line, and a link to the other catalog. Keeps the +// top of the page tight so cards reach the fold. + +.catalog-header { + margin: 2.4rem 0; + + .catalog-title { + margin: 0 0 0.6rem; + font-size: 3rem; // compact vs. the site hero + font-weight: 700; + letter-spacing: -0.01em; + line-height: 1.2; + color: $cat-fg; + } + + .catalog-lede { + margin: 0; + max-width: 82ch; + font-size: 1.55rem; + line-height: 1.5; + color: $cat-muted; + } + + // Clients and integrations are separate pages, so each points to the other + // for readers browsing the ecosystem rather than looking for one library. + .catalog-crosslink { + margin: 0.6rem 0 0; + max-width: 82ch; + font-size: 1.55rem; + line-height: 1.5; + color: $cat-muted; + + a { + text-decoration: underline; + text-underline-offset: 3px; + } + } +} + +// --------------------------------------------------------------------------- +// Toolbar: search + Valkey Project toggle + sort +// --------------------------------------------------------------------------- +// +// Three columns: the search field stretches; the toggle and sort take their +// intrinsic/fixed width on the right. Collapses to a single column on narrow +// viewports. + +.catalog .toolbar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto 180px; + gap: 1.2rem; + align-items: end; + margin: 0 0 3.2rem; + + @media (max-width: 900px) { + grid-template-columns: minmax(0, 1fr) auto; + } + + @media (max-width: 620px) { + grid-template-columns: minmax(0, 1fr); + } + + .field { + display: flex; + flex-direction: column; + gap: 0.6rem; + + label, + .toggle-label { + font-size: 1.3rem; + color: $cat-muted; + } + } + + .field-toggle { + // Keep the toggle control aligned to the bottom edge of the row. + justify-content: flex-end; + } + + .search-wrap { + position: relative; + } + + .search-icon { + position: absolute; + left: 1.4rem; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + color: $cat-muted; + pointer-events: none; + } + + .search-input { + width: 100%; + min-width: 0; + height: 44px; + padding: 1.1rem 1.4rem 1.1rem 4rem; + border: 1px solid $cat-border; + border-radius: $cat-radius; + background: $cat-surface; + color: $cat-fg; + font: inherit; + font-size: 1.5rem; + outline: none; + + &:focus { + border-color: $cat-accent; + outline: 2px solid $cat-accent-soft; + } + } + + .sort-control { + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 0.9rem; + width: 100%; + height: 44px; + padding: 1.1rem 1.4rem; + border: 1px solid $cat-border; + border-radius: $cat-radius; + background-color: $cat-surface; + color: $cat-fg; + font: inherit; + font-size: 1.5rem; + cursor: pointer; + white-space: nowrap; + transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease; + + &:hover { + border-color: rgba($cat-fg, 0.32); + } + + &:focus-visible { + border-color: $cat-accent; + outline: 2px solid $cat-accent; + outline-offset: 2px; + } + + .sort-control-text { + flex: 1 1 auto; + text-align: left; + } + + .sort-control-icon { + flex: none; + width: 16px; + height: 16px; + color: $cat-muted; + } + + // Descending state — flip the icon to signal reversed order. + &.is-desc .sort-control-icon { + transform: scaleY(-1); + } + } +} + +// --------------------------------------------------------------------------- +// Valkey Project toggle (a switch, distinct from the facet pills) +// --------------------------------------------------------------------------- + +.catalog .fp-toggle { + display: inline-flex; + align-items: center; + gap: 0.9rem; + height: 44px; + padding: 0 1.4rem; + border: 1px solid $cat-border; + border-radius: $cat-radius; + background: $cat-surface; + color: $cat-muted; + font: inherit; + font-size: 1.4rem; + cursor: pointer; + white-space: nowrap; + transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease; + + &:hover { + color: $cat-fg; + border-color: rgba($cat-fg, 0.32); + } + + &:focus-visible { + outline: 2px solid $cat-accent; + outline-offset: 2px; + } + + .fp-toggle-track { + position: relative; + display: inline-block; + width: 34px; + height: 18px; + flex: none; + border-radius: 999px; + background: $cat-border; + transition: background 0.15s ease; + } + + .fp-toggle-thumb { + position: absolute; + top: 2px; + left: 2px; + width: 14px; + height: 14px; + border-radius: 50%; + background: $cat-surface; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); + transition: transform 0.15s ease; + } + + // On state — accent fill, thumb slides right, text inks. + &.is-on, + &[aria-checked="true"] { + color: $cat-fg; + border-color: rgba($cat-accent, 0.55); + background: $cat-accent-soft; + + .fp-toggle-track { + background: $cat-accent; + } + + .fp-toggle-thumb { + transform: translateX(16px); + } + } +} + +// --------------------------------------------------------------------------- +// Filter pills +// --------------------------------------------------------------------------- + +.catalog .filterbar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.8rem 1.2rem; + margin: 0 0 3.2rem; + + &[hidden] { + display: none; + } + + .filterbar-label { + // Force the label onto its own line so the pills always sit *below* it, + // not inline. Without this the pills flow next to the label whenever they + // fit, which made the Language bar (few pills) look different from the + // Category bar (many pills). A full-width flex item guarantees the pill + // row wraps beneath the label on every catalog kind. + flex: 0 0 100%; + font-size: 1.4rem; + font-weight: 600; + letter-spacing: 0.01em; + color: $cat-fg; + margin-right: 0.2rem; + } + + .pills { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; + } + + .filter-pill { + display: inline-flex; + align-items: center; + gap: 0.7rem; + padding: 0.9rem 1.4rem; + min-height: 44px; + border: 1px solid $cat-border; + border-radius: 999px; + background: $cat-surface; + color: $cat-muted; + font: inherit; + font-size: 1.35rem; + cursor: pointer; + + &:hover { + color: $cat-fg; + border-color: mix($cat-fg, $cat-border, 32%); + } + + &:focus-visible { + outline: 2px solid $cat-accent; + outline-offset: 2px; + } + + &[disabled] { + opacity: 0.4; + cursor: default; + pointer-events: none; + } + + // Selected state — fills with ink, not accent. + &[aria-pressed="true"] { + background: $cat-fg; + border-color: $cat-fg; + color: $cat-surface; + font-weight: 600; + } + + .pill-count { + display: inline-block; + min-width: 1.6em; + padding: 0 0.5em; + border-radius: 999px; + background: rgba(0, 0, 0, 0.08); + font-size: 1.2rem; + font-weight: 600; + text-align: center; + } + + &[aria-pressed="true"] .pill-count { + background: rgba(255, 255, 255, 0.25); + color: $cat-surface; + } + } +} + +// --------------------------------------------------------------------------- +// Card grid +// --------------------------------------------------------------------------- + +.catalog-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 2rem; + margin: 0 0 5.6rem; + + @media (max-width: 680px) { + grid-template-columns: minmax(0, 1fr); + } + + .entry-card.is-hidden { + display: none; + } +} + +// Entry card. Each card spans three rows of the catalog grid (head, description, the rest) and +// shares them through subgrid, so every part lines up across a row whatever its text length. +// The card must have exactly these three children. minmax(0, 1fr) stops nowrap install +// commands from widening the card. +.catalog .entry-card { + display: grid; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: subgrid; + grid-row: span 3; + row-gap: 1.2rem; + padding: 2.4rem; + border: 1px solid $cat-border; + border-radius: $cat-radius-lg; + background: $cat-surface; + min-width: 0; + + &:hover { + border-color: rgba($cat-fg, 0.26); + } + + .entry-head { + display: flex; + flex-direction: column; + gap: 1.2rem; + } + + .entry-name { + margin: 0; + font-size: 1.9rem; + font-weight: 600; + color: $cat-fg; + line-height: 1.3; + letter-spacing: -0.005em; + word-break: break-word; + + a { + color: $cat-fg; + + &:hover { + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: $cat-accent; + } + } + } + + .entry-tags { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + gap: 0.6rem; + } + + .entry-desc { + margin: 0; + font-size: 1.45rem; + line-height: 1.55; + color: $cat-muted; + overflow-wrap: anywhere; + + // Marks the span of a description-only search match (JS-applied). Title + // matches are not highlighted since the name heading already makes those + // obvious. + mark.search-hit { + background: $cat-accent-soft; + color: $cat-fg; + border-radius: 3px; + padding: 0 0.15em; + } + } + + // Fills the card's third row, so the license/footer sink to the bottom. + .entry-anchor { + display: flex; + flex-direction: column; + } + + .entry-anchor-top { + display: flex; + flex-direction: column; + gap: 1.2rem; + } + + // Pinned to the bottom so it absorbs extra row height instead of the copy box shifting down. + .entry-anchor-bottom { + display: flex; + flex-direction: column; + gap: 1.2rem; + margin-top: auto; + padding-top: 1.2rem; + } + + .kv { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.4rem 1.2rem; + margin: 0; + font-size: 1.35rem; + color: $cat-fg; + + // The "License" label is secondary; the license name itself is primary and + // should read as dark, high-contrast ink. + dt { + color: $cat-muted; + } + + dd { + margin: 0; + color: $cat-fg; + } + + .num { + @include monospace; + font-variant-numeric: tabular-nums; + } + } + + .entry-foot { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1.2rem; + padding-top: 1.2rem; + border-top: 1px solid $cat-border; + font-size: 1.35rem; + + a { + color: $cat-fg; + border-bottom: 1px solid $cat-border; + + &:hover { + border-bottom-color: $cat-accent; + } + } + } +} + +// --------------------------------------------------------------------------- +// Chips + badges +// --------------------------------------------------------------------------- + +.catalog .chip { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.3rem 0.9rem; + border-radius: 999px; + font-size: 1.2rem; + line-height: 1.4; + white-space: nowrap; +} + +// Language chip (clients) — outlined neutral. +.catalog .chip-language { + background: $cat-surface; + color: $cat-muted; + border: 1px solid $cat-border; +} + +// Small language monogram badge shown inside the language chip. Data-driven +// (color/fg/mono from _data/lang_colors.toml, using MIT-licensed GitHub +// Linguist color data — no brand logos). The inline `style` on the element +// sets background/color per language; this rule only handles size and shape. +.catalog .lang-badge { + display: inline-flex; + align-items: center; + justify-content: center; + flex: none; + // Square by default for 1-2 char monograms (min-width == height), but allow + // the box to grow for 3-char monograms like "PHP" so the glyphs keep + // horizontal color padding instead of butting against the chip's white + // background. + box-sizing: border-box; + min-width: 16px; + height: 16px; + padding: 0 0.3rem; + border-radius: 3px; + font-size: 0.95rem; + font-weight: 700; + line-height: 1; + letter-spacing: -0.02em; +} + +// Tag chip (integrations) — filled ink-soft, no border (the "shelf label"). +.catalog .chip-tag { + background: $cat-fg-soft; + color: $cat-fg; + border: 1px solid transparent; +} + +// Valkey Project chip — accent-tinted, marks first-party entries on both pages. +.catalog .chip-project { + background: $cat-accent-soft; + color: mix(#000, $cat-accent, 22%); + border: 1px solid rgba($cat-accent, 0.4); + font-weight: 600; +} + +// Icon inside the Valkey Project chip (the Valkey logo mark). +.catalog .chip-icon { + flex: none; + width: 11px; + height: 12.5px; + display: block; +} + + +// --------------------------------------------------------------------------- +// Install command + copy button +// --------------------------------------------------------------------------- + +.catalog .entry-card .cmd { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.2rem; + padding: 0.8rem 0.8rem 0.8rem 1.2rem; + border: 1px solid $cat-border; + border-radius: 8px; + background: $cat-bg; + min-width: 0; + + // Non-scrolling wrapper around the command. It stays fixed while the inner + // scrolls, which lets the fade overlay (::after) stay pinned to the + // visible right edge — independent of the copy button's label width + // ("Copy" vs "Copy failed"). + .cmd-scroll { + position: relative; + flex: 1 1 auto; + min-width: 0; + display: block; + } + + // Soft fade at the right edge, signalling that an over-long command + // continues rather than ending at an abrupt hard cutoff. Anchored to the + // non-scrolling wrapper so it never moves with the scrolled text. + // Non-interactive so it never blocks scrolling; hidden from assistive tech. + .cmd-scroll::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + right: 0; + width: 3.2rem; + pointer-events: none; + background: linear-gradient(to right, rgba($cat-bg, 0), $cat-bg); + } + + .install-cmd { + display: block; + overflow-x: auto; + scrollbar-width: thin; + @include monospace; + font-size: 1.25rem; + white-space: nowrap; + // Room on the right so the last glyphs sit under the fade, not flush + // against it. + padding-right: 1.6rem; + } + + .copy-btn { + flex: 0 0 auto; + min-height: 30px; + padding: 0.5rem 0.9rem; + border: 1px solid $cat-border; + border-radius: 6px; + background: $cat-surface; + color: $cat-muted; + font: inherit; + font-size: 1.2rem; + cursor: pointer; + + &:hover { + border-color: $cat-fg; + color: $cat-fg; + } + + &.is-copied { + border-color: $cat-fg; + color: $cat-fg; + } + + &.is-failed { + border-color: $red-200; + color: $red-300; + } + } +} + +// --------------------------------------------------------------------------- +// Supported-feature list (clients only) +// --------------------------------------------------------------------------- + +.catalog .entry-card .disc { + border-top: 1px solid $cat-border; + padding-top: 1.2rem; + font-size: 1.3rem; + + // Collapsed by default (native
); this rule fixes every client + // card's advanced-features block to the same closed-state height so cards + // line up side by side in the grid regardless of how many features a given + // client supports. + summary { + display: flex; + align-items: center; + gap: 0.8rem; + margin: 0; + list-style: none; + font-size: 1.35rem; + font-weight: 400; + color: $cat-muted; + cursor: pointer; + + &::-webkit-details-marker { + display: none; + } + + &::before { + content: ""; + flex: none; + width: 10px; + height: 10px; + border-right: 1.5px solid $cat-muted; + border-bottom: 1.5px solid $cat-muted; + transform: rotate(-45deg); + transition: transform 0.15s ease; + } + + &:focus-visible { + outline: 2px solid $cat-accent; + outline-offset: 2px; + } + } + + &[open] summary::before { + transform: rotate(45deg); + } + + .disc-count { + color: $cat-muted; + } + + .disc-none { + margin: 1.2rem 0 0; + color: $cat-muted; + font-size: 1.3rem; + } + + .feature-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.4rem 1.6rem; + margin: 1.2rem 0 0; + padding: 0; + list-style: none; + font-size: 1.3rem; + + @media (max-width: 620px) { + grid-template-columns: minmax(0, 1fr); + } + } + + .feature-badge { + display: flex; + gap: 0.8rem; + align-items: baseline; + padding: 0; + background: none; + color: $cat-fg; + font-size: 1.3rem; + font-weight: 400; + cursor: help; + + &::before { + content: "\2713"; // ✓ + font-family: "Fira Mono", Consolas, Menlo, Monaco, "Courier New", Courier, monospace; + width: 12px; + flex: none; + color: $cat-fg; + } + } +} + +// --------------------------------------------------------------------------- +// Feature-definition legend (clients only) +// --------------------------------------------------------------------------- + +.catalog .legend { + margin: 0 0 5.6rem; + padding: 2.4rem; + border: 1px solid $cat-border; + border-radius: $cat-radius-lg; + background: $cat-surface; + font-size: 1.3rem; + + h2, + h3 { + margin: 0 0 1.2rem; + font-size: 1.9rem; + font-weight: 600; + letter-spacing: -0.005em; + color: $cat-fg; + } + + dl { + display: grid; + grid-template-columns: 1fr; + gap: 0.4rem 1.6rem; + margin: 0; + + @include respond-min(768px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @include respond-min(1024px) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + + dt { + font-weight: 600; + color: $cat-fg; + } + + dd { + margin: 0 0 0.5rem; + color: $cat-muted; + } +} + +// --------------------------------------------------------------------------- +// Empty state +// --------------------------------------------------------------------------- + +.catalog .empty-state { + grid-column: 1 / -1; + padding: 4.4rem 2.8rem; + text-align: center; + border: 1px dashed $cat-border; + border-radius: $cat-radius-lg; + background: $cat-surface; + + &[hidden] { + display: none; + } + + .empty-msg { + margin: 0 auto 2rem; + max-width: 46ch; + font-size: 1.45rem; + font-weight: 600; + color: $cat-fg; + } + + .empty-recovery { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1.2rem; + margin: 0; + padding: 0; + list-style: none; + + button { + min-height: 36px; + padding: 0.7rem 1.2rem; + border: 1px solid $cat-border; + border-radius: $cat-radius; + background: $cat-surface; + color: $cat-fg; + font: inherit; + font-size: 1.35rem; + cursor: pointer; + + &:hover { + border-color: $cat-fg; + } + } + } +} + +// --------------------------------------------------------------------------- +// Submission CTA +// --------------------------------------------------------------------------- + +.catalog .submission-heading { + margin: 0 0 0.6rem; + font-size: 3rem; // matches .catalog-title + font-weight: 700; + letter-spacing: -0.01em; + line-height: 1.2; + color: $cat-fg; +} + +.catalog .submission-lede { + margin: 0 0 2rem; + max-width: 82ch; + font-size: 1.55rem; + line-height: 1.5; + color: $cat-muted; +} + +.catalog .submission-cta { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 44px; + margin: 0 0 5.6rem; + padding: 1.1rem 1.8rem; + border: 1px solid $cat-accent; + border-radius: $cat-radius; + background: $cat-accent; + color: $cat-surface !important; + font-size: 1.5rem; + font-weight: 500; + letter-spacing: -0.005em; + text-align: center; + + &:hover { + background: mix(#000, $cat-accent, 12%); + border-color: mix(#000, $cat-accent, 12%); + text-decoration: none; + } +} + +.catalog-nojs-note { + margin: 2rem auto; + max-width: var(--max-width); + padding: 0 20px; + color: $cat-muted; + font-size: 1.4rem; +} + +// =========================================================================== +// Progressive-enhancement gating +// =========================================================================== +// +// Without JS: show every card on the page, hide JS-only controls so no broken +// control appears. Every toolbar control needs JS, so the whole toolbar is +// hidden rather than its fields one by one. + +.catalog .toolbar, +.catalog .copy-btn, +.catalog .filterbar, +.catalog .empty-state { + display: none; +} + +// Once `js` is set on (synchronously, before first paint) reveal the +// JS-only controls. All cards remain visible until the controller's first +// apply() classifies them via `.is-hidden`. +.js .catalog { + .toolbar { + display: grid; + } + + .copy-btn { + display: inline-block; + } + + .filterbar:not([hidden]) { + display: flex; + } + + .empty-state:not([hidden]) { + display: block; + } +} + +// After the controller marks the root `catalog-ready`, visibility is driven +// entirely by the `.is-hidden` class the JS toggles on each card. +.js .catalog-ready [data-catalog] .entry-card { + display: grid; +} + +.js .catalog-ready [data-catalog] .entry-card.is-hidden { + display: none; +} diff --git a/sass/_valkey.scss b/sass/_valkey.scss index 5e44681a..7967e0a3 100644 --- a/sass/_valkey.scss +++ b/sass/_valkey.scss @@ -1542,148 +1542,6 @@ html.banner-hidden .banner { } } -.client-list { - h2 { - margin-top: 2rem; - margin-bottom: 1rem; - - } - - - .language-clients { - margin-bottom: 3rem; - - .client-item { - margin-bottom: 2rem; - padding: 1rem; - border: 1px solid #e0e0e0; - border-radius: 4px; - background-color: #fff; - max-width: 1500px; - width: 100%; - - h3 { - margin-top: 0; - color: #222; - } - - ul { - list-style-type: none; - padding-left: 0; - - > li { - position: relative; - padding-left: 1.5em; - - &::before { - content: "\2022"; - position: absolute; - left: 0.5em; - color: #000; - } - - > ul > li { - padding-left: 1.5em; - - &::before { - content: "\25E6"; - position: absolute; - left: 0.5em; - color: #000; - } - } - } - } - - code, pre { - background-color: #f5f5f5; - padding: 0.2rem 0.4rem; - border-radius: 3px; - width:850px - } - } - } -} - -.feature-comparison-table { - margin-top: 3rem; - - .table-container { - padding: 0; - background-color: #fafafa; - border: 1px solid #ddd; - border-radius: 8px; - overflow-x: auto; - - table { - width: 100%; - border-collapse: collapse; - th, td { - padding: 8px 12px; - text-align: center; - border-right: 1px solid #e0e0e0; - border-bottom: 1px solid #e0e0e0; - } - - th:first-child, - td:first-child { - position: sticky; - left: 0; - background-color: #fafafa; - z-index: 2; - } - } - - .table-header { - background-color: #f4f4f4; - font-weight: bold; - - .table-header-cell { - padding: 10px; - color: #555; - position: sticky; - top: 0; - z-index: 1; - background-color: #f4f4f4; - } - - .table-header-cell:first-child { - z-index: 3; - } - } - - .table-row { - &.even-row { - background-color: #f9f9f9; - } - &.odd-row { - background-color: #fff; - } - - .table-cell { - color: #444; - white-space: normal; - word-wrap: break-word; - - &:first-child { - font-weight: bold; - color: #555; - } - } - } - } -} - -.checkmark-true { - color: green; - font-weight: bold; -} - -.checkmark-false { - color: rgb(190, 9, 9); - font-weight: bold; -} - // Hero Section Styles .hero-section { position: relative; diff --git a/sass/css/styles.scss b/sass/css/styles.scss index 41154fc5..755f9691 100755 --- a/sass/css/styles.scss +++ b/sass/css/styles.scss @@ -8,6 +8,7 @@ @import '../pygments'; @import '../valkey'; @import '../email-form'; +@import '../catalog'; @import '../community'; /* Styling for Markdown Tables */ diff --git a/static/assets/js/libraries.js b/static/assets/js/libraries.js new file mode 100644 index 00000000..039780f9 --- /dev/null +++ b/static/assets/js/libraries.js @@ -0,0 +1,692 @@ +/* + * libraries.js — Libraries catalog page (/clients/ and /integrations/). + * + * Each page renders one catalog. Two layers: + * 1. A pure-logic core (no DOM/clipboard/timers) that decides which cards + * to show and in what order. + * 2. DOM wiring that reads pre-rendered card attributes, holds interaction + * state, listens to events, and calls the pure functions. + * + * The facet (Language on /clients/, Category on /integrations/), the Valkey + * toggle, the search text, and the reversible sort all compose. + * + * Card descriptor: { el, facetValues:string[], firstParty:boolean, search, + * nameLower }. + * State: { facet:Set, firstPartyOnly, search, sortDir:"asc"|"desc" }. + */ + +(function (root, factory) { + var api = factory(); + if (typeof root !== "undefined") { + root.LibrariesCatalog = api; + } + if (typeof module !== "undefined" && module.exports) { + module.exports = api; + } +})(typeof globalThis !== "undefined" ? globalThis : this, function () { + "use strict"; + + function toSet(values) { + if (values instanceof Set) return values; + var s = new Set(); + if (Array.isArray(values)) { + for (var i = 0; i < values.length; i++) s.add(values[i]); + } + return s; + } + + function cardFacetValues(card) { + return Array.isArray(card && card.facetValues) ? card.facetValues : []; + } + + // The selected facet values. + function activeFacet(state) { + return toSet(state && state.facet); + } + + // A card matches if ANY of its facet values is selected (OR). A client has + // one value (its language); an integration has one per tag. An empty + // selection matches every card. + function matchesFacet(card, state) { + var sel = activeFacet(state); + if (sel.size === 0) return true; + var values = cardFacetValues(card); + for (var i = 0; i < values.length; i++) { + if (sel.has(values[i])) return true; + } + return false; + } + + function matchesFirstParty(card, state) { + return !state.firstPartyOnly || card.firstParty === true; + } + + function matchesSearch(card, search) { + if (!search) return true; + var q = String(search).toLowerCase(); + var hay = (card.search || "").toLowerCase(); + return hay.indexOf(q) !== -1; + } + + // Whether the search term matches the card's title (name). Used to rank + // title matches ahead of description-only matches while a search is active. + function matchesTitle(card, search) { + if (!search) return false; + var q = String(search).toLowerCase(); + var name = (card.nameLower || "").toLowerCase(); + return name.indexOf(q) !== -1; + } + + // A card is visible iff it matches the facet, the first-party toggle, and + // the search text. Preserves input order so a later stable sort can rely on + // it. + function computeVisible(cards, state) { + var list = Array.isArray(cards) ? cards : []; + var st = state || {}; + var search = st.search || ""; + return list.filter(function (c) { + return ( + c && + matchesFacet(c, st) && + matchesFirstParty(c, st) && + matchesSearch(c, search) + ); + }); + } + + // Direction-aware stable sort by nameLower. `direction` is "asc" (default) or + // "desc"; ties keep original input order in both directions (stable). When + // `search` is non-empty, title matches are grouped before description-only + // matches; within each group the name ordering still applies. + function sortVisible(visible, direction, search) { + var list = Array.isArray(visible) ? visible.slice() : []; + var desc = direction === "desc"; + var q = search || ""; + var decorated = list.map(function (card, index) { + return { card: card, index: index }; + }); + decorated.sort(function (a, b) { + if (q) { + var aTitle = matchesTitle(a.card, q) ? 0 : 1; + var bTitle = matchesTitle(b.card, q) ? 0 : 1; + if (aTitle !== bTitle) return aTitle - bTitle; + } + var na = ((a.card && a.card.nameLower) || "").toLowerCase(); + var nb = ((b.card && b.card.nameLower) || "").toLowerCase(); + if (na < nb) return desc ? 1 : -1; + if (na > nb) return desc ? -1 : 1; + return a.index - b.index; + }); + return decorated.map(function (d) { + return d.card; + }); + } + + // Live count for a single facet-value pill: cards satisfying the current + // search and first-party toggle whose facet values include `value`. Other + // facet selections are ignored so the count reflects "how many would match + // if this pill were active". The "All" pill (null/"__all__") counts every + // searched, toggle-eligible card. + function pillCount(cards, value, state) { + var list = Array.isArray(cards) ? cards : []; + var st = state || {}; + var search = st.search || ""; + var count = 0; + for (var i = 0; i < list.length; i++) { + var c = list[i]; + if (!c) continue; + if (!matchesSearch(c, search)) continue; + if (!matchesFirstParty(c, st)) continue; + var ok; + if (value === null || value === undefined || value === "__all__") { + ok = true; + } else { + ok = cardFacetValues(c).indexOf(value) !== -1; + } + if (ok) count += 1; + } + return count; + } + + // Recovery suggestions for an empty result. + function baseState(state, overrides) { + var st = state || {}; + var patch = { + facet: toSet(st.facet), + firstPartyOnly: !!st.firstPartyOnly, + search: st.search || "", + sortDir: st.sortDir || "asc", + }; + for (var k in overrides) { + if (Object.prototype.hasOwnProperty.call(overrides, k)) patch[k] = overrides[k]; + } + return patch; + } + + function clearedFilterState(state) { + return baseState(state, { + facet: new Set(), + firstPartyOnly: false, + }); + } + + function clearedSearchState(state) { + return baseState(state, { search: "" }); + } + + function clearedAllState(state) { + return baseState(state, { + facet: new Set(), + firstPartyOnly: false, + search: "", + }); + } + + function recovery(cards, state) { + var list = Array.isArray(cards) ? cards : []; + var st = state || {}; + var suggestions = []; + + function nonEmptyUnder(patch) { + return computeVisible(list, patch).length > 0; + } + + var hasFilter = activeFacet(st).size > 0 || !!st.firstPartyOnly; + var filterPatch = clearedFilterState(st); + if (hasFilter && nonEmptyUnder(filterPatch)) { + suggestions.push({ + action: "clear-filters", + label: "Clear filters", + patch: filterPatch, + }); + } + + var searchPatch = clearedSearchState(st); + if (st.search && nonEmptyUnder(searchPatch)) { + suggestions.push({ + action: "clear-search", + label: "Clear search", + patch: searchPatch, + }); + } + + if (suggestions.length === 0) { + var allPatch = clearedAllState(st); + if (nonEmptyUnder(allPatch)) { + suggestions.push({ + action: "clear-all", + label: "Clear search and filters", + patch: allPatch, + }); + } + } + + return suggestions.slice(0, 5); + } + + return { + computeVisible: computeVisible, + sortVisible: sortVisible, + pillCount: pillCount, + recovery: recovery, + matchesTitle: matchesTitle, + }; +}); + +/* + * DOM wiring layer, built on the pure-logic core above. Sets the `js` flag, + * reads pre-rendered card DOM into descriptors, holds interaction state, runs + * apply() (computeVisible + sortVisible to toggle a hidden class and reorder + * nodes), refreshes pill counts, reveals/hides the empty state, and wires the + * facet pills, Valkey toggle, debounced search, sort, and copy. + * + * Exposed as LibrariesCatalog.initDom(root); auto-initializes on + * DOMContentLoaded when a libraries catalog page is present. + */ + +(function (root) { + "use strict"; + + var api = root && root.LibrariesCatalog; + if (!api) return; + + var HIDDEN_CLASS = "is-hidden"; + var SEARCH_DEBOUNCE_MS = 120; + var ANNOUNCE_DELAY_MS = 500; + var COPY_RESET_MS = 1500; + var FACET_ALL = "__all__"; + + function q(el, sel) { + return el.querySelector(sel); + } + function qa(el, sel) { + return Array.prototype.slice.call(el.querySelectorAll(sel)); + } + + function escapeHtml(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function escapeRegExp(str) { + return String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + + // Wraps the first match of `term` (case-insensitive) inside `text` with a + // , HTML-escaping the rest. Returns escaped plain text when there is + // no term or no match. + function highlightMatch(text, term) { + var safeText = escapeHtml(text); + if (!term) return safeText; + var re = new RegExp(escapeRegExp(term), "i"); + var match = re.exec(text); + if (!match) return safeText; + var start = match.index; + var end = start + match[0].length; + return ( + escapeHtml(text.slice(0, start)) + + "" + + escapeHtml(text.slice(start, end)) + + "" + + escapeHtml(text.slice(end)) + ); + } + + function setScriptingFlag(doc) { + if (doc && doc.documentElement) { + doc.documentElement.classList.add("js"); + } + } + + // Read pre-rendered data-* attributes into the plain descriptor the pure + // logic operates on. No card construction. `facetName` is the facet bar's + // data-facet ("language" or "tags"); the card attribute of the same name + // holds its values, separated by "|". + function readCardDescriptor(cardEl, facetName) { + var facetAttr = facetName ? cardEl.getAttribute("data-" + facetName) || "" : ""; + return { + el: cardEl, + facetValues: facetAttr.split("|").filter(function (v) { + return v !== ""; + }), + firstParty: cardEl.getAttribute("data-first-party") === "true", + search: cardEl.getAttribute("data-search") || "", + desc: cardEl.getAttribute("data-desc") || "", + nameLower: cardEl.getAttribute("data-name-lower") || "", + descEl: q(cardEl, ".entry-desc"), + descOriginal: null, + }; + } + + function Controller(root) { + this.root = root; + this.doc = root.ownerDocument || root.document || document; + + this.catalogRoot = q(root, "[data-catalog-root]") || root; + this.grid = q(root, "[data-catalog]"); + this.searchInput = q(root, "[data-search-input]"); + this.sortControl = q(root, "[data-sort-control]"); + this.emptyState = q(root, "[data-empty-state]"); + this.toggleBtn = q(root, "[data-first-party-toggle]"); + this.facetBar = q(root, ".filterbar[data-facet]"); + this.resultCount = q(root, "[data-result-count]"); + + var facetName = this.facetBar ? this.facetBar.getAttribute("data-facet") : ""; + this.cardEls = this.grid ? qa(this.grid, ".entry-card") : []; + this.cards = this.cardEls.map(function (el) { + return readCardDescriptor(el, facetName); + }); + + this.state = { + facet: new Set(), + firstPartyOnly: false, + search: "", + sortDir: "asc", + }; + + this._searchTimer = null; + this._announceTimer = null; + this._copyTimers = []; + } + + // The core render pass: compute visibility + order, toggle the hidden class + // on every card, reorder visible nodes in place, refresh pill counts, + // reveal/hide the empty state. + Controller.prototype.apply = function () { + var visible = api.computeVisible(this.cards, this.state); + var ordered = api.sortVisible(visible, this.state.sortDir, this.state.search); + + var visibleSet = new Set( + visible.map(function (c) { + return c.el; + }) + ); + + for (var i = 0; i < this.cards.length; i++) { + var el = this.cards[i].el; + if (visibleSet.has(el)) { + el.classList.remove(HIDDEN_CLASS); + el.removeAttribute("hidden"); + } else { + el.classList.add(HIDDEN_CLASS); + } + } + + this.applyDescriptionHighlights(visibleSet); + + if (this.grid) { + for (var j = 0; j < ordered.length; j++) { + this.grid.appendChild(ordered[j].el); + } + } + + this.refreshPillCounts(); + this.updateEmptyState(ordered.length); + this.visibleCount = ordered.length; + }; + + // Highlights the search term in a card's description, but only for cards + // whose match came from the description rather than the title — title + // matches are already obvious from the (unhighlighted) heading. Restores + // the original description text for cards that are hidden or no longer + // matched by description. + Controller.prototype.applyDescriptionHighlights = function (visibleSet) { + var search = this.state.search || ""; + for (var i = 0; i < this.cards.length; i++) { + var card = this.cards[i]; + if (!card.descEl) continue; + if (card.descOriginal === null) card.descOriginal = card.descEl.textContent; + + var shouldHighlight = + search && + visibleSet.has(card.el) && + !api.matchesTitle(card, search) && + card.desc.indexOf(search) !== -1; + + if (shouldHighlight) { + card.descEl.innerHTML = highlightMatch(card.descOriginal, search); + } else { + card.descEl.textContent = card.descOriginal; + } + } + }; + + Controller.prototype.refreshPillCounts = function () { + var bar = this.facetBar; + if (!bar) return; + var self = this; + qa(bar, ".filter-pill").forEach(function (pill) { + var slot = q(pill, "[data-count]"); + if (!slot) return; + var value = pill.getAttribute("data-value"); + var facetValue = value === FACET_ALL ? null : value; + slot.textContent = String(api.pillCount(self.cards, facetValue, self.state)); + }); + }; + + Controller.prototype.updateEmptyState = function (visibleCount) { + if (!this.emptyState) return; + if (visibleCount > 0) { + this.emptyState.hidden = true; + return; + } + this.emptyState.hidden = false; + try { + this.renderRecovery(); + } catch (e) { + if (root.console && root.console.warn) { + root.console.warn("libraries: recovery rendering failed", e); + } + } + }; + + // Called from the search, pill, toggle and recovery handlers rather than + // apply(), so loading and sorting stay silent. The delay turns typing into + // one announcement instead of one per pause. + Controller.prototype.announceCount = function () { + if (!this.resultCount) return; + var self = this; + root.clearTimeout(this._announceTimer); + this._announceTimer = root.setTimeout(function () { + var n = self.visibleCount; + var region = self.resultCount; + var noun = n === 1 ? region.getAttribute("data-noun-one") : region.getAttribute("data-noun"); + region.textContent = n === 0 ? "No " + noun + " match" : n + " " + noun + " shown"; + }, ANNOUNCE_DELAY_MS); + }; + + Controller.prototype.renderRecovery = function () { + var list = q(this.emptyState, "[data-recovery]"); + if (!list) return; + var suggestions = api.recovery(this.cards, this.state) || []; + var doc = this.doc; + + while (list.firstChild) list.removeChild(list.firstChild); + + suggestions.slice(0, 5).forEach(function (s) { + var li = doc.createElement("li"); + var btn = doc.createElement("button"); + btn.type = "button"; + btn.setAttribute("data-recover", s.action); + btn.textContent = s.label; + li.appendChild(btn); + list.appendChild(li); + }); + }; + + Controller.prototype.applyRecovery = function (suggestion) { + var patch = suggestion.patch || {}; + if (patch.facet) { + this.state.facet = patch.facet instanceof Set ? patch.facet : new Set(); + } + this.state.firstPartyOnly = !!patch.firstPartyOnly; + this.state.search = patch.search || ""; + + if (this.searchInput) this.searchInput.value = this.state.search; + this.syncPillPressedState(); + this.syncToggleState(); + this.apply(); + this.announceCount(); + }; + + // --- Facet pills -------------------------------------------------------- + + Controller.prototype.onPillClick = function (pill) { + var value = pill.getAttribute("data-value"); + if (value === FACET_ALL) { + this.state.facet = new Set(); + } else { + this.toggleInSet(this.state.facet, value); + } + this.syncPillPressedState(); + this.apply(); + this.announceCount(); + }; + + Controller.prototype.toggleInSet = function (set, value) { + if (set.has(value)) set.delete(value); + else set.add(value); + }; + + // Reflect selection state in aria-pressed on the pills. The "All" pill is + // pressed exactly when no value is selected. + Controller.prototype.syncPillPressedState = function () { + var bar = this.facetBar; + if (!bar) return; + var sel = this.state.facet; + qa(bar, ".filter-pill").forEach(function (pill) { + var value = pill.getAttribute("data-value"); + var pressed = value === FACET_ALL ? sel.size === 0 : sel.has(value); + pill.setAttribute("aria-pressed", pressed ? "true" : "false"); + }); + }; + + // --- Valkey (first-party) toggle --------------------------------------- + + Controller.prototype.onToggle = function () { + this.state.firstPartyOnly = !this.state.firstPartyOnly; + this.syncToggleState(); + this.apply(); + this.announceCount(); + }; + + Controller.prototype.syncToggleState = function () { + if (!this.toggleBtn) return; + var on = this.state.firstPartyOnly; + this.toggleBtn.setAttribute("aria-checked", on ? "true" : "false"); + this.toggleBtn.classList.toggle("is-on", on); + }; + + // --- Reversible sort ---------------------------------------------------- + + Controller.prototype.onSortToggle = function () { + this.state.sortDir = this.state.sortDir === "asc" ? "desc" : "asc"; + this.syncSortState(); + this.apply(); + }; + + Controller.prototype.syncSortState = function () { + if (!this.sortControl) return; + var desc = this.state.sortDir === "desc"; + var textEl = q(this.sortControl, "[data-sort-text]"); + if (textEl) textEl.textContent = desc ? "Name Z–A" : "Name A–Z"; + this.sortControl.classList.toggle("is-desc", desc); + }; + + // --- Copy button -------------------------------------------------------- + + Controller.prototype.onCopyClick = function (btn) { + var command = btn.getAttribute("data-copy") || ""; + var self = this; + function indicate(cls, text) { + btn.classList.remove("is-copied", "is-failed"); + btn.classList.add(cls); + if (!btn.getAttribute("data-label")) { + btn.setAttribute("data-label", btn.textContent); + } + btn.textContent = text; + var timer = root.setTimeout(function () { + btn.classList.remove(cls); + btn.textContent = btn.getAttribute("data-label"); + }, COPY_RESET_MS); + self._copyTimers.push(timer); + } + + var clipboard = root.navigator && root.navigator.clipboard; + if (clipboard && typeof clipboard.writeText === "function") { + clipboard.writeText(command).then( + function () { + indicate("is-copied", "Copied"); + }, + function () { + indicate("is-failed", "Copy failed"); + } + ); + } else { + indicate("is-failed", "Copy failed"); + } + }; + + // --- Wiring ------------------------------------------------------------- + + Controller.prototype.bind = function () { + var self = this; + + if (this.facetBar) { + var bar = this.facetBar; + bar.addEventListener("click", function (ev) { + var pill = ev.target.closest ? ev.target.closest(".filter-pill") : null; + if (pill && bar.contains(pill)) self.onPillClick(pill); + }); + } + + if (this.toggleBtn) { + this.toggleBtn.addEventListener("click", function () { + self.onToggle(); + }); + } + + if (this.searchInput) { + this.searchInput.addEventListener("input", function () { + if (self._searchTimer) root.clearTimeout(self._searchTimer); + self._searchTimer = root.setTimeout(function () { + self.state.search = self.searchInput.value.toLowerCase(); + self.apply(); + self.announceCount(); + }, SEARCH_DEBOUNCE_MS); + }); + } + + if (this.sortControl) { + this.sortControl.addEventListener("click", function () { + self.onSortToggle(); + }); + } + + if (this.grid) { + this.grid.addEventListener("click", function (ev) { + var btn = ev.target.closest ? ev.target.closest(".copy-btn") : null; + if (btn && self.grid.contains(btn)) self.onCopyClick(btn); + }); + } + + if (this.emptyState) { + var recoveryList = q(this.emptyState, "[data-recovery]"); + if (recoveryList) { + recoveryList.addEventListener("click", function (ev) { + var btn = ev.target.closest ? ev.target.closest("[data-recover]") : null; + if (!btn || !recoveryList.contains(btn)) return; + var action = btn.getAttribute("data-recover"); + var suggestions = api.recovery(self.cards, self.state) || []; + for (var i = 0; i < suggestions.length; i++) { + if (suggestions[i].action === action) { + self.applyRecovery(suggestions[i]); + return; + } + } + }); + } + } + }; + + Controller.prototype.init = function () { + setScriptingFlag(this.doc); + this.bind(); + this.syncPillPressedState(); + this.syncToggleState(); + this.syncSortState(); + this.apply(); + if (this.catalogRoot && this.catalogRoot.classList) { + this.catalogRoot.classList.add("catalog-ready"); + } + return this; + }; + + function initDom(rootEl) { + var host = rootEl || (root.document ? root.document : null); + if (!host) return null; + var selector = '[data-catalog-root][data-page="libraries"]'; + var hostIsRoot = host.matches && host.matches(selector); + if (!hostIsRoot && host.querySelector && !host.querySelector(selector)) { + return null; + } + var scope = hostIsRoot ? host : host.querySelector(selector) || host; + return new Controller(scope).init(); + } + + api.initDom = initDom; + api.Controller = Controller; + + if (root.document && root.document.addEventListener) { + root.document.addEventListener("DOMContentLoaded", function () { + setScriptingFlag(root.document); + if (root.document.querySelector('[data-catalog-root][data-page="libraries"]')) { + initDom(root.document); + } + }); + } +})(typeof globalThis !== "undefined" ? globalThis : this); diff --git a/templates/client-feature-table.html b/templates/client-feature-table.html deleted file mode 100644 index 1e5744ba..00000000 --- a/templates/client-feature-table.html +++ /dev/null @@ -1,32 +0,0 @@ -{% block main_content %} -
-

Feature Comparison Table

- -
- - - - {% for header in client_fields %} - - {% endfor %} - - - - {%- for path in client_paths %} - {% set json_data = load_data(path = config.extra.client_json_path ~ path, format="json") %} - - {% for field in client_fields %} - - {% endfor %} - - {%- endfor %} - -
{{ header | replace(from="_", to=" ") | title | replace (from= "Az", to= "AZ") }}
- {% if field not in ["name", "language"] %} - {{< boolean_to_checkmark value={json_data[field]} />}} - {% else %} - {{ json_data[field] }} - {% endif %} -
-
-{% endblock main_content %} \ No newline at end of file diff --git a/templates/client-list.html b/templates/client-list.html deleted file mode 100644 index a547f814..00000000 --- a/templates/client-list.html +++ /dev/null @@ -1,77 +0,0 @@ -{% extends "fullwidth.html" %} - -{% block main_content %} - {% set client_path = section.extra.recommended_clients_paths | first %} - {% set first_client = load_data(path = config.extra.client_json_path ~ client_path, format="json", required = false) %} - {% if not first_client %} - Clients page not found. You likely need to build the client page. See "Building additional content" in the README.md file. - {% else %} -
- {% set content_parts = section.content | split(pat="") %} - {{ content_parts[0] | markdown | safe }} -
- -
-

Table of Contents

-
    - {% set languages = [] %} - {% for path in section.extra.recommended_clients_paths %} - {% set language = path | split(pat="/") | nth(n=1) %} - {% if language not in languages %} - {% set_global languages = [...languages, language] %} - {% endif %} - {% endfor %} - {% for language in languages %} -
  • {{< format_language language={language} />}}
  • - {% endfor %} -
  • Feature Comparison Table
  • -
- {% for language in languages %} -
-

{{< format_language language={language} />}}

- {% for path in section.extra.recommended_clients_paths %} - {% set client = load_data(path = config.extra.client_json_path ~ path, format="json") %} - {% if client.language == language %} -
-

{{ client.name }}

-
    -
  • Repo: {{ client.name }}
  • - {% if client.website %} -
  • Website: {{ client.website }}
  • - {% endif %} -
  • - Installation: - {% if client.installation is not string %} -
      - {% for installation_type in client.installation %} -
    • - {{ installation_type.type }}: -
      {{ installation_type.command }}
      -
    • - {% endfor %} -
    - {% else %} - {{ client.installation }} - {% endif %} -
  • -
  • Version: {{ client.version }}
  • -
  • Version Released: {{ client.version_released }}
  • -
  • Description: {{ client.description }}
  • -
  • License: {{ client.license }}
  • -
-
- {% endif %} - {% endfor %} -
- {% endfor %} -
-
- {{ content_parts[1] | markdown | safe }} -
-
- {% set client_paths = section.extra.recommended_clients_paths %} - {% set client_fields = section.extra.client_fields %} - {% include "client-feature-table.html" %} -
- {% endif %} -{% endblock main_content %} diff --git a/templates/default.html b/templates/default.html index a6d930b4..a886903d 100644 --- a/templates/default.html +++ b/templates/default.html @@ -50,7 +50,8 @@ Blog diff --git a/templates/libraries.html b/templates/libraries.html new file mode 100644 index 00000000..e7faebfc --- /dev/null +++ b/templates/libraries.html @@ -0,0 +1,282 @@ +{% extends "default.html" %} + +{# Libraries catalog page. Each URL renders one catalog: #} +{# content/clients/_index.md → kind = "clients" #} +{# content/integrations/_index.md → kind = "integrations" #} +{# Both pages load every entry, then render only their kind. Entries are #} +{# validated elsewhere, so this template trusts them. #} +{# Cards are rendered at build time from _data/libraries/ (no runtime fetch); #} +{# libraries.js filters and sorts them. Without JS the JS-only controls are #} +{# hidden and every card shows. #} + +{% block content %} + + + {%- set kind = section.extra.kind | default(value="clients") -%} + + {# Data assembly #} + {% set manifest = load_data(path="_data/libraries/manifest.json", format="json") %} + {# featureDescriptions is an array so cards and the legend list features in its order. #} + {% set meta = load_data(path="_data/libraries/metadata.json", format="json") %} + + {# Per-language colors from GitHub Linguist (MIT-licensed color data, not #} + {# brand logos). See _data/lang_colors.toml. #} + {% set lang_colors = load_data(path="_data/lang_colors.toml", format="toml") %} + + {# --- Clients ----------------------------------------------------------- #} + {% set_global clients = [] %} + {% for rel in manifest.clients %} + {% set entry = load_data(path="_data/libraries/" ~ rel, format="json") %} + {% set_global clients = [...clients, entry] %} + {% endfor %} + + {# --- Integrations ------------------------------------------------------ #} + {% set_global integrations = [] %} + {% for rel in manifest.integrations %} + {% set entry = load_data(path="_data/libraries/" ~ rel, format="json") %} + {% set_global integrations = [...integrations, entry] %} + {% endfor %} + + {# --- Clients: feature suppression + languages -------------------------- #} + {% set client_count = clients | length %} + {% set_global suppressed = [] %} + {% for feat in meta.featureDescriptions %} + {% set_global tcount = 0 %} + {% for e in clients %} + {% if e.features[feat.key] %} + {% set_global tcount = tcount + 1 %} + {% endif %} + {% endfor %} + {# A feature every client supports, or none does, doesn't tell clients #} + {# apart, so it's left off the cards and out of the legend. #} + {% if client_count > 0 and (tcount == client_count or tcount == 0) %} + {% set_global suppressed = [...suppressed, feat.key] %} + {% endif %} + {% endfor %} + + {% set_global languages = [] %} + {% for e in clients %} + {% set lang = e.language | default(value="") %} + {% if lang != "" and lang not in languages %} + {% set_global languages = [...languages, lang] %} + {% endif %} + {% endfor %} + + {# --- Integrations: distinct tags --------------------------------------- #} + {% set_global tags = [] %} + {% for e in integrations %} + {% if e.tags is defined and e.tags is iterable %} + {% for t in e.tags %} + {% set v = t | trim %} + {% if v != "" and v not in tags %}{% set_global tags = [...tags, v] %}{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + +
+ + {% if kind == "clients" %} +
+

Clients

+

Client libraries and tools used for connecting to Valkey. Anything that speaks the Redis 7.2 wire protocol works with Valkey 7.2 and later, so most existing clients need no changes to migrate.

+ +
+ {% else %} +
+

Integrations

+

Frameworks, job queues, AI libraries, and operations tools that work with Valkey.

+ +
+ {% endif %} + + {# Shared toolbar: search + Valkey toggle + sort #} + {{}} + +
+ + {# The facet bar's data-facet names the card attribute it filters on. #} + {% if kind == "clients" %} +
+ Language +
+ + {% for v in languages %} + {% set_global fp_n = 0 %} + {% for e in clients %}{% if e.language == v %}{% set_global fp_n = fp_n + 1 %}{% endif %}{% endfor %} + + {% endfor %} +
+
+ {% else %} +
+ Category +
+ + {% for v in tags %} + {% set_global fp_n = 0 %} + {% for e in integrations %}{% if e.tags is defined and v in e.tags %}{% set_global fp_n = fp_n + 1 %}{% endif %}{% endfor %} + + {% endfor %} +
+
+ {% endif %} + +
+ + {% if kind == "clients" %} + {% for entry in clients %} + {% set is_first_party = entry.repository is starting_with(pat="https://github.com/valkey-io/") %} + {% set lang = entry.language | default(value="") %} + {% set search_text = entry.name ~ " " ~ entry.description ~ " " ~ lang %} + {% set lang_slug = lang | lower | replace(from='#', to='-sharp') | replace(from='.', to='-') | replace(from=' ', to='-') %} +
+
+

+ {% if entry.documentation %}{{ entry.name }}{% else %}{{ entry.name }}{% endif %} +

+ +
+ +

{{ entry.description }}

+ + {# Copy box + features flow after the description; only the license/footer #} + {# is pinned to the bottom to absorb extra row height. #} +
+
+ {% if entry.installCommand %} +
+ {{ entry.installCommand }} + +
+ {% endif %} + + {% set_global disc_count = 0 %} + {% for feat in meta.featureDescriptions %} + {% if entry.features[feat.key] and feat.key not in suppressed %} + {% set_global disc_count = disc_count + 1 %} + {% endif %} + {% endfor %} +
+ Advanced features supported ({{ disc_count }}) + {% if disc_count > 0 %} +
    + {% for feat in meta.featureDescriptions %} + {% if entry.features[feat.key] and feat.key not in suppressed %} +
  • {{ feat.label }}
  • + {% endif %} + {% endfor %} +
+ {% else %} +

None reported.

+ {% endif %} +
+
+ +
+ {% if entry.license %}
License
{{ entry.license }}
{% endif %} + +
+ {% if entry.repository and (entry.repository is starting_with(pat="http://") or entry.repository is starting_with(pat="https://")) %}Repository{% endif %} + {% if entry.documentation %}Documentation{% endif %} +
+
+
+
+ {% endfor %} + + {% else %} + {% for entry in integrations %} + {% set is_first_party = entry.repository is starting_with(pat="https://github.com/valkey-io/") %} + {% set_global ec_tags = [] %} + {% if entry.tags is defined and entry.tags is iterable %} + {% for t in entry.tags %} + {% set v = t | trim %} + {% if v != "" and v not in ec_tags %}{% set_global ec_tags = [...ec_tags, v] %}{% endif %} + {% endfor %} + {% endif %} + {% set tagstr = ec_tags | join(sep=" ") %} + {% set search_text = entry.name ~ " " ~ entry.description ~ " " ~ tagstr %} +
+
+

+ {% if entry.documentation %}{{ entry.name }}{% else %}{{ entry.name }}{% endif %} +

+ +
+ +

{{ entry.description }}

+ + {# Copy box flows after the description; only the license/footer is #} + {# pinned to the bottom to absorb extra row height. #} +
+
+ {% if entry.installCommand %} +
+ {{ entry.installCommand }} + +
+ {% endif %} +
+ +
+ {% if entry.license %}
License
{{ entry.license }}
{% endif %} + +
+ {% if entry.repository and (entry.repository is starting_with(pat="http://") or entry.repository is starting_with(pat="https://")) %}Repository{% endif %} + {% if entry.documentation %}Documentation{% endif %} +
+
+
+
+ {% endfor %} + {% endif %} + + {{}} +
+ + {% if kind == "clients" %} +
+

Feature definitions

+
+ {% for feat in meta.featureDescriptions %} + {% if feat.key not in suppressed %} +
+
{{ feat.label }}
+
{{ feat.definition }}
+
+ {% endif %} + {% endfor %} +
+
+ {% endif %} + + {{}} + + +{% endblock content %} diff --git a/templates/macros/catalog.html b/templates/macros/catalog.html new file mode 100644 index 00000000..7d1dd933 --- /dev/null +++ b/templates/macros/catalog.html @@ -0,0 +1,64 @@ +{# Shared catalog chrome for the Libraries page (templates/libraries.html), #} +{# served at both /clients/ and /integrations/: the toolbar, empty-state, and #} +{# submission-CTA / noscript footer. Kind-specific parts stay in libraries.html. #} + +{# Toolbar: search box + Valkey toggle + sort control. #} +{% component catalog.toolbar(search_placeholder) %} +
+ +
+ + +
+
+ + +
+
+{% endcomponent catalog.toolbar %} + +{# Empty state shown by JS when no card matches. #} +{% component catalog.empty_state(noun) %} + +{% endcomponent catalog.empty_state %} + +{# Contribute heading + submission CTA + noscript note. #} +{% component catalog.footer_cta(cta_text, noscript_noun) %} +

Add your client, tool or integration

+

Building something on Valkey - a client, a module, a tool, or an AI integration? We'd love to see it here alongside the rest. Everything on this page lives in valkey-io repo, so adding yours is just a pull request away.

+ {{ cta_text }} +
+ + +{% endcomponent catalog.footer_cta %} diff --git a/templates/macros/docs.html b/templates/macros/docs.html index c833058c..672d003c 100644 --- a/templates/macros/docs.html +++ b/templates/macros/docs.html @@ -60,21 +60,3 @@ | safe }} {% endcomponent fix_links_docs %} - -{% component boolean_to_checkmark(value) %} - {% if value == true %} - ✓ - {% else %} - ✗ - {% endif %} -{% endcomponent boolean_to_checkmark %} - -{% component format_language(language) %} - {% if language == "php" %} - {{ language | upper }} - {% elif language == "csharp" %} - C# - {% else %} - {{ language | title }} - {% endif %} -{% endcomponent format_language %}