From 6fa4575fb1db7d01ee506364d82f12935c8bad3b Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk Date: Wed, 24 Jun 2026 17:13:24 +0200 Subject: [PATCH 01/14] Adopt changes requested by @phunt --- .github/workflows/website.yaml | 9 +++-- NOTICE.txt | 1 - conf/zoo_sample.cfg | 2 +- pom.xml | 3 -- zookeeper-client/zookeeper-client-c/README | 8 +++-- .../zkui/src/zkui/templates/tree.mako | 2 +- .../zookeeper-contrib-monitoring/README | 2 +- zookeeper-recipes/README.txt | 5 +-- .../zookeeper-recipes-election/README.txt | 2 +- .../zookeeper-recipes-lock/README.txt | 2 +- .../zookeeper-recipes-queue/README.txt | 2 +- .../java/org/apache/zookeeper/ZooKeeper.java | 2 +- .../auth/KeyAuthenticationProvider.java | 2 +- .../server/command/FourLetterCommands.java | 36 +++++++++---------- .../server/quorum/QuorumPeerMain.java | 4 +-- zookeeper-website/app/lib/current-version.ts | 2 +- .../app/pages/_landing/security/content.md | 4 +-- .../app/routes/_api/llms-full.ts | 3 -- zookeeper-website/package-lock.json | 2 +- zookeeper-website/package.json | 2 +- zookeeper-website/public/.htaccess | 2 ++ .../unit-tests/llms-full.test.ts | 10 ++++++ zookeeper-website/vitest.config.ts | 1 + 23 files changed, 61 insertions(+), 47 deletions(-) diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml index c4a4a456640..c1d2d5ef6cd 100644 --- a/.github/workflows/website.yaml +++ b/.github/workflows/website.yaml @@ -32,7 +32,7 @@ jobs: timeout-minutes: 120 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK 11 uses: actions/setup-java@v5 with: @@ -40,7 +40,7 @@ jobs: distribution: temurin cache: 'maven' - name: Set up Node.js 22 - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: 22 cache: 'npm' @@ -50,6 +50,11 @@ jobs: - name: Install website npm dependencies working-directory: zookeeper-website run: npm ci + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('zookeeper-website/package-lock.json') }} - name: Install Playwright browsers and system dependencies working-directory: zookeeper-website run: npx playwright install --with-deps diff --git a/NOTICE.txt b/NOTICE.txt index 9b3afcb4705..3acf69dda59 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -18,7 +18,6 @@ These BSD licensed files: ./zookeeper-client/zookeeper-client-c/src/hashtable/hashtable_itr.c ./zookeeper-client/zookeeper-client-c/src/hashtable/hashtable_itr.h ./zookeeper-client/zookeeper-client-c/src/hashtable/hashtable_private.h - ./zookeeper-docs/src/main/resources/markdown/skin/prototype.js This Apache 2.0 licensed file: ./zookeeper-contrib/zookeeper-contrib-zooinspector/src/main/java/com/nitido/utils/toaster/Toaster.java diff --git a/conf/zoo_sample.cfg b/conf/zoo_sample.cfg index d0db6e99a1d..c70eb842d69 100644 --- a/conf/zoo_sample.cfg +++ b/conf/zoo_sample.cfg @@ -19,7 +19,7 @@ clientPort=2181 # Be sure to read the maintenance section of the # administrator guide before turning on autopurge. # -# https://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance +# https://zookeeper.apache.org/doc/current/admin-ops/administrators-guide/administration#maintenance # # The number of snapshots to retain in dataDir #autopurge.snapRetainCount=3 diff --git a/pom.xml b/pom.xml index 2e14fe6abd3..8df97d1fb1e 100644 --- a/pom.xml +++ b/pom.xml @@ -1082,9 +1082,6 @@ .travis.yml excludeFindBugsFilter.xml README_packaging.md - src/main/resources/markdown/skin/* - src/main/resources/markdown/html/* - src/main/resources/markdown/images/* **/src/test/resources/embedded/*.conf **/JMX-RESOURCES diff --git a/zookeeper-client/zookeeper-client-c/README b/zookeeper-client/zookeeper-client-c/README index 24869756b63..a31c6aa060b 100644 --- a/zookeeper-client/zookeeper-client-c/README +++ b/zookeeper-client/zookeeper-client-c/README @@ -8,7 +8,8 @@ For the latest information about ZooKeeper, please visit our website at: and our wiki, at: https://cwiki.apache.org/confluence/display/ZOOKEEPER -Full documentation for this release can also be found in ../../docs/index.html +Full documentation for this release can also be found at: + https://zookeeper.apache.org/doc/current/ OVERVIEW @@ -34,8 +35,9 @@ Sync and Async API. INSTALLATION -Please refer to the "Installation" item under "C Binding" section in file -".../trunk/zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md" +Please refer to the "Installation" item under "C Binding" in the +Programmer's Guide: + https://zookeeper.apache.org/doc/current/developer/programmers-guide/bindings#installation EXAMPLE/SAMPLE C CLIENT SHELL diff --git a/zookeeper-contrib/zookeeper-contrib-huebrowser/zkui/src/zkui/templates/tree.mako b/zookeeper-contrib/zookeeper-contrib-huebrowser/zkui/src/zkui/templates/tree.mako index 07c91c331bc..814676257c3 100644 --- a/zookeeper-contrib/zookeeper-contrib-huebrowser/zkui/src/zkui/templates/tree.mako +++ b/zookeeper-contrib/zookeeper-contrib-huebrowser/zkui/src/zkui/templates/tree.mako @@ -69,7 +69,7 @@ ${shared.header("ZooKeeper Browser > Tree > %s > %s" % (cluster['nice_name'], pa
-Details on stat information. +Details on stat information. ${shared.footer()} diff --git a/zookeeper-contrib/zookeeper-contrib-monitoring/README b/zookeeper-contrib/zookeeper-contrib-monitoring/README index 81521fcb0e2..44f7b2cefc8 100644 --- a/zookeeper-contrib/zookeeper-contrib-monitoring/README +++ b/zookeeper-contrib/zookeeper-contrib-monitoring/README @@ -81,5 +81,5 @@ Apache License 2.0 or later. ZooKeeper 4letterwords Commands ------------------------------- -http://zookeeper.apache.org/docs/current/zookeeperAdmin.html#sc_zkCommands +https://zookeeper.apache.org/doc/current/admin-ops/administrators-guide/commands#the-four-letter-words diff --git a/zookeeper-recipes/README.txt b/zookeeper-recipes/README.txt index bc788741d63..db41fa0d463 100644 --- a/zookeeper-recipes/README.txt +++ b/zookeeper-recipes/README.txt @@ -13,8 +13,9 @@ some unit testing with both the c and java recipe code. zkr_recipe-name_methodname (eg. zkr_lock_lock in zookeeper-recipes-lock/src/c) -6) The various recipes are in ../docs/recipes.html or -../../docs/reciped.pdf. Also, this is not an exhaustive list by any chance. +6) The various recipes are documented at + https://zookeeper.apache.org/doc/current/developer/recipes + Also, this is not an exhaustive list by any chance. Zookeeper is used (and can be used) for more than what we have listed in the docs. 7) To run the c tests in all the recipes, diff --git a/zookeeper-recipes/zookeeper-recipes-election/README.txt b/zookeeper-recipes/zookeeper-recipes-election/README.txt index f854b275eb8..3de76293a30 100644 --- a/zookeeper-recipes/zookeeper-recipes-election/README.txt +++ b/zookeeper-recipes/zookeeper-recipes-election/README.txt @@ -16,7 +16,7 @@ --> 1) This election interface recipe implements the leader election recipe -mentioned in ../../docs/recipes.[html,pdf]. +documented at https://zookeeper.apache.org/doc/current/developer/recipes 2) To compile the leader election java recipe you can just run ant jar from this directory. diff --git a/zookeeper-recipes/zookeeper-recipes-lock/README.txt b/zookeeper-recipes/zookeeper-recipes-lock/README.txt index 438de1bdbb9..e4c63081ab1 100644 --- a/zookeeper-recipes/zookeeper-recipes-lock/README.txt +++ b/zookeeper-recipes/zookeeper-recipes-lock/README.txt @@ -16,7 +16,7 @@ --> 1) This lock interface recipe implements the lock recipe -mentioned in ../../docs/recipes.[html,pdf]. +documented at https://zookeeper.apache.org/doc/current/developer/recipes 2) To compile the lock java recipe you can just run ant jar from this directory. For compiling the c library go to zookeeper-client/zookeeper-client-c and read diff --git a/zookeeper-recipes/zookeeper-recipes-queue/README.txt b/zookeeper-recipes/zookeeper-recipes-queue/README.txt index d59a3c3170a..49fb6f546d5 100644 --- a/zookeeper-recipes/zookeeper-recipes-queue/README.txt +++ b/zookeeper-recipes/zookeeper-recipes-queue/README.txt @@ -16,7 +16,7 @@ --> 1) This queue interface recipe implements the queue recipe -mentioned in ../../../docs/recipes.[html,pdf]. +documented at https://zookeeper.apache.org/doc/current/developer/recipes A more detailed explanation is at http://www.cloudera.com/blog/2009/05/28/building-a-distributed-concurrent-queue-with-apache-zookeeper/ 2) This recipe does not handle KeeperException.ConnectionLossException or ZCONNECTIONLOSS. It will only work correctly once ZOOKEEPER-22 https://issues.apache.org/jira/browse/ZOOKEEPER-22 is resolved. diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java index 32893d46af4..239f97b0f70 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java @@ -1718,7 +1718,7 @@ public void delete(final String path, int version) throws InterruptedException, * Note: The maximum allowable size of all of the data arrays in all of * the setData operations in this single request is typically 1 MB * (1,048,576 bytes). This limit is specified on the server via - * jute.maxbuffer. + * jute.maxbuffer. * Requests larger than this will cause a KeeperException to be * thrown. * diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java index cf252b9b08a..92059bef5ce 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java @@ -37,7 +37,7 @@ * See the "Pluggable ZooKeeper authentication" section of the * "Zookeeper Programmer's Guide" for general details of implementing an * authentication plugin. e.g. - * http://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#sc_ZooKeeperPluggableAuthentication + * https://zookeeper.apache.org/doc/current/developer/programmers-guide/pluggable-authentication * * This class looks for a numeric "key" under the /key node. * Authorization is granted if the user passes in as authorization a number diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/command/FourLetterCommands.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/command/FourLetterCommands.java index 3c615f4faf7..6566cdaf3c6 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/command/FourLetterCommands.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/command/FourLetterCommands.java @@ -33,108 +33,108 @@ public class FourLetterCommands { /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int confCmd = ByteBuffer.wrap("conf".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int consCmd = ByteBuffer.wrap("cons".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int crstCmd = ByteBuffer.wrap("crst".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int dirsCmd = ByteBuffer.wrap("dirs".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int dumpCmd = ByteBuffer.wrap("dump".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int enviCmd = ByteBuffer.wrap("envi".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int getTraceMaskCmd = ByteBuffer.wrap("gtmk".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int ruokCmd = ByteBuffer.wrap("ruok".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int setTraceMaskCmd = ByteBuffer.wrap("stmk".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int srvrCmd = ByteBuffer.wrap("srvr".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int srstCmd = ByteBuffer.wrap("srst".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int statCmd = ByteBuffer.wrap("stat".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int wchcCmd = ByteBuffer.wrap("wchc".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int wchpCmd = ByteBuffer.wrap("wchp".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int wchsCmd = ByteBuffer.wrap("wchs".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int mntrCmd = ByteBuffer.wrap("mntr".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int isroCmd = ByteBuffer.wrap("isro".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ protected static final int hashCmd = ByteBuffer.wrap("hash".getBytes()).getInt(); diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java index c126ea2e290..86b3b8a7c6a 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java @@ -52,8 +52,8 @@ * configuration information. This file is a Properties file, so keys and * values are separated by equals (=) and the key/value pairs are separated * by new lines. The following is a general summary of keys used in the - * configuration file. For full details on this see the documentation in - * docs/index.html + * configuration file. For full details see the + * Administrator's Guide. *
    *
  1. dataDir - The directory where the ZooKeeper data is stored.
  2. *
  3. dataLogDir - The directory where the ZooKeeper transaction log is stored.
  4. diff --git a/zookeeper-website/app/lib/current-version.ts b/zookeeper-website/app/lib/current-version.ts index d02009316e6..06e06d7fdff 100644 --- a/zookeeper-website/app/lib/current-version.ts +++ b/zookeeper-website/app/lib/current-version.ts @@ -16,4 +16,4 @@ // limitations under the License. // -export const CURRENT_VERSION = "3.9.5"; +export const CURRENT_VERSION = "3.9.10"; diff --git a/zookeeper-website/app/pages/_landing/security/content.md b/zookeeper-website/app/pages/_landing/security/content.md index 2182c632619..66daa4a49d9 100644 --- a/zookeeper-website/app/pages/_landing/security/content.md +++ b/zookeeper-website/app/pages/_landing/security/content.md @@ -24,7 +24,7 @@ The ASF Security team maintains a page with a description of how vulnerabilities ## Security model -ZooKeeper is a coordination service intended for use inside a trusted network, not exposed directly to the Internet. The [Admin Guide](https://zookeeper.apache.org/doc/current/zookeeperAdmin.html) states it plainly: "A ZooKeeper ensemble is expected to operate in a trusted computing environment. It is thus recommended deploying ZooKeeper behind a firewall." A fresh ensemble ships with no transport encryption, no peer authentication, and world-readable/writable znodes. Hardening is a shared responsibility between the ZooKeeper project and the operator. +ZooKeeper is a coordination service intended for use inside a trusted network, not exposed directly to the Internet. The [Admin Guide](https://zookeeper.apache.org/doc/current/admin-ops/administrators-guide/) states it plainly: "A ZooKeeper ensemble is expected to operate in a trusted computing environment. It is thus recommended deploying ZooKeeper behind a firewall." A fresh ensemble ships with no transport encryption, no peer authentication, and world-readable/writable znodes. Hardening is a shared responsibility between the ZooKeeper project and the operator. ### Security is opt-in @@ -254,7 +254,7 @@ See the documentation for more details on correct cluster administration. **Credit:** This issue was identified by Földi Tamás and Eugene Koontz. -**References:** [ZOOKEEPER-1045](https://issues.apache.org/jira/browse/ZOOKEEPER-1045) · [Server-Server mutual authentication](https://cwiki.apache.org/confluence/display/ZOOKEEPER/Server-Server+mutual+authentication) · [ZooKeeper Admin Guide](https://zookeeper.apache.org/doc/current/zookeeperAdmin.html) +**References:** [ZOOKEEPER-1045](https://issues.apache.org/jira/browse/ZOOKEEPER-1045) · [Server-Server mutual authentication](https://cwiki.apache.org/confluence/display/ZOOKEEPER/Server-Server+mutual+authentication) · [ZooKeeper Admin Guide](https://zookeeper.apache.org/doc/current/admin-ops/administrators-guide/) --- diff --git a/zookeeper-website/app/routes/_api/llms-full.ts b/zookeeper-website/app/routes/_api/llms-full.ts index e226e562fc1..94942e233d8 100644 --- a/zookeeper-website/app/routes/_api/llms-full.ts +++ b/zookeeper-website/app/routes/_api/llms-full.ts @@ -87,8 +87,5 @@ export function resolveLLMTextLinks( } function escapeRegExp(value: string): string { - // "#" is special in regex (start of comment). Escape it in the pattern only: - // in: /admin-ops/.../configuration-parameters#advanced-configuration - // out: /admin-ops/.../configuration-parameters\#advanced-configuration (RegExp string) return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } diff --git a/zookeeper-website/package-lock.json b/zookeeper-website/package-lock.json index 3f3b90f11c1..58db82f217d 100644 --- a/zookeeper-website/package-lock.json +++ b/zookeeper-website/package-lock.json @@ -22,7 +22,7 @@ "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "1.2.2", "@radix-ui/react-separator": "1.1.1", - "@radix-ui/react-slot": "latest", + "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-tabs": "1.1.2", "@radix-ui/react-tooltip": "1.1.6", "@react-router/node": "^7.17.0", diff --git a/zookeeper-website/package.json b/zookeeper-website/package.json index 35d46131e4d..3dc9ff98b9e 100644 --- a/zookeeper-website/package.json +++ b/zookeeper-website/package.json @@ -46,7 +46,7 @@ "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "1.2.2", "@radix-ui/react-separator": "1.1.1", - "@radix-ui/react-slot": "latest", + "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-tabs": "1.1.2", "@radix-ui/react-tooltip": "1.1.6", "@react-router/node": "^7.17.0", diff --git a/zookeeper-website/public/.htaccess b/zookeeper-website/public/.htaccess index 4b989a6561c..55aa05d65da 100644 --- a/zookeeper-website/public/.htaccess +++ b/zookeeper-website/public/.htaccess @@ -36,6 +36,8 @@ ErrorDocument 404 /404.html + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "DENY" Header always set Cache-Control "no-cache, must-revalidate" diff --git a/zookeeper-website/unit-tests/llms-full.test.ts b/zookeeper-website/unit-tests/llms-full.test.ts index e4684dcdd00..68cdc6a4a6c 100644 --- a/zookeeper-website/unit-tests/llms-full.test.ts +++ b/zookeeper-website/unit-tests/llms-full.test.ts @@ -86,6 +86,16 @@ describe("resolveLLMTextLinks", () => { ).toBe(text); }); + it("rewrites links with hash fragments in the path", () => { + const href = + "/admin-ops/administrators-guide/configuration-parameters#advanced-configuration"; + const text = `[Advanced Configuration](${href})`; + + expect(resolveLLMTextLinks(text, [{ href }])).toBe( + `[Advanced Configuration](${CURRENT_DOCS_PATH}${href})` + ); + }); + it("rewrites links inside MDX JSX text blocks when Fumadocs extracted them", () => { const text = ` See [Dynamic Reconfiguration](/admin-ops/dynamic-reconfiguration). diff --git a/zookeeper-website/vitest.config.ts b/zookeeper-website/vitest.config.ts index e8d59801d5f..f7aa2e237fe 100644 --- a/zookeeper-website/vitest.config.ts +++ b/zookeeper-website/vitest.config.ts @@ -30,6 +30,7 @@ export default defineConfig({ __CURRENT_VERSION__: JSON.stringify(CURRENT_VERSION) }, test: { + execArgv: ["--no-webstorage"], globals: true, environment: "happy-dom", setupFiles: ["./unit-tests/setup.ts"], From c567539cf71ee37938b4f58fc6a356fa499fe702 Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk Date: Thu, 25 Jun 2026 12:09:21 +0200 Subject: [PATCH 02/14] Enforce 22, 23, or 24 Node.js versions --- zookeeper-website/package.json | 2 +- zookeeper-website/vitest.config.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/zookeeper-website/package.json b/zookeeper-website/package.json index 3dc9ff98b9e..f0653c763ed 100644 --- a/zookeeper-website/package.json +++ b/zookeeper-website/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "engines": { - "node": ">=22.12.0" + "node": "^22.12.0 || ^23.0.0 || ^24.0.0" }, "scripts": { "build": "tsx scripts/build-site.ts", diff --git a/zookeeper-website/vitest.config.ts b/zookeeper-website/vitest.config.ts index f7aa2e237fe..e8d59801d5f 100644 --- a/zookeeper-website/vitest.config.ts +++ b/zookeeper-website/vitest.config.ts @@ -30,7 +30,6 @@ export default defineConfig({ __CURRENT_VERSION__: JSON.stringify(CURRENT_VERSION) }, test: { - execArgv: ["--no-webstorage"], globals: true, environment: "happy-dom", setupFiles: ["./unit-tests/setup.ts"], From 78c9a6a584a28cd041c88a3a0dbd4d85986b9d59 Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk Date: Thu, 25 Jun 2026 12:11:29 +0200 Subject: [PATCH 03/14] Fix versioning --- zookeeper-website/app/components/links.ts | 13 +- .../app/components/site-navbar.tsx | 84 ------------- .../app/lib/released-docs-versions.ts | 43 ++++--- zookeeper-website/e2e-tests/constants.ts | 5 - .../e2e-tests/older-docs-picker.spec.ts | 113 +----------------- zookeeper-website/public/sitemap.xml | 2 +- .../unit-tests/older-docs-picker.test.tsx | 17 +-- .../unit-tests/released-docs-versions.test.ts | 42 ++----- 8 files changed, 47 insertions(+), 272 deletions(-) diff --git a/zookeeper-website/app/components/links.ts b/zookeeper-website/app/components/links.ts index 6f7ca909ef8..36073b14a4d 100644 --- a/zookeeper-website/app/components/links.ts +++ b/zookeeper-website/app/components/links.ts @@ -16,8 +16,7 @@ // limitations under the License. // -import { CURRENT_VERSION } from "@/lib/current-version"; -import { CURRENT_DOCS_PATH } from "@/lib/docs-paths"; +import { getReleasedDocUrl, LTS_VERSIONS } from "@/lib/released-docs-versions"; interface LinkType { label: string; @@ -62,10 +61,12 @@ export const projectLinks: LinkType[] = [ ]; export const documentationLinks: (LinkType | NestedLinkType)[] = [ - { - label: `${CURRENT_VERSION} Documentation`, - to: CURRENT_DOCS_PATH - }, + ...LTS_VERSIONS.map( + (version): LinkType => ({ + label: `${version} Documentation`, + to: getReleasedDocUrl(version) + }) + ), { label: "Issue Tracking", to: "https://issues.apache.org/jira/browse/ZOOKEEPER", diff --git a/zookeeper-website/app/components/site-navbar.tsx b/zookeeper-website/app/components/site-navbar.tsx index 044c7fe2c0b..64638562622 100644 --- a/zookeeper-website/app/components/site-navbar.tsx +++ b/zookeeper-website/app/components/site-navbar.tsx @@ -21,7 +21,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, @@ -37,11 +36,6 @@ import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { Link } from "@/components/link"; import { asfLinks, documentationLinks, projectLinks } from "./links"; -import { OlderDocsVersionList } from "@/components/docs/older-docs-picker"; -import { - getReleasedDocUrl, - getReleasedDocVersions -} from "@/lib/released-docs-versions"; import { ThemeToggle } from "./theme-toggle"; const navLinkClass = @@ -163,17 +157,6 @@ function ProjectMenu() { ); } -function OlderDocsSubMenu() { - return ( - - Older docs - - - - - ); -} - function DocsMenu() { return ( @@ -217,8 +200,6 @@ function DocsMenu() { ) )} - - ); @@ -355,20 +336,6 @@ function MobileMenu() { ); } -function MobileOlderDocsSection() { - return ( - - - Older docs - - - - - - - ); -} - function MobileDocsSection({ onLinkClick }: { onLinkClick: () => void }) { return ( @@ -414,7 +381,6 @@ function MobileDocsSection({ onLinkClick }: { onLinkClick: () => void }) { ) )} - ); @@ -528,31 +494,6 @@ function NoJSDocsMenu() { ) )} - - - - ); -} - -function NoJSOlderDocsSubMenu() { - const versions = getReleasedDocVersions(); - - return ( -
    - - Older docs - - -
    - {versions.map((version) => ( - - {version} - - ))}
    ); @@ -678,31 +619,6 @@ function NoJSMobileDocsSection() { ) )} - - - - ); -} - -function NoJSMobileOlderDocsSection() { - const versions = getReleasedDocVersions(); - - return ( -
    - - Older docs - - -
    - {versions.map((version) => ( - - {version} - - ))}
    ); diff --git a/zookeeper-website/app/lib/released-docs-versions.ts b/zookeeper-website/app/lib/released-docs-versions.ts index 382953df96c..c56e8fd62e6 100644 --- a/zookeeper-website/app/lib/released-docs-versions.ts +++ b/zookeeper-website/app/lib/released-docs-versions.ts @@ -16,6 +16,8 @@ // limitations under the License. // +import { CURRENT_VERSION } from "./current-version"; + type PreRelease = "alpha" | "beta" | "stable"; interface ParsedVersion { @@ -47,7 +49,7 @@ const preReleaseOrder: Record = { alpha: 0 }; -export const LEGACY_RELEASED_DOC_VERSIONS = new Set([ +const RAW_RELEASED_DOC_VERSIONS_LIST = [ "3.1.2", "3.2.2", "3.3.2", @@ -99,16 +101,22 @@ export const LEGACY_RELEASED_DOC_VERSIONS = new Set([ "3.9.1", "3.9.2", "3.9.3", - "3.9.4" -]); + "3.9.4", + "3.9.5", + CURRENT_VERSION +] as const; -export const REACT_ROUTER_RELEASED_DOC_VERSIONS = new Set(); +export type ReleasedDocVersion = + (typeof RAW_RELEASED_DOC_VERSIONS_LIST)[number]; + +export const RAW_RELEASED_DOC_VERSIONS = new Set( + RAW_RELEASED_DOC_VERSIONS_LIST +); -export const RAW_RELEASED_DOC_VERSIONS: string[] = [ - ...new Set([ - ...LEGACY_RELEASED_DOC_VERSIONS, - ...REACT_ROUTER_RELEASED_DOC_VERSIONS - ]) +export const LTS_VERSIONS: ReleasedDocVersion[] = [ + CURRENT_VERSION, + "3.8.6", + "3.7.2" ]; export function sortVersionsDesc(versions: string[]): string[] { @@ -126,18 +134,17 @@ export function sortVersionsDesc(versions: string[]): string[] { * All released documentation versions available under /doc/. * Maintained manually because archived docs live in the asf-site branch. */ -export const RELEASED_DOC_VERSIONS: string[] = sortVersionsDesc( - RAW_RELEASED_DOC_VERSIONS -); +export const RELEASED_DOC_VERSIONS: string[] = sortVersionsDesc([ + ...RAW_RELEASED_DOC_VERSIONS +]); export function getReleasedDocUrl(version: string): string { - const basePath = `/doc/r${version}`; - return LEGACY_RELEASED_DOC_VERSIONS.has(version) - ? `${basePath}/index.html` - : `${basePath}/`; + return `/doc/r${version}/`; } export function getReleasedDocVersions(): string[] { + const ltsSet = new Set(LTS_VERSIONS); + if (typeof window !== "undefined") { const override = window.localStorage.getItem( "__released_doc_versions_override__" @@ -149,7 +156,7 @@ export function getReleasedDocVersions(): string[] { Array.isArray(parsed) && parsed.every((value) => typeof value === "string") ) { - return sortVersionsDesc(parsed); + return sortVersionsDesc(parsed).filter((v) => !ltsSet.has(v)); } } catch { // Ignore invalid test overrides and fall back to build-time data. @@ -157,5 +164,5 @@ export function getReleasedDocVersions(): string[] { } } - return RELEASED_DOC_VERSIONS; + return RELEASED_DOC_VERSIONS.filter((v) => !ltsSet.has(v)); } diff --git a/zookeeper-website/e2e-tests/constants.ts b/zookeeper-website/e2e-tests/constants.ts index 549507c0d67..cebe92f9932 100644 --- a/zookeeper-website/e2e-tests/constants.ts +++ b/zookeeper-website/e2e-tests/constants.ts @@ -17,7 +17,6 @@ // import { CURRENT_VERSION } from "../app/lib/current-version"; -import { RELEASED_DOC_VERSIONS } from "../app/lib/released-docs-versions"; // Current docs are served from this base in every build. Derived from // CURRENT_VERSION so a version bump needs no e2e edits. @@ -27,7 +26,3 @@ export const DOCS_ROOT = `/doc/r${CURRENT_VERSION}/`; export const DOCS_URL_PATTERN = new RegExp( `doc/r${CURRENT_VERSION.replace(/\./g, "\\.")}` ); - -// The no-JS older-docs fallback renders every released version from the real -// list, so assert against its length instead of a hardcoded count. -export const RELEASED_DOC_VERSION_COUNT = RELEASED_DOC_VERSIONS.length; diff --git a/zookeeper-website/e2e-tests/older-docs-picker.spec.ts b/zookeeper-website/e2e-tests/older-docs-picker.spec.ts index c5bd2317e9e..c02e392c433 100644 --- a/zookeeper-website/e2e-tests/older-docs-picker.spec.ts +++ b/zookeeper-website/e2e-tests/older-docs-picker.spec.ts @@ -17,16 +17,12 @@ // import { test, expect } from "@playwright/test"; -import { DOCS_ROOT, RELEASED_DOC_VERSION_COUNT } from "./constants"; +import { DOCS_ROOT } from "./constants"; const MOCK_RELEASED_DOC_VERSIONS = ["3.10.0", "3.9.4", "3.9.3"]; const RELEASED_DOC_VERSIONS_OVERRIDE_KEY = "__released_doc_versions_override__"; function expectedReleasedDocUrl(version: string): string { - if (version === "3.9.4" || version === "3.9.3") { - return `/doc/r${version}/index.html`; - } - return `/doc/r${version}/`; } @@ -154,110 +150,3 @@ test.describe("Older Docs Picker – sidebar", () => { await expect(newInput).toHaveValue(""); }); }); - -test.describe("Older Docs Picker – navbar Documentation menu", () => { - test.beforeEach(async ({ page }) => { - await page.addInitScript( - ({ key, versions }) => { - window.localStorage.setItem(key, JSON.stringify(versions)); - }, - { - key: RELEASED_DOC_VERSIONS_OVERRIDE_KEY, - versions: MOCK_RELEASED_DOC_VERSIONS - } - ); - await page.goto("/"); - await page.waitForLoadState("networkidle"); - }); - - test("Documentation menu contains an 'Older docs' sub-menu trigger", async ({ - page - }) => { - // Open the Documentation dropdown (scope to banner to avoid matching the mobile collapsible) - await page - .getByRole("banner") - .getByRole("button", { name: /documentation/i }) - .click(); - - // The sub-menu trigger should be visible - const olderDocs = page.getByRole("menuitem", { name: /older docs/i }); - await expect(olderDocs).toBeVisible(); - }); - - test("hovering 'Older docs' in the navbar opens a version sub-menu", async ({ - page - }) => { - await page - .getByRole("banner") - .getByRole("button", { name: /documentation/i }) - .click(); - - const olderDocs = page.getByRole("menuitem", { name: /older docs/i }); - await olderDocs.hover(); - // ArrowRight reliably opens Radix sub-menus cross-browser (hover alone is flaky in Firefox) - await olderDocs.press("ArrowRight"); - - // Wait until the sub-menu actually opens (a second menu element becomes visible) - await expect(page.getByRole("menu")).toHaveCount(2, { timeout: 10000 }); - const subMenu = page.getByRole("menu").last(); - - const versionLinks = subMenu.locator('a[href^="/doc/r"]'); - await expect(versionLinks.first()).toBeVisible(); - await expect(versionLinks).toHaveCount(MOCK_RELEASED_DOC_VERSIONS.length); - }); - - test("navbar older-docs links point to the correct archive paths", async ({ - page - }) => { - await page - .getByRole("banner") - .getByRole("button", { name: /documentation/i }) - .click(); - - const olderDocs = page.getByRole("menuitem", { name: /older docs/i }); - await olderDocs.hover(); - await olderDocs.press("ArrowRight"); - - const subMenu = page.getByRole("menu").last(); - await expect(subMenu).toBeVisible(); - - const links = subMenu.locator('a[href^="/doc/r"]'); - await expect(links).toHaveCount(MOCK_RELEASED_DOC_VERSIONS.length); - for (let i = 0; i < MOCK_RELEASED_DOC_VERSIONS.length; i++) { - const href = await links.nth(i).getAttribute("href"); - expect(href).toBe(expectedReleasedDocUrl(MOCK_RELEASED_DOC_VERSIONS[i])); - } - }); -}); - -test.describe("Older Docs Picker – no-JS navbar fallback", () => { - test.use({ javaScriptEnabled: false }); - - test("Documentation menu exposes older-docs links without JavaScript", async ({ - page - }) => { - await page.goto("/"); - - const documentationMenu = page - .getByRole("banner") - .locator("details") - .filter({ hasText: "Documentation" }) - .first(); - await documentationMenu.locator("summary").first().click(); - - const olderDocs = documentationMenu - .locator("details") - .filter({ hasText: "Older docs" }) - .first(); - await expect(olderDocs.locator("summary")).toBeVisible(); - - await olderDocs.locator("summary").click(); - - const links = olderDocs.locator('a[href^="/doc/r"]'); - await expect(links.first()).toBeVisible(); - await expect(links).toHaveCount(RELEASED_DOC_VERSION_COUNT); - await expect( - olderDocs.locator('a[href="/doc/r3.9.4/index.html"]') - ).toBeVisible(); - }); -}); diff --git a/zookeeper-website/public/sitemap.xml b/zookeeper-website/public/sitemap.xml index be9b4451c35..82a01e7d622 100644 --- a/zookeeper-website/public/sitemap.xml +++ b/zookeeper-website/public/sitemap.xml @@ -1 +1 @@ -https://zookeeper.apache.org/https://zookeeper.apache.org/bylaws/https://zookeeper.apache.org/credits/https://zookeeper.apache.org/doc/r3.9.5/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/administration/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/best-practices/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/commands/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/communication-using-the-netty-framework/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/configuration-parameters/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/data-file-management/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/deployment/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/cli/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/dynamic-reconfiguration/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/jmx/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/monitor-and-audit-logs/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/observers-guide/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/quorums/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/quota-guide/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/snapshot-and-restore-guide/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/tools/https://zookeeper.apache.org/doc/r3.9.5/developer/basic-tutorial/https://zookeeper.apache.org/doc/r3.9.5/developer/java-example/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/access-control-using-acls/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/bindings/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/building-blocks-a-guide-to-zookeeper-operations/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/consistency-guarantees/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/data-model/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/gotchas-common-problems-and-troubleshooting/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/pluggable-authentication/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/sessions/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/watches/https://zookeeper.apache.org/doc/r3.9.5/developer/recipes/https://zookeeper.apache.org/doc/r3.9.5/developer/use-cases/https://zookeeper.apache.org/doc/r3.9.5/miscellaneous/internals/https://zookeeper.apache.org/doc/r3.9.5/overview/quick-start/https://zookeeper.apache.org/doc/r3.9.5/overview/release-notes/https://zookeeper.apache.org/events/https://zookeeper.apache.org/irc/https://zookeeper.apache.org/mailing-lists/https://zookeeper.apache.org/news/https://zookeeper.apache.org/releases/https://zookeeper.apache.org/security/https://zookeeper.apache.org/version-control/ \ No newline at end of file +https://zookeeper.apache.org/https://zookeeper.apache.org/bylaws/https://zookeeper.apache.org/credits/https://zookeeper.apache.org/doc/r3.9.10/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/administration/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/best-practices/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/commands/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/communication-using-the-netty-framework/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/configuration-parameters/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/data-file-management/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/deployment/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/cli/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/dynamic-reconfiguration/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/jmx/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/monitor-and-audit-logs/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/observers-guide/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/quorums/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/quota-guide/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/snapshot-and-restore-guide/https://zookeeper.apache.org/doc/r3.9.10/admin-ops/tools/https://zookeeper.apache.org/doc/r3.9.10/developer/basic-tutorial/https://zookeeper.apache.org/doc/r3.9.10/developer/java-example/https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/access-control-using-acls/https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/bindings/https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/building-blocks-a-guide-to-zookeeper-operations/https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/consistency-guarantees/https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/data-model/https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/gotchas-common-problems-and-troubleshooting/https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/pluggable-authentication/https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/sessions/https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/watches/https://zookeeper.apache.org/doc/r3.9.10/developer/recipes/https://zookeeper.apache.org/doc/r3.9.10/developer/use-cases/https://zookeeper.apache.org/doc/r3.9.10/miscellaneous/internals/https://zookeeper.apache.org/doc/r3.9.10/overview/quick-start/https://zookeeper.apache.org/doc/r3.9.10/overview/release-notes/https://zookeeper.apache.org/events/https://zookeeper.apache.org/irc/https://zookeeper.apache.org/mailing-lists/https://zookeeper.apache.org/news/https://zookeeper.apache.org/releases/https://zookeeper.apache.org/security/https://zookeeper.apache.org/version-control/ \ No newline at end of file diff --git a/zookeeper-website/unit-tests/older-docs-picker.test.tsx b/zookeeper-website/unit-tests/older-docs-picker.test.tsx index 1baad0e13b2..5738c061286 100644 --- a/zookeeper-website/unit-tests/older-docs-picker.test.tsx +++ b/zookeeper-website/unit-tests/older-docs-picker.test.tsx @@ -23,9 +23,8 @@ import { renderWithProviders } from "./utils"; import { OlderDocsPicker } from "@/components/docs/older-docs-picker"; import { getReleasedDocUrl, - LEGACY_RELEASED_DOC_VERSIONS, + RAW_RELEASED_DOC_VERSIONS, RELEASED_DOC_VERSIONS, - REACT_ROUTER_RELEASED_DOC_VERSIONS, sortVersionsDesc } from "@/lib/released-docs-versions"; @@ -73,22 +72,16 @@ describe("RELEASED_DOC_VERSIONS", () => { expect(RELEASED_DOC_VERSIONS).toEqual(sorted); }); - it("combines legacy and React Router archive versions", () => { + it("matches the sorted raw archive versions", () => { expect(RELEASED_DOC_VERSIONS).toEqual( - sortVersionsDesc([ - ...LEGACY_RELEASED_DOC_VERSIONS, - ...REACT_ROUTER_RELEASED_DOC_VERSIONS - ]) + sortVersionsDesc([...RAW_RELEASED_DOC_VERSIONS]) ); }); }); describe("getReleasedDocUrl", () => { - it("uses /index.html for legacy static archives", () => { - expect(getReleasedDocUrl("3.9.4")).toBe("/doc/r3.9.4/index.html"); - }); - - it("uses /doc/r/ for React Router archives", () => { + it("links every archived version to /doc/r/", () => { + expect(getReleasedDocUrl("3.9.4")).toBe("/doc/r3.9.4/"); expect(getReleasedDocUrl("3.10.0")).toBe("/doc/r3.10.0/"); }); }); diff --git a/zookeeper-website/unit-tests/released-docs-versions.test.ts b/zookeeper-website/unit-tests/released-docs-versions.test.ts index 4b10e123b45..0d6c858e703 100644 --- a/zookeeper-website/unit-tests/released-docs-versions.test.ts +++ b/zookeeper-website/unit-tests/released-docs-versions.test.ts @@ -19,9 +19,7 @@ import { describe, it, expect } from "vitest"; import { getReleasedDocUrl, - LEGACY_RELEASED_DOC_VERSIONS, RAW_RELEASED_DOC_VERSIONS, - REACT_ROUTER_RELEASED_DOC_VERSIONS, RELEASED_DOC_VERSIONS, sortVersionsDesc } from "@/lib/released-docs-versions"; @@ -51,8 +49,8 @@ describe("sortVersionsDesc with mocked released-docs versions", () => { }); describe("RAW_RELEASED_DOC_VERSIONS", () => { - it("exposes an array", () => { - expect(Array.isArray(RAW_RELEASED_DOC_VERSIONS)).toBe(true); + it("exposes a Set", () => { + expect(RAW_RELEASED_DOC_VERSIONS).toBeInstanceOf(Set); }); it("strips the leading 'r' prefix from every folder name", () => { @@ -74,52 +72,28 @@ describe("RAW_RELEASED_DOC_VERSIONS", () => { }); it("contains the first and latest archived documentation versions", () => { - expect(RAW_RELEASED_DOC_VERSIONS).toContain("3.1.2"); - expect(RAW_RELEASED_DOC_VERSIONS).toContain("3.9.4"); - }); - - it("contains every archived docs version exactly once", () => { - expect(new Set(RAW_RELEASED_DOC_VERSIONS).size).toBe( - RAW_RELEASED_DOC_VERSIONS.length - ); - expect(RAW_RELEASED_DOC_VERSIONS.length).toBe(52); - }); - - it("combines legacy and React Router archive versions", () => { - expect(RAW_RELEASED_DOC_VERSIONS).toEqual([ - ...LEGACY_RELEASED_DOC_VERSIONS, - ...REACT_ROUTER_RELEASED_DOC_VERSIONS - ]); - }); - - it("keeps legacy and React Router archive version sets distinct", () => { - const legacyVersions = new Set(LEGACY_RELEASED_DOC_VERSIONS); - REACT_ROUTER_RELEASED_DOC_VERSIONS.forEach((version) => { - expect(legacyVersions.has(version)).toBe(false); - }); + expect(RAW_RELEASED_DOC_VERSIONS.has("3.1.2")).toBe(true); + expect(RAW_RELEASED_DOC_VERSIONS.has("3.9.4")).toBe(true); }); }); describe("RELEASED_DOC_VERSIONS (sorted output)", () => { it("equals sortVersionsDesc applied to the raw virtual-module data", () => { expect(RELEASED_DOC_VERSIONS).toEqual( - sortVersionsDesc(RAW_RELEASED_DOC_VERSIONS) + sortVersionsDesc([...RAW_RELEASED_DOC_VERSIONS]) ); }); it("contains the same entries as RAW_RELEASED_DOC_VERSIONS, just reordered", () => { expect(RELEASED_DOC_VERSIONS.slice().sort()).toEqual( - RAW_RELEASED_DOC_VERSIONS.slice().sort() + [...RAW_RELEASED_DOC_VERSIONS].sort() ); }); }); describe("getReleasedDocUrl", () => { - it("links legacy static archives to index.html", () => { - expect(getReleasedDocUrl("3.9.4")).toBe("/doc/r3.9.4/index.html"); - }); - - it("links new React Router archives to /doc/r/", () => { + it("links every archived version to /doc/r/", () => { + expect(getReleasedDocUrl("3.9.4")).toBe("/doc/r3.9.4/"); expect(getReleasedDocUrl("3.10.0")).toBe("/doc/r3.10.0/"); }); }); From d2863c1c655037f75e3b57815b6323a46f426a68 Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk Date: Thu, 25 Jun 2026 12:21:58 +0200 Subject: [PATCH 04/14] Update links --- zookeeper-website/app/components/getting-started.tsx | 2 +- zookeeper-website/app/pages/_landing/home/community.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/zookeeper-website/app/components/getting-started.tsx b/zookeeper-website/app/components/getting-started.tsx index 633c8b7c58a..fae43f7d4ad 100644 --- a/zookeeper-website/app/components/getting-started.tsx +++ b/zookeeper-website/app/components/getting-started.tsx @@ -35,7 +35,7 @@ export function GettingStartedSection() { { title: "3. Write a Client", desc: "Follow the basic tutorial to implement distributed primitives like barriers and queues.", - to: `${CURRENT_DOCS_PATH}/developer/basic-tutorial` + to: "https://cwiki.apache.org/confluence/display/ZOOKEEPER/Tutorial" } ]; return ( diff --git a/zookeeper-website/app/pages/_landing/home/community.tsx b/zookeeper-website/app/pages/_landing/home/community.tsx index 32113878002..186f2a3fd64 100644 --- a/zookeeper-website/app/pages/_landing/home/community.tsx +++ b/zookeeper-website/app/pages/_landing/home/community.tsx @@ -40,7 +40,10 @@ export function CommunitySection() { Mailing Lists From 3056d8967efb080f9d81e92302f1859b9f3cf88b Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk Date: Thu, 25 Jun 2026 15:04:32 +0200 Subject: [PATCH 05/14] Add README --- zookeeper-website/README.md | 838 ++++++++++++++++++++++++++++++++++++ 1 file changed, 838 insertions(+) create mode 100644 zookeeper-website/README.md diff --git a/zookeeper-website/README.md b/zookeeper-website/README.md new file mode 100644 index 00000000000..f3e0e4f3d61 --- /dev/null +++ b/zookeeper-website/README.md @@ -0,0 +1,838 @@ + + +# Apache ZooKeeper Website + +The official website for Apache ZooKeeper, built with modern web technologies to provide a fast, accessible, and maintainable web presence. + +--- + +## Table of Contents + +- [Content Editing](#content-editing) +- [Development](#development) + - [Prerequisites](#prerequisites) + - [Technology Stack](#technology-stack) + - [Project Architecture](#project-architecture) + - [Getting Started](#getting-started) + - [Development Workflow](#development-workflow) + - [Testing](#testing) + - [Building for Production](#building-for-production) + - [Building Archived Docs](#building-archived-docs) + - [Continuous Integration](#continuous-integration) + - [Maven Integration](#maven-integration) + - [Deployment](#deployment) + - [Publishing a New ZooKeeper Release](#publishing-a-new-zookeeper-release) + - [Troubleshooting](#troubleshooting) + +--- + +## Content Editing + +Most landing pages store content in **Markdown (`.md`)**, and some in **JSON (`.json`)**, files located in `app/pages/_landing/[page-name]/`. Docs content lives under `app/pages/_docs/` and content is stored in `.mdx` (it's just extended markdown). + +**Examples:** + +- `app/pages/_landing/credits/developers.json` - JSON data for developers +- `app/pages/_landing/releases/index.tsx` - Releases page +- `app/pages/_docs/docs/_mdx/...` - MDX content for documentation + +--- + +## Development + +### Prerequisites + +Before you begin, ensure you have the following installed: + +- **Node.js — 22, 23, or 24** (JavaScript runtime; like the JVM for Java) + - Download from [nodejs.org](https://nodejs.org/) + - Verify installation: `node --version` (must satisfy `^22.12.0 || ^23.0.0 || ^24.0.0`; see [`zookeeper-website/package.json`](package.json) `engines.node`) + - **Node 25 and 26 are not supported** — some tests fail under those releases. The `engines` field in `package.json` advertises the allowed range; npm prints a warning when it does not match but still installs. Treat the warning as blocking and downgrade to a supported Node line before running the CI pipeline. + +- **NPM** - Node Package Manager (like Maven for Java) + - Comes bundled with Node.js + - Verify installation: `npm --version` + +### Technology Stack + +This website uses modern web technologies. Here's what each one does (with Java analogies): + +#### Core Framework + +- **React Router** - Full-stack web framework with Server-Side Generation (SSG) + - Handles routing (like Spring MVC controllers) + - Provides server-side rendering for better performance and SEO + - Enables progressive enhancement (see below) + - [Documentation](https://reactrouter.com/) + +#### Documentation Framework + +- **Fumadocs** - Documentation framework used for the docs section + - Provides MDX-based docs structure and navigation + - Lives alongside the landing pages in the same React Router app + - [Documentation](https://fumadocs.com/) + +#### Progressive Enhancement + +The website uses **progressive enhancement** ([learn more](https://reactrouter.com/explanation/progressive-enhancement)), which means: + +- **With JavaScript enabled**: Users get a Single Page Application (SPA) experience + - Fast page transitions without full page reloads + - Smooth animations and interactive features + - Enhanced user experience + +- **Without JavaScript**: Core content and navigation remain accessible via server-rendered HTML + - Primary page content and most links work without client-side routing + - Critical flows such as the "Older docs" menu include no-JS fallbacks + - Some features (search, theme toggle, interactive menus) don't work since they require JavaScript + +This approach keeps the site usable without JavaScript while still providing a richer experience when it is available. + +#### UI Components + +- **shadcn/ui** - Pre-built, accessible UI components + - Similar to a component library like PrimeFaces or Vaadin in Java + - Provides buttons, cards, navigation menus, etc. + - [Documentation](https://ui.shadcn.com/) + +#### Styling + +- **TailwindCSS** - Utility-first CSS framework, aka Bootstrap on steroids + - Instead of writing CSS files, you apply classes directly in components + - Example: `className="text-blue-500 font-bold"` makes blue, bold text + +#### Code Quality Tools + +- **TypeScript** - Typed superset of JavaScript + - Similar to Java's type system + - Catches errors at compile-time instead of runtime + - Provides autocomplete and better IDE support + +- **ESLint + Prettier** - Code linting and formatting (like Checkstyle) + - ESLint analyzes code for potential errors and enforces coding standards + - Prettier handles automatic code formatting (spacing, indentation, etc.) + - Integrated together: `npm run lint:fix` handles both linting and formatting + - Configuration: `eslint.config.js` and `prettier.config.js` + +### Project Architecture + +The project follows a clear directory structure with separation of concerns: + +``` +zookeeper-website/ +├── app/ # Application source code +│ ├── ui/ # Reusable UI components (no business logic) +│ │ ├── button.tsx # Generic button component +│ │ ├── card.tsx # Card container component +│ │ └── ... # Other UI primitives +│ │ +│ ├── components/ # Reusable components WITH business logic +│ │ ├── site-navbar.tsx # Website navigation bar +│ │ ├── site-footer.tsx # Website footer +│ │ ├── theme-toggle.tsx # Dark/light mode toggle +│ │ +│ ├── pages/ # Complete pages (composed of ui + components) +│ │ ├── _landing/ # Landing pages + layout +│ │ │ ├── home/ # Home page +│ │ │ │ ├── index.tsx # Main page component (exported) +│ │ │ │ ├── hero.tsx # Hero section (not exported) +│ │ │ │ ├── features.tsx # Features section (not exported) +│ │ │ │ └── ... +│ │ │ ├── news/ # Landing page content +│ │ │ └── ... +│ │ ├── _docs/ # Documentation (Fumadocs) +│ │ │ ├── docs/ # MDX content and structure +│ │ │ ├── docs-layout.tsx # Fumadocs layout wrapper +│ │ │ └── ... +│ │ +│ ├── routes/ # Route definitions and metadata +│ │ ├── _landing/ # Landing page routes +│ │ │ ├── home.tsx # Home route configuration +│ │ │ ├── news.tsx # News route configuration +│ │ │ └── ... +│ │ └── _docs/ # Docs route configuration +│ │ +│ ├── lib/ # Utility functions and integrations +│ │ ├── utils.ts # Helper functions +│ │ └── theme-provider.tsx # Theme management +│ │ +│ ├── routes.ts # Main routing configuration +│ ├── root.tsx # Root layout component +│ └── app.css # Global styles +│ +├── build/ # Generated files (DO NOT EDIT) +│ ├── client/ # Browser-side assets +│ │ ├── index.html # HTML files for each page +│ │ ├── assets/ # JavaScript, CSS bundles +│ │ └── images/ # Optimized images +│ +├── public/ # Static files (copied as-is to build/) +│ ├── favicon.ico # Website icon +│ ├── images/ # Image assets +│ ├── docs-images/ # Docs image assets +│ └── ... +│ +├── node_modules/ # Dependencies (like Maven's .m2 directory) +├── package.json # Project metadata and dependencies (like pom.xml) +├── tsconfig.json # TypeScript configuration +└── react-router.config.ts # React Router framework configuration +``` + +#### Key Principles + +1. **UI Components (`/ui`)**: Pure, reusable components with no business logic + - Can be used anywhere in the application + - Only concerned with appearance and basic interaction + +2. **Business Components (`/components`)**: Reusable across pages + - May contain business logic specific to ZooKeeper website + - Examples: navigation, footer, theme toggle + +3. **Pages (`/pages`)**: Complete pages combining ui and components + - Each page has its own directory + - Only `index.tsx` is exported + - Internal components stay within the page directory + - If a component needs to be reused, move it to `/components` + +4. **Routes (`/routes`)**: Define routing and metadata + - Maps URLs to pages + - Sets page titles, meta tags, etc. + +5. **One Codebase, Two Independent Builds**: + - **Landing pages** live under `app/pages/_landing/` and use the landing layout; they are served from the site root (`/`). + - **Docs pages** live under `app/pages/_docs/` and use Fumadocs layouts; they are served from a versioned base (`/doc/r/`). + - They share one codebase but are produced as two **separate** builds (landing at base `/`, docs at base `/doc/r/`). A docs change therefore never rewrites landing assets, and vice versa, keeping `asf-site` diffs clean. `npm run build` runs both and merges them; `npm run dev` serves a combined site so local authoring is unaffected. + - The build context is selected by environment variables read in [`app/routes.ts`](app/routes.ts) and the Vite/React Router configs: + - `ZOOKEEPER_DOCS_ARCHIVE_BASE=/doc/rX/` → **docs build** (any version, including the current one): docs catch-all + `api/search` + `llms-full.txt`, served under `/doc/rX/`. + - `ZOOKEEPER_BUILD_TARGET=landing` → **landing build**: landing pages + the `/doc` redirect only. + - neither (default, e.g. `react-router dev`) → **combined** site for local development. + +6. **Documentation Structure**: + - **Docs** live under `app/pages/_docs/docs/_mdx/` and are the source of truth. + +7. **Documentation Versions**: + + Three constants drive the versions surfaced by the site, all defined together for a reason: + + - **`CURRENT_VERSION`** ([`app/lib/current-version.ts`](app/lib/current-version.ts)) — the version the in-tree MDX corresponds to. On `master` this is the **canary** version that the next release is being prepared for. The docs build serves it from `/doc/r/`. + - **`RAW_RELEASED_DOC_VERSIONS_LIST`** ([`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts)) — every documentation version that has ever been published under `/doc/`. New entries are appended when a release ships, and the list is kept in sync with the `content/doc/r/` directories on `asf-site`. `CURRENT_VERSION` is included at the end so it is exposed through the same machinery. + - **`LTS_VERSIONS`** ([`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts)) — versions that are pinned to the top of the **Documentation** dropdown in the site navbar. By convention `CURRENT_VERSION` is **first** in this list, followed by the still-supported long-term branches. Everything else from `RAW_RELEASED_DOC_VERSIONS_LIST` is shown afterwards under "Older versions", sorted in descending order. + + When releasing a new version, both `RAW_RELEASED_DOC_VERSIONS_LIST` and `LTS_VERSIONS` typically need to be touched — see the [release flow](#publishing-a-new-zookeeper-release). For routine non-release edits, neither file changes. + +#### Important Conventions + +##### Custom Link Component + +**Always use the custom Link component from `@/components/link` instead of importing Link directly from `react-router`.** + +The ZooKeeper website includes pages that are not part of this React Router application (e.g., documentation pages, API docs). The custom Link component automatically determines whether a link should trigger a hard reload or use React Router's client-side navigation: + +**Usage:** + +```typescript +// ✅ CORRECT - Use custom Link component +import { Link } from "@/components/link"; + +export const MyComponent = () => ( + News +); +``` + +```typescript +// ❌ WRONG - Do not import Link from react-router +import { Link } from "react-router"; + +export const MyComponent = () => ( + News +); +``` + +The ESLint configuration includes a custom rule (`custom/no-react-router-link`) that will throw an error if you attempt to import `Link` from `react-router`, helping enforce this convention automatically. + +### Getting Started + +#### 1. Install Dependencies + +Think of this as `mvn install`: + +```bash +npm ci +``` + +`npm ci` (short for "clean install") downloads all required packages from npm (similar to Maven Central). **Prefer `ci` over `npm install` for any reproducible build** — your local dev tree, contributor checkouts, the release publish flow, CI: + +- It installs the **exact** versions pinned in `package-lock.json`. `npm install` is allowed to upgrade dependencies within the semver ranges in `package.json` and to rewrite the lock file; `npm ci` never touches the lock file. +- It wipes `node_modules/` first, so the resulting tree always matches the lock file rather than carrying over stale packages from a previous install. +- It is also typically faster than `npm install` in a fresh checkout. + +#### 2. Start Development Server + +```bash +npm run dev +``` + +This starts a local development server with: + +- **Hot Module Replacement (HMR)**: Code changes appear instantly without full page reload +- **Live at**: `http://localhost:5173` + +### Development Workflow + +#### Making Changes + +1. **Edit code** in the `app/` directory +2. **Save the file** - changes appear automatically in the browser +3. **Check for errors** in the terminal where `npm run dev` is running and in browser console + +#### Common Tasks + +**Add a new landing page:** + +1. Create a directory in `app/pages/_landing/my-new-page/` +2. Create `index.tsx` in that directory and export the page component +3. Create a route file in `app/routes/_landing/my-new-page.tsx` with `meta()` and a default export +4. Register the route in `app/routes.ts` inside the `_landing` layout block + +**Add a new documentation page:** + +1. Create a new `.mdx` file in `app/pages/_docs/docs/_mdx/` (for example `my-topic.mdx`). +2. Add the new file to the relevant `meta.json` in the same section folder so it appears in navigation. + +When writing docs content, follow these conventions: + +- Internal doc links are **doc-root-relative** — write `/admin-ops/cli`, not `/docs/...` and not a hardcoded `/doc/r/...`. The version prefix (`/doc/r`) is added automatically at render time (`resolveDocsHref` on the live site, React Router `basename` in an archive). +- Images use the static asset path `/docs-images/...` (served from `public/docs-images/`). +- Use absolute `https://` URLs for off-site links (e.g. `https://zookeeper.apache.org/`). + +**Update content:** + +- Edit the appropriate `.md`, `.mdx`, or `.json` file +- Changes appear automatically + +**Add a UI component:** + +- Check if shadcn/ui has what you need first +- Only create custom components if necessary + +**Update the 404 page:** + +There are two separate 404 mechanisms: + +- **Static Apache 404** (`public/404.html`): Served by Apache for requests that never reach the SPA (missing static files, direct URL hits). Configure handling in `public/.htaccess` (`ErrorDocument 404 /404.html`). Keep `public/robots.txt` disallowing `/404.html` so the error page is not indexed. +- **In-app error boundary** (`app/root.tsx`): Handles unknown routes after the React app loads (for example, invalid docs paths). Edit `ErrorBoundary` in `root.tsx` to change that experience. + +**Check code quality:** + +```bash +npm run lint +``` + +**Fix linting and formatting issues:** + +```bash +npm run lint:fix +``` + +### Testing + +The project uses [Vitest](https://vitest.dev/) and [Playwright](http://playwright.dev/) for testing. Vitest is for unit testing, while Playwright is for e2e testing. + +**Note:** Playwright tests are configured to run against the production build. Therefore, you must build the project (`npm run build`) before running the e2e tests locally. + +**Run tests:** + +```bash +# Run all tests (unit + e2e) +# Note: e2e tests require a production build first (`npm run build`) +npm test + +# Run unit tests once (for CI/CD) +npm run test:unit:run + +# Run unit tests with UI +npm run test:unit:ui + +# Run e2e tests (requires `npm run build` first) +npm run test:e2e + +# Run e2e tests with UI (requires `npm run build` first) +npm run test:e2e:ui +``` + +**Writing new tests:** + +Use the `renderWithProviders` utility in `unit-tests/utils.tsx` to ensure components have access to routing and theme context: + +```typescript +import { renderWithProviders, screen } from "./utils"; +import { MyComponent } from "@/components/my-component"; + +describe("MyComponent", () => { + it("renders correctly", () => { + renderWithProviders(); + expect(screen.getByText("Hello World")).toBeInTheDocument(); + }); +}); +``` + +### Building for Production + +**CI/CD Workflow:** + +Before merging or deploying, run the full CI pipeline: + +```bash +npm run ci +``` + +This command runs all quality checks and builds the project. All checks must pass before code is considered ready. + +If you need the CI flow without unit/e2e test suites, use: + +```bash +npm run ci-skip-tests +``` + +This runs docs initialization and the production build (without lint/typecheck and without unit/e2e test suites). + +Generated files are located under the `build/` directory. + +**Build commands:** + +The landing site and the docs are produced as two independent Vite builds (see [One Codebase, Two Independent Builds](#key-principles)). `npm run build` orchestrates them via [`scripts/build-site.ts`](scripts/build-site.ts): + +- `npm run build` — builds the current docs (`/doc/r/`), then the landing site (base `/`), then merges the docs into `build/client/doc/r/` and regenerates the full sitemap (`--scope all`). The merged `build/client/` is what `npm run start`, `vite preview`, and the e2e tests serve. +- `npm run build:landing` — landing-only build (sets `ZOOKEEPER_BUILD_TARGET=landing`), output at `build/client/` (root pages), and upserts the landing slice of the sitemap (`--scope landing`), keeping the current-docs URLs from the committed `public/sitemap.xml`. +- `npm run build:docs` — pure docs build for `CURRENT_VERSION`, output at `build/doc/r/`, and upserts the docs slice of the sitemap (`--scope docs`), keeping landing URLs from the committed base. No CI checks. +- `npm run build:docs-archive` — runs `npm run ci` first, then the same docs build, output at `build/doc/r/`, with the archived-docs banner enabled, for snapshotting `CURRENT_VERSION` (see below). It does not touch the sitemap (archived versions are excluded). + +The docs version always comes from [`app/lib/current-version.ts`](app/lib/current-version.ts). + +`CURRENT_VERSION` on the **`master`** branch is **not** a released version — it is the **canary** for the next release that is being worked on. The actual released version is whatever sits at `content/doc/current/` on `asf-site`. `CURRENT_VERSION` is bumped on `master` as soon as work on the next version begins (see [Publishing a New ZooKeeper Release](#publishing-a-new-zookeeper-release)); from that point on, edits to docs on `master` accumulate under the new canary value and ship together when that release cuts. + +Because each docs version and the landing site are separate Vite builds, they have independent asset hashes: editing docs never changes landing output, and vice versa. This is what makes it possible to **ship landing-page updates and docs updates independently**: + +- A typo fix or new section in the landing pages can be published to `asf-site` without rebuilding or re-publishing the current docs directory. +- A docs-only change (a fix in MDX content, or a new archived docs version) can be published without touching the landing files. +- The [basic workflow](#basic-workflow) below describes the combined rebuild. + +> Note: React Router cleans the entire `build/` directory at the start of every build, so the orchestrator stashes the docs output outside `build/` before running the landing build, then restores it into `build/client/`. + +#### Building Archived Docs + +To snapshot a released documentation version, build a versioned website archive: + +```bash +npm run build:docs-archive +``` + +This creates `build/doc/r/`, ready to copy to `asf-site` at `content/doc/r/`. + +Before building the archive, this command runs `npm run ci` without the archive base path. That verifies lint, typecheck, unit tests, the normal production build, and Playwright e2e tests. It then rebuilds with the archive base path and validates the generated docs output, including that every source MDX docs page has a corresponding HTML page. + +The current version's docs are built with this **same** pipeline (`npm run build:docs`); an archive is the same build, just produced before `CURRENT_VERSION` is bumped and with the switch-to-latest banner enabled. Archived and current docs share one versioned route shape: + +- `/doc/r3.9.6/` +- `/doc/r3.9.6/overview/quick-start` +- `/doc/r3.9.6/developer/programmers-guide` + +The docs build uses React Router's `basename` and Vite's `base` to serve docs from `/doc/r/`. It uses the docs route set from `app/routes.ts`: the docs catch-all, `api/search`, and `llms-full.txt`. Non-doc archive-local requests such as `/doc/r3.9.6/news` are redirected by the generated `.htaccess` file to the matching live-site path (`/news`). + +Docs builds set the `ZOOKEEPER_DOCS_ARCHIVE_BASE` env var. Vite turns it into the app `base`, which the browser reads back as `import.meta.env.BASE_URL`. Rule of thumb for the build context: + +- Node-side code (vite/react-router configs, scripts) reads `ZOOKEEPER_DOCS_ARCHIVE_BASE` and `ZOOKEEPER_BUILD_TARGET` from `process.env` (via `getDocsArchiveBase()` / `getBuildTarget()` in [`app/lib/docs-archive.ts`](app/lib/docs-archive.ts)). +- Bundled app code reads the docs base back as `import.meta.env.BASE_URL`. + +`ZOOKEEPER_DOCS_ARCHIVE_BASE` and `import.meta.env.BASE_URL` are the same value in two execution contexts. `ZOOKEEPER_BUILD_TARGET=landing` is the orthogonal signal that selects the landing-only build. + +`build:docs-archive` additionally sets `VITE_ZOOKEEPER_DOCS_ARCHIVE_SNAPSHOT=1`. Vite exposes that flag to bundled browser code via `import.meta.env`, and the archive banner reads it directly. The flag is intentionally not a version comparison: archives are built before `CURRENT_VERSION` is bumped, so the outgoing version can still equal the current source version during the archive build. + +The docs build script packages the generated docs into a self-contained directory. React Router prerenders docs HTML under `build/client/doc/r/`, while Vite and `public/` assets are emitted at the build root (`build/client/assets/`, `build/client/docs-images/`, `build/client/fonts/`, `build/client/images/`, `build/client/favicon.ico`). The script copies the docs HTML plus those known static assets into `build/doc/r/` so URLs such as `/doc/r3.9.6/assets/...` exist after publishing. If new top-level static folders or asset roots are introduced, update `scripts/build-docs.ts` so they are copied, URL-rewritten if needed, and covered by docs build validation. + +Archive output intentionally does not copy the live site's root-only files such as `404.html`, `robots.txt`, `__spa-fallback.html`, or the root `.htaccess`. Archives get their own generated `.htaccess` (described above). The docs build keeps only docs routes, `api/search`, and `llms-full.txt`. + +#### Continuous Integration + +GitHub Actions runs the website build on every push and pull request via [`.github/workflows/website.yaml`](../.github/workflows/website.yaml). The workflow: + +1. Sets up JDK 11 and Node.js 22 +2. Runs `npm ci` and installs Playwright browsers with system dependencies +3. Executes `mvn -pl zookeeper-website site` (same checks as local `npm run ci`) + +Pull requests should pass the **Website / website-site** check before merge. + +### Maven Integration + +The website is integrated into the Apache ZooKeeper Maven build through two POMs: + +- The repository root `pom.xml` includes `zookeeper-website` as a Maven module. +- `zookeeper-website/pom.xml` owns the website-specific build and configures `frontend-maven-plugin`. + +The frontend plugin is intentionally bound only to the Maven `site` lifecycle: + +- `pre-site` installs Node.js/npm and runs `npm install` +- `site` runs the website CI command + +Because of that, the website is built **only during site generation** (`mvn site`) and is skipped during regular Maven lifecycle phases such as `mvn clean install`. + +#### When the Website Builds + +The website build is triggered **only** when you run: + +```bash +mvn site +``` + +The website will **NOT** build during regular commands like: + +- `mvn clean install` +- `mvn package` +- `mvn compile` + +This keeps regular ZooKeeper builds fast while still allowing the website to be generated when needed. + +#### What the Website POM Does During `mvn site` + +When you run `mvn site`, the website module automatically: + +1. **Cleans previous build artifacts** + - Removes `build/` directory + - Removes `node_modules/` directory + - Ensures a fresh build environment + +2. **Installs Node.js v22.20.0 and npm 11.6.2** (if not already available) + - Installed to `target/` directory + - Does not affect your system Node/npm installation + +3. **Runs `npm install`** to install all dependencies + - Reads from `package.json` + - Installs to `node_modules/` + +4. **Runs a website CI command**: + - Default (`mvn site`): `npm run ci` + - With test skipping (`mvn site -DskipTests`): `npm run ci-skip-tests` + + `npm run ci` executes: + - `npm run fumadocs-init` - Initialize Fumadocs + - `npm run lint` - ESLint code quality checks + - `npm run typecheck` - TypeScript type checking + - `npm run test:unit:run` - Vitest unit tests + - `npm run build` - Production build + - `npx playwright install` - Installs Playwright browsers + - `npm run test:e2e` - Playwright e2e tests + + `npm run ci-skip-tests` executes: + - `npm run fumadocs-init` - Initialize Fumadocs + - `npm run build` - Production build + +5. **Build Output**: Generated files are in `build/` directory + +#### Maven Commands + +**Build ZooKeeper WITHOUT the Website (default):** + +```bash +# From ZooKeeper root directory +mvn clean install +``` + +**Build the Website:** + +```bash +# From ZooKeeper root directory +mvn site +``` + +This generates the full ZooKeeper website including documentation and the React-based website. + +**Build the Website While Skipping Test Suites:** + +```bash +# From ZooKeeper root or zookeeper-website directory +mvn site -DskipTests +``` + +This runs `npm run ci-skip-tests` for the website module. + +**Build Website Only:** + +```bash +# From ZooKeeper root directory +mvn -pl zookeeper-website site + +# Or from the website module directory +cd zookeeper-website +mvn site +``` + +### Deployment + +The website source lives on the **`master`** branch of the [apache/zookeeper](https://github.com/apache/zookeeper) repository under `zookeeper-website/`. The live production site at [zookeeper.apache.org](https://zookeeper.apache.org) is served from the **`asf-site`** branch. Any commit pushed to `asf-site` is immediately reflected on the live site. + +#### Basic workflow + +Use this flow for any update to an already-published version: landing edits, docs fixes, news, maintenance. For shipping a new version, see [Publishing a New ZooKeeper Release](#publishing-a-new-zookeeper-release). + +The end-to-end flow has two halves: **source changes land on `master` via PR**, then **the rebuilt site is published to `asf-site` by hand**. Because the landing site and the current docs are independent builds with independent asset hashes, they live in separate directories under `content/` on `asf-site` (landing at the root, current docs under `content/doc/r/`). If a part's source did not change, its rebuilt output is byte-identical and produces no `content/` diff, so seeing changes in only landing or only docs after a rebuild is expected. The current docs directory is self-contained (its own assets, `.htaccess`, and `llms-full.txt`); only the Maven-generated `apidocs/` is layered back in during publish. + +The steps: + +1. **Clone the repo** and check out `master`. +2. **Install dependencies** with `npm ci`. +3. **Make changes** on a feature branch based off `master`. +4. **Verify locally** by running the full CI pipeline (`npm run ci`). +5. **Open a PR and merge to `master`** after review. +6. **Build the production bundle** on `master` and stage `build/client/` into `/tmp`. The output is what will land under `content/` on `asf-site`: landing files at the root of `build/client/` and current docs under `build/client/doc/r/`. +7. **Switch to `asf-site`** and wipe everything not tracked by that branch with `git clean -fxd`. +8. **Stage the current version's `apidocs/`** out to `/tmp`. It is published by the Maven build (not by the website CI pipeline), so it must be preserved across the docs replace in step 10. +9. **Replace the landing files** at the root of `content/` from `/tmp`, leaving `content/doc/` untouched so every archived version stays in place. +10. **Replace the current docs directory** (`content/doc/r/`) with the freshly built one, then layer the staged `apidocs/` back in. +11. **Commit and push `asf-site`**, then clean up the staging directories under `/tmp`. + +Step-by-step, in shell: + +```bash +# Set this to the current docs version from app/lib/current-version.ts (the canary on master) +DOC_VERSION=3.9.5 + +# Step 1: Clone and check out master +git clone https://github.com/apache/zookeeper.git +cd zookeeper +git checkout master + +# Step 2: Install dependencies +cd zookeeper-website +npm ci + +# Step 3: Make changes +git checkout -b +# Edit content in zookeeper-website/, then continue... + +# Step 4: Verify locally +npm run ci + +# Step 5: Push, open a PR, merge to master +git add +git commit -m "Update website content" +git push origin +# ...open a PR, get review, merge to master, then pull master locally before step 6. + +# Step 6: Stage the build output to /tmp BEFORE switching branches +# Preventive measure to ensure no conflicts appear. +rm -rf /tmp/zookeeper-site-build +cp -R build/client /tmp/zookeeper-site-build + +# Step 7: Switch to asf-site and clean +cd .. +git checkout asf-site +# Drop every untracked file left over from the master checkout +git clean -fxd + +# Step 8: Preserve the current version's API docs +# Same preemptive-wipe pattern as above. +rm -rf /tmp/zookeeper-apidocs-r${DOC_VERSION} +cp -R content/doc/r${DOC_VERSION}/apidocs /tmp/zookeeper-apidocs-r${DOC_VERSION} + +# Step 9: Replace landing files (everything in content/ except content/doc/) +find content -mindepth 1 -maxdepth 1 ! -name doc -exec rm -rf {} + +find /tmp/zookeeper-site-build -mindepth 1 -maxdepth 1 ! -name doc -exec cp -R {} content/ \; + +# Step 10: Replace the current docs version, then restore its API docs +rm -rf content/doc/r${DOC_VERSION} +mkdir -p content/doc/r${DOC_VERSION} +cp -R /tmp/zookeeper-site-build/doc/r${DOC_VERSION}/. content/doc/r${DOC_VERSION}/ +cp -R /tmp/zookeeper-apidocs-r${DOC_VERSION} content/doc/r${DOC_VERSION}/apidocs + +# Step 11: Commit, push, clean up +git add content +git commit -m "Publish website " +git push origin asf-site + +rm -rf /tmp/zookeeper-site-build /tmp/zookeeper-apidocs-r${DOC_VERSION} +``` + +Once `asf-site` is pushed the updates are live within minutes. + +--- + +### Publishing a New ZooKeeper Release + +A ZooKeeper release follows the established Apache "cut a release branch off the line branch" pattern, and the website piggybacks on it. The big picture is: + +1. **Prepare** `master` for the release — update landing content, version constants, MDX docs, etc. `CURRENT_VERSION` should already match the version being released. +2. **Cut the release branch** — e.g. `branch-3.9` -> `branch-3.9.6`, push. +3. **Build the docs from the release branch** so the rendered HTML pins to that tag rather than to whatever is on `master`. +4. **Publish on `asf-site`** — add `content/doc/r3.9.6/`, repoint `content/doc/current` to it, replace the landing pages in the root directory. + +Example: current release is **3.9.5**, new release is **3.9.6**. The line branch is **`branch-3.9`**. Throughout this section: `OUTGOING=3.9.5`, `CURRENT=3.9.6`. + +#### Step 1 — Prepare landing + docs source on `master` + +Done on the `master` branch, merged through normal PRs **before** the release branch is cut. + +1. Confirm `CURRENT_VERSION` in [`app/lib/current-version.ts`](app/lib/current-version.ts) already equals the release being prepared (e.g. `"3.9.6"`). The bump should have happened when work on this version started. If it doesn't, do the bump now as a normal PR (see [Bumping `CURRENT_VERSION`](#bumping-current_version-canary)). +2. Update landing pages that reference the version: `releases`, `news`, anywhere else that calls out the latest version by string. +3. Update `RAW_RELEASED_DOC_VERSIONS_LIST` and `LTS_VERSIONS` in [`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts): + - Append the new version to `RAW_RELEASED_DOC_VERSIONS_LIST` (just before the trailing `CURRENT_VERSION` entry). The list must mirror the `content/doc/r/` directories on `asf-site` once the release publishes. + - Update `LTS_VERSIONS` if this release changes long-term-support membership. By convention `CURRENT_VERSION` is the **first** entry — these versions get pinned to the top of the navbar's **Documentation** dropdown. +4. Update MDX under `app/pages/_docs/docs/_mdx/` for the new release (release notes, version-specific instructions, etc.). +5. Run `npm ci && npm run ci` locally and open a PR. Merge once green. + +#### Step 2 — Cut the release branch + +This is the standard ZooKeeper release-branching step (e.g. `branch-3.9` → `branch-3.9.6`). + +#### Step 3 — Build docs and landing from the release branch + +```bash +CURRENT=3.9.6 +OUTGOING=3.9.5 + +# Working from branch-3.9.6 on a clean checkout +cd zookeeper-website +npm ci +npm run ci + +# Stage the build output before we ever switch branches. +rm -rf /tmp/zookeeper-site-build +cp -R build/client /tmp/zookeeper-site-build + +# Stage the Java API docs generated by the Maven build for this release. +cd .. +rm -rf /tmp/zookeeper-apidocs-r${CURRENT} +cp -R /tmp/zookeeper-apidocs-r${CURRENT} +``` + +The fresh `build/client/doc/r${CURRENT}/` directory is the contents of `doc/r3.9.6/` that will be added to `asf-site` in Step 4. + +#### Step 4 — Publish on `asf-site` + +```bash +git checkout asf-site +git clean -fxd + +# 4a. Add the new docs version +mkdir -p content/doc/r${CURRENT} +cp -R /tmp/zookeeper-site-build/doc/r${CURRENT}/. content/doc/r${CURRENT}/ +cp -R /tmp/zookeeper-apidocs-r${CURRENT} content/doc/r${CURRENT}/apidocs + +# 4b. Repoint content/doc/current at the new release. +# `content/doc/current` is a symlink (NOT a directory) whose target is +# the current `r` directory. Update it in place so URLs under +# /doc/current/... resolve to the new release. +ln -sfn r${CURRENT} content/doc/current + +# 4c. Replace the landing pages (everything in content/ except content/doc/). +find content -mindepth 1 -maxdepth 1 ! -name doc -exec rm -rf {} + +find /tmp/zookeeper-site-build -mindepth 1 -maxdepth 1 ! -name doc -exec cp -R {} content/ \; + +git add content +git commit -m "Publish ZooKeeper ${CURRENT}" +git push origin asf-site + +# Cleanup +rm -rf \ + /tmp/zookeeper-site-build \ + /tmp/zookeeper-apidocs-r${CURRENT} +``` + +Once `asf-site` is pushed the live site reflects the release within minutes. + +#### Bumping `CURRENT_VERSION` (canary) + +`CURRENT_VERSION` on `master` is bumped **once per upcoming release**, at the moment the team starts working on that version. + +Files to edit: + +- **[`app/lib/current-version.ts`](app/lib/current-version.ts)** — bump `CURRENT_VERSION` to the new canary (e.g. `"3.9.7"`). +- **[`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts)** — append the version that just shipped to `RAW_RELEASED_DOC_VERSIONS_LIST` (it now has a published `content/doc/r/` directory on `asf-site`), and revisit `LTS_VERSIONS`: the new `CURRENT_VERSION` should be first, and the rest of the list may need entries added, removed, or swapped depending on which lines are still being supported. + +```bash +git checkout master +git pull origin master +git checkout -b bump-current-version + +# Edit the two files described above. + +cd zookeeper-website +npm ci +npm run ci +cd .. + +git commit -am "Bump CURRENT_VERSION canary to 3.9.7" +git push -u origin bump-current-version +# open a PR, merge. +``` + +After this lands, every subsequent docs edit on `master` accumulates under the new canary until the next release branch is cut. + +### Troubleshooting + +#### TypeScript Types Are Broken + +If you see type errors related to React Router's `+types`, regenerate them: + +```bash +npx react-router typegen +``` + +#### Port Already in Use + +If `npm run dev` fails because port 5173 is in use: + +```bash +# Kill the process using the port +lsof -ti:5173 | xargs kill -9 + +# Or change the port in vite.config.ts +``` + +#### Build Fails + +1. **Clear generated files:** + + ```bash + rm -rf build/ node_modules/ .vite/ .react-router/ .source/ + ``` + +2. **Reinstall dependencies:** + + ```bash + npm ci + ``` + +3. **Try building again:** + ```bash + npm run build + ``` + +--- + +## Additional Resources + +- **React Router Documentation**: https://reactrouter.com/ +- **Progressive Enhancement Explained**: https://reactrouter.com/explanation/progressive-enhancement +- **shadcn/ui Components**: https://ui.shadcn.com/ +- **TailwindCSS Docs**: https://tailwindcss.com/ +- **TypeScript Handbook**: https://www.typescriptlang.org/docs/ + +--- + +Built with ❤️ for the Apache ZooKeeper community. From a74fc1578d15db369625cea8e3b780d5010f0297 Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk Date: Thu, 25 Jun 2026 16:46:11 +0200 Subject: [PATCH 06/14] WIP --- zookeeper-website/README.md | 20 +++++++++++++------- zookeeper-website/pom.xml | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/zookeeper-website/README.md b/zookeeper-website/README.md index f3e0e4f3d61..a3c5f6bfbc5 100644 --- a/zookeeper-website/README.md +++ b/zookeeper-website/README.md @@ -231,7 +231,7 @@ zookeeper-website/ Three constants drive the versions surfaced by the site, all defined together for a reason: - **`CURRENT_VERSION`** ([`app/lib/current-version.ts`](app/lib/current-version.ts)) — the version the in-tree MDX corresponds to. On `master` this is the **canary** version that the next release is being prepared for. The docs build serves it from `/doc/r/`. - - **`RAW_RELEASED_DOC_VERSIONS_LIST`** ([`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts)) — every documentation version that has ever been published under `/doc/`. New entries are appended when a release ships, and the list is kept in sync with the `content/doc/r/` directories on `asf-site`. `CURRENT_VERSION` is included at the end so it is exposed through the same machinery. + - **`RAW_RELEASED_DOC_VERSIONS_LIST`** (private literal in [`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts), exported as the Set `RAW_RELEASED_DOC_VERSIONS`) — every documentation version that has ever been published under `/doc/`. New entries are appended when a release ships, and the list is kept in sync with the `content/doc/r/` directories on `asf-site`. `CURRENT_VERSION` is included at the end so it is exposed through the same machinery. - **`LTS_VERSIONS`** ([`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts)) — versions that are pinned to the top of the **Documentation** dropdown in the site navbar. By convention `CURRENT_VERSION` is **first** in this list, followed by the still-supported long-term branches. Everything else from `RAW_RELEASED_DOC_VERSIONS_LIST` is shown afterwards under "Older versions", sorted in descending order. When releasing a new version, both `RAW_RELEASED_DOC_VERSIONS_LIST` and `LTS_VERSIONS` typically need to be touched — see the [release flow](#publishing-a-new-zookeeper-release). For routine non-release edits, neither file changes. @@ -317,6 +317,14 @@ This starts a local development server with: When writing docs content, follow these conventions: +- Each MDX file starts with a YAML frontmatter block declaring at minimum `title` and `description`. These power the page ``, the sidebar label, and the meta description: + + ```mdx + --- + title: "My topic" + description: "Short one-liner." + --- + ``` - Internal doc links are **doc-root-relative** — write `/admin-ops/cli`, not `/docs/...` and not a hardcoded `/doc/r<version>/...`. The version prefix (`/doc/r<version>`) is added automatically at render time (`resolveDocsHref` on the live site, React Router `basename` in an archive). - Images use the static asset path `/docs-images/...` (served from `public/docs-images/`). - Use absolute `https://` URLs for off-site links (e.g. `https://zookeeper.apache.org/`). @@ -487,7 +495,7 @@ The website is integrated into the Apache ZooKeeper Maven build through two POMs The frontend plugin is intentionally bound only to the Maven `site` lifecycle: -- `pre-site` installs Node.js/npm and runs `npm install` +- `pre-site` installs Node.js/npm and runs `npm ci` - `site` runs the website CI command Because of that, the website is built **only during site generation** (`mvn site`) and is skipped during regular Maven lifecycle phases such as `mvn clean install`. @@ -521,8 +529,8 @@ When you run `mvn site`, the website module automatically: - Installed to `target/` directory - Does not affect your system Node/npm installation -3. **Runs `npm install`** to install all dependencies - - Reads from `package.json` +3. **Runs `npm ci`** to install all dependencies + - Reads from `package.json` and pins to `package-lock.json` - Installs to `node_modules/` 4. **Runs a website CI command**: @@ -689,9 +697,7 @@ Done on the `master` branch, merged through normal PRs **before** the release br 1. Confirm `CURRENT_VERSION` in [`app/lib/current-version.ts`](app/lib/current-version.ts) already equals the release being prepared (e.g. `"3.9.6"`). The bump should have happened when work on this version started. If it doesn't, do the bump now as a normal PR (see [Bumping `CURRENT_VERSION`](#bumping-current_version-canary)). 2. Update landing pages that reference the version: `releases`, `news`, anywhere else that calls out the latest version by string. -3. Update `RAW_RELEASED_DOC_VERSIONS_LIST` and `LTS_VERSIONS` in [`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts): - - Append the new version to `RAW_RELEASED_DOC_VERSIONS_LIST` (just before the trailing `CURRENT_VERSION` entry). The list must mirror the `content/doc/r<version>/` directories on `asf-site` once the release publishes. - - Update `LTS_VERSIONS` if this release changes long-term-support membership. By convention `CURRENT_VERSION` is the **first** entry — these versions get pinned to the top of the navbar's **Documentation** dropdown. +3. Revisit `LTS_VERSIONS` in [`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts) if this release changes long-term-support membership. By convention `CURRENT_VERSION` is the **first** entry — these versions get pinned to the top of the navbar's **Documentation** dropdown. `RAW_RELEASED_DOC_VERSIONS_LIST` already terminates with `CURRENT_VERSION`, so the new version is exposed automatically. 4. Update MDX under `app/pages/_docs/docs/_mdx/` for the new release (release notes, version-specific instructions, etc.). 5. Run `npm ci && npm run ci` locally and open a PR. Merge once green. diff --git a/zookeeper-website/pom.xml b/zookeeper-website/pom.xml index b0d38ce7b85..568781dee37 100644 --- a/zookeeper-website/pom.xml +++ b/zookeeper-website/pom.xml @@ -97,7 +97,7 @@ </goals> <phase>pre-site</phase> <configuration> - <arguments>install</arguments> + <arguments>ci</arguments> </configuration> </execution> <execution> From 925967f8685d019ffff93c664f564282c3a23cdf Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk <yurapalamarchuk111@gmail.com> Date: Thu, 25 Jun 2026 16:52:19 +0200 Subject: [PATCH 07/14] Display current version in docs --- zookeeper-website/app/pages/_docs/docs/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zookeeper-website/app/pages/_docs/docs/index.tsx b/zookeeper-website/app/pages/_docs/docs/index.tsx index f161a819f2e..256418539db 100644 --- a/zookeeper-website/app/pages/_docs/docs/index.tsx +++ b/zookeeper-website/app/pages/_docs/docs/index.tsx @@ -38,6 +38,7 @@ import { OlderDocsPicker } from "@/components/docs/older-docs-picker"; import type { MDXComponents } from "mdx/types"; import { getDocsBasePath, resolveDocsHref } from "@/lib/docs-paths"; import { SITE_URL } from "@/lib/site"; +import { CURRENT_VERSION } from "@/lib/current-version"; // Extend default MDX components to include shared UI blocks globally. // Note: We'll override the 'a' component in the renderer to handle route-specific logic @@ -66,7 +67,7 @@ export function baseOptions(): BaseLayoutProps { width={16} height={16} /> - <p>Apache ZooKeeper</p> + <p>Apache ZooKeeper {CURRENT_VERSION}</p> </div> ) }, From d72ede625bf97735ecb53e74d3a0f1ab46ebf2c5 Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk <yurapalamarchuk111@gmail.com> Date: Thu, 25 Jun 2026 17:06:28 +0200 Subject: [PATCH 08/14] Remove archive banner --- zookeeper-website/README.md | 24 ++---- .../components/docs/archive-docs-banner.tsx | 78 ------------------ zookeeper-website/app/lib/docs-archive.ts | 4 - .../app/pages/_docs/docs-layout.tsx | 2 - zookeeper-website/package.json | 1 - zookeeper-website/scripts/build-docs.ts | 68 +++------------ .../unit-tests/archive-docs-banner.test.tsx | 82 ------------------- ...ocs-archive.test.ts => build-docs.test.ts} | 10 +-- 8 files changed, 21 insertions(+), 248 deletions(-) delete mode 100644 zookeeper-website/app/components/docs/archive-docs-banner.tsx delete mode 100644 zookeeper-website/unit-tests/archive-docs-banner.test.tsx rename zookeeper-website/unit-tests/{build-docs-archive.test.ts => build-docs.test.ts} (96%) diff --git a/zookeeper-website/README.md b/zookeeper-website/README.md index a3c5f6bfbc5..e772c773786 100644 --- a/zookeeper-website/README.md +++ b/zookeeper-website/README.md @@ -33,7 +33,7 @@ The official website for Apache ZooKeeper, built with modern web technologies to - [Development Workflow](#development-workflow) - [Testing](#testing) - [Building for Production](#building-for-production) - - [Building Archived Docs](#building-archived-docs) + - [How the Docs Build Works](#how-the-docs-build-works) - [Continuous Integration](#continuous-integration) - [Maven Integration](#maven-integration) - [Deployment](#deployment) @@ -429,7 +429,6 @@ The landing site and the docs are produced as two independent Vite builds (see [ - `npm run build` — builds the current docs (`/doc/r<CURRENT_VERSION>/`), then the landing site (base `/`), then merges the docs into `build/client/doc/r<CURRENT_VERSION>/` and regenerates the full sitemap (`--scope all`). The merged `build/client/` is what `npm run start`, `vite preview`, and the e2e tests serve. - `npm run build:landing` — landing-only build (sets `ZOOKEEPER_BUILD_TARGET=landing`), output at `build/client/` (root pages), and upserts the landing slice of the sitemap (`--scope landing`), keeping the current-docs URLs from the committed `public/sitemap.xml`. - `npm run build:docs` — pure docs build for `CURRENT_VERSION`, output at `build/doc/r<CURRENT_VERSION>/`, and upserts the docs slice of the sitemap (`--scope docs`), keeping landing URLs from the committed base. No CI checks. -- `npm run build:docs-archive` — runs `npm run ci` first, then the same docs build, output at `build/doc/r<CURRENT_VERSION>/`, with the archived-docs banner enabled, for snapshotting `CURRENT_VERSION` (see below). It does not touch the sitemap (archived versions are excluded). The docs version always comes from [`app/lib/current-version.ts`](app/lib/current-version.ts). @@ -443,25 +442,17 @@ Because each docs version and the landing site are separate Vite builds, they ha > Note: React Router cleans the entire `build/` directory at the start of every build, so the orchestrator stashes the docs output outside `build/` before running the landing build, then restores it into `build/client/`. -#### Building Archived Docs +#### How the Docs Build Works -To snapshot a released documentation version, build a versioned website archive: +`npm run build:docs` produces a self-contained docs tree at `build/doc/r<CURRENT_VERSION>/`, ready to copy to `asf-site` at `content/doc/r<CURRENT_VERSION>/`. It validates the output before exiting — every source MDX docs page must have a corresponding HTML page. -```bash -npm run build:docs-archive -``` - -This creates `build/doc/r<CURRENT_VERSION>/`, ready to copy to `asf-site` at `content/doc/r<CURRENT_VERSION>/`. - -Before building the archive, this command runs `npm run ci` without the archive base path. That verifies lint, typecheck, unit tests, the normal production build, and Playwright e2e tests. It then rebuilds with the archive base path and validates the generated docs output, including that every source MDX docs page has a corresponding HTML page. - -The current version's docs are built with this **same** pipeline (`npm run build:docs`); an archive is the same build, just produced before `CURRENT_VERSION` is bumped and with the switch-to-latest banner enabled. Archived and current docs share one versioned route shape: +The docs build uses React Router's `basename` and Vite's `base` to serve docs from `/doc/r<version>/`. URL shape: - `/doc/r3.9.6/` - `/doc/r3.9.6/overview/quick-start` - `/doc/r3.9.6/developer/programmers-guide` -The docs build uses React Router's `basename` and Vite's `base` to serve docs from `/doc/r<version>/`. It uses the docs route set from `app/routes.ts`: the docs catch-all, `api/search`, and `llms-full.txt`. Non-doc archive-local requests such as `/doc/r3.9.6/news` are redirected by the generated `.htaccess` file to the matching live-site path (`/news`). +It uses the docs route set from `app/routes.ts`: the docs catch-all, `api/search`, and `llms-full.txt`. Non-doc archive-local requests such as `/doc/r3.9.6/news` are redirected by the generated `.htaccess` file to the matching live-site path (`/news`). Docs builds set the `ZOOKEEPER_DOCS_ARCHIVE_BASE` env var. Vite turns it into the app `base`, which the browser reads back as `import.meta.env.BASE_URL`. Rule of thumb for the build context: @@ -470,11 +461,9 @@ Docs builds set the `ZOOKEEPER_DOCS_ARCHIVE_BASE` env var. Vite turns it into th `ZOOKEEPER_DOCS_ARCHIVE_BASE` and `import.meta.env.BASE_URL` are the same value in two execution contexts. `ZOOKEEPER_BUILD_TARGET=landing` is the orthogonal signal that selects the landing-only build. -`build:docs-archive` additionally sets `VITE_ZOOKEEPER_DOCS_ARCHIVE_SNAPSHOT=1`. Vite exposes that flag to bundled browser code via `import.meta.env`, and the archive banner reads it directly. The flag is intentionally not a version comparison: archives are built before `CURRENT_VERSION` is bumped, so the outgoing version can still equal the current source version during the archive build. - The docs build script packages the generated docs into a self-contained directory. React Router prerenders docs HTML under `build/client/doc/r<version>/`, while Vite and `public/` assets are emitted at the build root (`build/client/assets/`, `build/client/docs-images/`, `build/client/fonts/`, `build/client/images/`, `build/client/favicon.ico`). The script copies the docs HTML plus those known static assets into `build/doc/r<version>/` so URLs such as `/doc/r3.9.6/assets/...` exist after publishing. If new top-level static folders or asset roots are introduced, update `scripts/build-docs.ts` so they are copied, URL-rewritten if needed, and covered by docs build validation. -Archive output intentionally does not copy the live site's root-only files such as `404.html`, `robots.txt`, `__spa-fallback.html`, or the root `.htaccess`. Archives get their own generated `.htaccess` (described above). The docs build keeps only docs routes, `api/search`, and `llms-full.txt`. +Docs output intentionally does not copy the live site's root-only files such as `404.html`, `robots.txt`, `__spa-fallback.html`, or the root `.htaccess`. Docs builds get their own generated `.htaccess` (described above). The docs build keeps only docs routes, `api/search`, and `llms-full.txt`. #### Continuous Integration @@ -709,7 +698,6 @@ This is the standard ZooKeeper release-branching step (e.g. `branch-3.9` → `br ```bash CURRENT=3.9.6 -OUTGOING=3.9.5 # Working from branch-3.9.6 on a clean checkout cd zookeeper-website diff --git a/zookeeper-website/app/components/docs/archive-docs-banner.tsx b/zookeeper-website/app/components/docs/archive-docs-banner.tsx deleted file mode 100644 index ba78c0bbd44..00000000000 --- a/zookeeper-website/app/components/docs/archive-docs-banner.tsx +++ /dev/null @@ -1,78 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { ArrowRight, X } from "lucide-react"; -import { useEffect, useState } from "react"; -import { CURRENT_VERSION } from "@/lib/current-version"; -import { DOCS_ARCHIVE_SNAPSHOT_ENV } from "@/lib/docs-archive"; -import { Button } from "@/ui/button"; - -const bannerStorageKey = `zookeeper-docs-archive-banner-dismissed:${CURRENT_VERSION}`; - -export function ArchiveDocsBanner() { - // Set only by `build:docs-archive`, never by the current docs build or dev. - const isArchiveDocsView = import.meta.env[DOCS_ARCHIVE_SNAPSHOT_ENV] === "1"; - const [visible, setVisible] = useState(false); - - useEffect(() => { - if (!isArchiveDocsView) { - return; - } - - setVisible(localStorage.getItem(bannerStorageKey) !== "1"); - }, [isArchiveDocsView]); - - if (!isArchiveDocsView || !visible) { - return null; - } - - const dismiss = () => { - localStorage.setItem(bannerStorageKey, "1"); - setVisible(false); - }; - - return ( - <div - role="status" - className="bg-muted text-foreground border-b px-4 py-2 text-sm" - > - <div className="flex items-center gap-4"> - <p className="mx-auto text-center"> - You are viewing the v{CURRENT_VERSION} docs.{" "} - <a - href="/doc/" - className="inline-flex items-center gap-1 font-medium underline underline-offset-4" - > - Switch to latest - <ArrowRight className="h-4 w-4" aria-hidden /> - </a> - </p> - <Button - type="button" - variant="ghost" - size="icon" - className="size-8 shrink-0" - aria-label="Dismiss archived docs notice" - onClick={dismiss} - > - <X className="h-4 w-4" /> - </Button> - </div> - </div> - ); -} diff --git a/zookeeper-website/app/lib/docs-archive.ts b/zookeeper-website/app/lib/docs-archive.ts index 625ca8d0726..6744e37300e 100644 --- a/zookeeper-website/app/lib/docs-archive.ts +++ b/zookeeper-website/app/lib/docs-archive.ts @@ -52,7 +52,3 @@ export function getBuildTarget(): "landing" | undefined { ? "landing" : undefined; } - -// Marks a docs build as an archived snapshot of an older release. The VITE_ -// prefix intentionally exposes it to bundled browser code via import.meta.env. -export const DOCS_ARCHIVE_SNAPSHOT_ENV = "VITE_ZOOKEEPER_DOCS_ARCHIVE_SNAPSHOT"; diff --git a/zookeeper-website/app/pages/_docs/docs-layout.tsx b/zookeeper-website/app/pages/_docs/docs-layout.tsx index d809707cb5e..cadfd4b4a1e 100644 --- a/zookeeper-website/app/pages/_docs/docs-layout.tsx +++ b/zookeeper-website/app/pages/_docs/docs-layout.tsx @@ -18,7 +18,6 @@ import { RootProvider } from "fumadocs-ui/provider/react-router"; import { Outlet } from "react-router"; -import { ArchiveDocsBanner } from "@/components/docs/archive-docs-banner"; import { SearchDialog } from "@/components/docs/search/docs-search"; export default function DocsLayout() { @@ -28,7 +27,6 @@ export default function DocsLayout() { SearchDialog }} > - <ArchiveDocsBanner /> <Outlet /> </RootProvider> ); diff --git a/zookeeper-website/package.json b/zookeeper-website/package.json index f0653c763ed..5b537ab1cfb 100644 --- a/zookeeper-website/package.json +++ b/zookeeper-website/package.json @@ -25,7 +25,6 @@ "test:e2e:debug": "playwright test --debug", "fumadocs-init": "fumadocs-mdx", "generate-sitemap": "tsx scripts/generate-sitemap.ts", - "build:docs-archive": "tsx scripts/build-docs.ts", "ci": "npm run fumadocs-init && npm run lint && npm run typecheck && npm run test:unit:run && npm run build && npx playwright install && npm run test:e2e", "ci-skip-tests": "npm run fumadocs-init && npm run build" }, diff --git a/zookeeper-website/scripts/build-docs.ts b/zookeeper-website/scripts/build-docs.ts index b2b4d80330f..dfc3c4717a4 100644 --- a/zookeeper-website/scripts/build-docs.ts +++ b/zookeeper-website/scripts/build-docs.ts @@ -31,7 +31,6 @@ import { fileURLToPath } from "node:url"; import { CURRENT_VERSION } from "../app/lib/current-version"; import { DOCS_ARCHIVE_BASE_ENV, - DOCS_ARCHIVE_SNAPSHOT_ENV, normalizeDocsArchiveBase } from "../app/lib/docs-archive"; import { formatDocsBase } from "../app/lib/docs-paths"; @@ -66,19 +65,10 @@ export const ROOT_URL_PATTERNS = [ const DOCS_LOCAL_PATHS = "(?:apidocs|assets|docs-images|fonts|images)\\b[^\"')]*|favicon\\.ico"; -interface DocsBuildArgs { - snapshot: boolean; -} - -export function parseDocsBuildArgs( - args = process.argv.slice(2), - scriptName = process.env.npm_lifecycle_event -): DocsBuildArgs { +export function parseDocsBuildArgs(args = process.argv.slice(2)): void { if (args.length > 0) { - throw new Error("Usage: npm run build:docs or npm run build:docs-archive"); + throw new Error("Usage: npm run build:docs"); } - - return { snapshot: scriptName === "build:docs-archive" }; } function runCommand(command: string, args: string[], env: NodeJS.ProcessEnv) { @@ -97,13 +87,6 @@ function runCommand(command: string, args: string[], env: NodeJS.ProcessEnv) { } } -function getWebsiteCheckEnv(): NodeJS.ProcessEnv { - const env = { ...process.env }; - delete env[DOCS_ARCHIVE_BASE_ENV]; - delete env[DOCS_ARCHIVE_SNAPSHOT_ENV]; - return env; -} - async function pathExists(path: string): Promise<boolean> { try { await access(path); @@ -282,17 +265,12 @@ export async function verifyArchiveOutput( // Pure, self-contained docs build for a single version. Produces a versioned // output at build/doc/r<version>/ with its own assets, .htaccess, rewritten -// docs-local URLs, and llms-full.txt. Pass `snapshot: true` for archived -// versions so the "switch to latest" banner shows. -export async function buildDocs( - version: string, - { snapshot = false }: { snapshot?: boolean } = {} -): Promise<string> { +// docs-local URLs, and llms-full.txt. +export async function buildDocs(version: string): Promise<string> { const docsBase = normalizeDocsArchiveBase(formatDocsBase(version)); const env = { ...process.env, - [DOCS_ARCHIVE_BASE_ENV]: docsBase, - ...(snapshot ? { [DOCS_ARCHIVE_SNAPSHOT_ENV]: "1" } : {}) + [DOCS_ARCHIVE_BASE_ENV]: docsBase }; console.log(`Building docs for ${docsBase}`); @@ -308,36 +286,16 @@ export async function buildDocs( } export async function main() { - const { snapshot } = parseDocsBuildArgs(); + parseDocsBuildArgs(); - if (snapshot) { - console.log("Running website checks before building the docs archive"); - runCommand("npm", ["run", "ci"], getWebsiteCheckEnv()); - } - - const version = CURRENT_VERSION; - await buildDocs(version, { snapshot }); - - // The current docs build owns the docs slice of the sitemap. Archive snapshots - // are intentionally excluded, so they never touch it. - if (!snapshot) { - runCommand( - "npm", - ["run", "generate-sitemap", "--", "--scope", "docs"], - process.env - ); - } + await buildDocs(CURRENT_VERSION); - if (snapshot) { - console.log(""); - console.log("Publish with:"); - console.log(` git checkout asf-site`); - console.log(` rm -rf content/doc/r${version}`); - console.log(` mkdir -p content/doc/r${version}`); - console.log( - ` cp -R zookeeper-website/build/doc/r${version}/. content/doc/r${version}/` - ); - } + // The current docs build owns the docs slice of the sitemap. + runCommand( + "npm", + ["run", "generate-sitemap", "--", "--scope", "docs"], + process.env + ); } if (process.argv[1] === fileURLToPath(import.meta.url)) { diff --git a/zookeeper-website/unit-tests/archive-docs-banner.test.tsx b/zookeeper-website/unit-tests/archive-docs-banner.test.tsx deleted file mode 100644 index fb8d3c01e4e..00000000000 --- a/zookeeper-website/unit-tests/archive-docs-banner.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { ArchiveDocsBanner } from "@/components/docs/archive-docs-banner"; -import { CURRENT_VERSION } from "@/lib/current-version"; -import { DOCS_ARCHIVE_SNAPSHOT_ENV } from "@/lib/docs-archive"; - -const storageKey = `zookeeper-docs-archive-banner-dismissed:${CURRENT_VERSION}`; - -function enableArchiveBuild() { - vi.stubEnv(DOCS_ARCHIVE_SNAPSHOT_ENV, "1"); -} - -beforeEach(() => { - localStorage.clear(); -}); - -afterEach(() => { - vi.unstubAllEnvs(); - localStorage.clear(); -}); - -describe("ArchiveDocsBanner", () => { - it("renders nothing outside an archive build", () => { - render(<ArchiveDocsBanner />); - - expect(screen.queryByRole("status")).not.toBeInTheDocument(); - }); - - it("shows the switch-to-latest notice on an archive snapshot", () => { - enableArchiveBuild(); - render(<ArchiveDocsBanner />); - - expect( - screen.getByText(new RegExp(`viewing the v${CURRENT_VERSION} docs`, "i")) - ).toBeInTheDocument(); - expect( - screen.getByRole("link", { name: /switch to latest/i }) - ).toHaveAttribute("href", "/doc/"); - }); - - it("dismisses the banner and remembers the dismissal", async () => { - const user = userEvent.setup(); - enableArchiveBuild(); - render(<ArchiveDocsBanner />); - - expect(screen.getByRole("status")).toBeInTheDocument(); - - await user.click( - screen.getByRole("button", { name: /dismiss archived docs notice/i }) - ); - - expect(screen.queryByRole("status")).not.toBeInTheDocument(); - expect(localStorage.getItem(storageKey)).toBe("1"); - }); - - it("stays hidden when already dismissed", () => { - enableArchiveBuild(); - localStorage.setItem(storageKey, "1"); - render(<ArchiveDocsBanner />); - - expect(screen.queryByRole("status")).not.toBeInTheDocument(); - }); -}); diff --git a/zookeeper-website/unit-tests/build-docs-archive.test.ts b/zookeeper-website/unit-tests/build-docs.test.ts similarity index 96% rename from zookeeper-website/unit-tests/build-docs-archive.test.ts rename to zookeeper-website/unit-tests/build-docs.test.ts index aa4fb9066fe..cb2341af2a8 100644 --- a/zookeeper-website/unit-tests/build-docs-archive.test.ts +++ b/zookeeper-website/unit-tests/build-docs.test.ts @@ -74,14 +74,8 @@ afterEach(async () => { }); describe("parseDocsBuildArgs", () => { - it("uses the current version docs build by default", () => { - expect(parseDocsBuildArgs([])).toEqual({ snapshot: false }); - }); - - it("marks archive builds from the npm script name", () => { - expect(parseDocsBuildArgs([], "build:docs-archive")).toEqual({ - snapshot: true - }); + it("accepts no arguments", () => { + expect(() => parseDocsBuildArgs([])).not.toThrow(); }); it("rejects CLI version overrides", () => { From 5e69e04dab695beea2fbce138b44d86e9994f62a Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk <yurapalamarchuk111@gmail.com> Date: Fri, 26 Jun 2026 11:49:19 +0200 Subject: [PATCH 09/14] Change version to 3.10.0 and fix flaky tests --- zookeeper-website/app/lib/current-version.ts | 2 +- .../app/lib/released-docs-versions.ts | 24 +-------- .../e2e-tests/older-docs-picker.spec.ts | 49 ++++++++----------- zookeeper-website/public/sitemap.xml | 2 +- 4 files changed, 25 insertions(+), 52 deletions(-) diff --git a/zookeeper-website/app/lib/current-version.ts b/zookeeper-website/app/lib/current-version.ts index 06e06d7fdff..c1c6461e016 100644 --- a/zookeeper-website/app/lib/current-version.ts +++ b/zookeeper-website/app/lib/current-version.ts @@ -16,4 +16,4 @@ // limitations under the License. // -export const CURRENT_VERSION = "3.9.10"; +export const CURRENT_VERSION = "3.10.0"; diff --git a/zookeeper-website/app/lib/released-docs-versions.ts b/zookeeper-website/app/lib/released-docs-versions.ts index c56e8fd62e6..53a901005dc 100644 --- a/zookeeper-website/app/lib/released-docs-versions.ts +++ b/zookeeper-website/app/lib/released-docs-versions.ts @@ -115,8 +115,8 @@ export const RAW_RELEASED_DOC_VERSIONS = new Set<ReleasedDocVersion>( export const LTS_VERSIONS: ReleasedDocVersion[] = [ CURRENT_VERSION, - "3.8.6", - "3.7.2" + "3.9.5", + "3.8.6" ]; export function sortVersionsDesc(versions: string[]): string[] { @@ -144,25 +144,5 @@ export function getReleasedDocUrl(version: string): string { export function getReleasedDocVersions(): string[] { const ltsSet = new Set<string>(LTS_VERSIONS); - - if (typeof window !== "undefined") { - const override = window.localStorage.getItem( - "__released_doc_versions_override__" - ); - if (override) { - try { - const parsed = JSON.parse(override); - if ( - Array.isArray(parsed) && - parsed.every((value) => typeof value === "string") - ) { - return sortVersionsDesc(parsed).filter((v) => !ltsSet.has(v)); - } - } catch { - // Ignore invalid test overrides and fall back to build-time data. - } - } - } - return RELEASED_DOC_VERSIONS.filter((v) => !ltsSet.has(v)); } diff --git a/zookeeper-website/e2e-tests/older-docs-picker.spec.ts b/zookeeper-website/e2e-tests/older-docs-picker.spec.ts index c02e392c433..2321fadb28c 100644 --- a/zookeeper-website/e2e-tests/older-docs-picker.spec.ts +++ b/zookeeper-website/e2e-tests/older-docs-picker.spec.ts @@ -18,25 +18,15 @@ import { test, expect } from "@playwright/test"; import { DOCS_ROOT } from "./constants"; +import { + getReleasedDocUrl, + getReleasedDocVersions +} from "../app/lib/released-docs-versions"; -const MOCK_RELEASED_DOC_VERSIONS = ["3.10.0", "3.9.4", "3.9.3"]; -const RELEASED_DOC_VERSIONS_OVERRIDE_KEY = "__released_doc_versions_override__"; - -function expectedReleasedDocUrl(version: string): string { - return `/doc/r${version}/`; -} +const EXPECTED_VERSIONS = getReleasedDocVersions(); test.describe("Older Docs Picker – sidebar", () => { test.beforeEach(async ({ page }) => { - await page.addInitScript( - ({ key, versions }) => { - window.localStorage.setItem(key, JSON.stringify(versions)); - }, - { - key: RELEASED_DOC_VERSIONS_OVERRIDE_KEY, - versions: MOCK_RELEASED_DOC_VERSIONS - } - ); await page.goto(DOCS_ROOT); await page.waitForLoadState("networkidle"); }); @@ -65,7 +55,7 @@ test.describe("Older Docs Picker – sidebar", () => { const options = list.getByRole("option"); await expect(options.first()).toBeVisible(); - await expect(options).toHaveCount(MOCK_RELEASED_DOC_VERSIONS.length); + await expect(options).toHaveCount(EXPECTED_VERSIONS.length); }); test("versions are displayed in descending order", async ({ page }) => { @@ -75,9 +65,7 @@ test.describe("Older Docs Picker – sidebar", () => { await expect(options.first()).toBeVisible(); const texts = await options.allTextContents(); - expect(texts.map((text) => text.trim())).toEqual( - MOCK_RELEASED_DOC_VERSIONS - ); + expect(texts.map((text) => text.trim())).toEqual(EXPECTED_VERSIONS); }); test("each version item links to the correct archive path", async ({ @@ -86,10 +74,10 @@ test.describe("Older Docs Picker – sidebar", () => { await page.getByRole("button", { name: /older docs/i }).click(); const options = page.getByRole("option"); - await expect(options).toHaveCount(MOCK_RELEASED_DOC_VERSIONS.length); - for (let i = 0; i < MOCK_RELEASED_DOC_VERSIONS.length; i++) { + await expect(options).toHaveCount(EXPECTED_VERSIONS.length); + for (let i = 0; i < EXPECTED_VERSIONS.length; i++) { const href = await options.nth(i).getAttribute("href"); - expect(href).toBe(expectedReleasedDocUrl(MOCK_RELEASED_DOC_VERSIONS[i])); + expect(href).toBe(getReleasedDocUrl(EXPECTED_VERSIONS[i])); } }); @@ -104,20 +92,25 @@ test.describe("Older Docs Picker – sidebar", () => { const allOptions = page.getByRole("option"); const totalBefore = await allOptions.count(); - // Type a prefix that matches only a subset of versions - await input.fill("3.9"); + const [major, minor] = EXPECTED_VERSIONS[0].split("."); + const prefix = `${major}.${minor}`; + const expectedMatches = EXPECTED_VERSIONS.filter((v) => + v.startsWith(prefix) + ).length; + + await input.fill(prefix); await page.waitForTimeout(200); const filtered = page.getByRole("option"); const totalAfter = await filtered.count(); - expect(totalBefore).toBe(MOCK_RELEASED_DOC_VERSIONS.length); - expect(totalAfter).toBe(2); + expect(totalBefore).toBe(EXPECTED_VERSIONS.length); + expect(totalAfter).toBe(expectedMatches); // Every remaining option must contain the search term for (let i = 0; i < totalAfter; i++) { const text = await filtered.nth(i).textContent(); - expect(text).toContain("3.9"); + expect(text).toContain(prefix); } }); @@ -138,7 +131,7 @@ test.describe("Older Docs Picker – sidebar", () => { await trigger.click(); const input = page.getByRole("combobox"); - await input.fill("3.9"); + await input.fill("anything"); // Close the popover by pressing Escape await page.keyboard.press("Escape"); diff --git a/zookeeper-website/public/sitemap.xml b/zookeeper-website/public/sitemap.xml index 82a01e7d622..f487fa926a6 100644 --- a/zookeeper-website/public/sitemap.xml +++ b/zookeeper-website/public/sitemap.xml @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://zookeeper.apache.org/</loc></url><url><loc>https://zookeeper.apache.org/bylaws/</loc></url><url><loc>https://zookeeper.apache.org/credits/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/administration/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/best-practices/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/commands/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/communication-using-the-netty-framework/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/configuration-parameters/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/data-file-management/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/administrators-guide/deployment/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/cli/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/dynamic-reconfiguration/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/jmx/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/monitor-and-audit-logs/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/observers-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/quorums/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/quota-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/snapshot-and-restore-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/admin-ops/tools/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/basic-tutorial/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/java-example/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/access-control-using-acls/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/bindings/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/building-blocks-a-guide-to-zookeeper-operations/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/consistency-guarantees/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/data-model/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/gotchas-common-problems-and-troubleshooting/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/pluggable-authentication/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/sessions/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/programmers-guide/watches/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/recipes/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/developer/use-cases/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/miscellaneous/internals/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/overview/quick-start/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.9.10/overview/release-notes/</loc></url><url><loc>https://zookeeper.apache.org/events/</loc></url><url><loc>https://zookeeper.apache.org/irc/</loc></url><url><loc>https://zookeeper.apache.org/mailing-lists/</loc></url><url><loc>https://zookeeper.apache.org/news/</loc></url><url><loc>https://zookeeper.apache.org/releases/</loc></url><url><loc>https://zookeeper.apache.org/security/</loc></url><url><loc>https://zookeeper.apache.org/version-control/</loc></url></urlset> \ No newline at end of file +<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://zookeeper.apache.org/</loc></url><url><loc>https://zookeeper.apache.org/bylaws/</loc></url><url><loc>https://zookeeper.apache.org/credits/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/administration/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/best-practices/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/commands/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/communication-using-the-netty-framework/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/configuration-parameters/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/data-file-management/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/deployment/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/cli/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/dynamic-reconfiguration/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/jmx/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/monitor-and-audit-logs/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/observers-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/quorums/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/quota-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/snapshot-and-restore-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/tools/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/basic-tutorial/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/java-example/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/access-control-using-acls/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/bindings/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/building-blocks-a-guide-to-zookeeper-operations/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/consistency-guarantees/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/data-model/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/gotchas-common-problems-and-troubleshooting/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/pluggable-authentication/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/sessions/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/watches/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/recipes/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/use-cases/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/miscellaneous/internals/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/overview/quick-start/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/overview/release-notes/</loc></url><url><loc>https://zookeeper.apache.org/events/</loc></url><url><loc>https://zookeeper.apache.org/irc/</loc></url><url><loc>https://zookeeper.apache.org/mailing-lists/</loc></url><url><loc>https://zookeeper.apache.org/news/</loc></url><url><loc>https://zookeeper.apache.org/releases/</loc></url><url><loc>https://zookeeper.apache.org/security/</loc></url><url><loc>https://zookeeper.apache.org/version-control/</loc></url></urlset> \ No newline at end of file From 364b3f300dc97ffb1bc8dca65a2e082a0fe23ffc Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk <yurapalamarchuk111@gmail.com> Date: Fri, 26 Jun 2026 13:17:25 +0200 Subject: [PATCH 10/14] Generate developers based on pom.xml --- zookeeper-website/.gitignore | 3 + zookeeper-website/README.md | 15 +- .../pages/_landing/credits/developers.json | 47 ---- .../app/pages/_landing/credits/index.tsx | 48 ++-- .../app/routes/_docs/{docs.tsx => doc.tsx} | 0 zookeeper-website/package.json | 5 +- zookeeper-website/public/sitemap.xml | 1 - zookeeper-website/scripts/build-docs.ts | 6 +- zookeeper-website/scripts/build-site.ts | 3 + .../scripts/extract-developers.d.ts | 28 ++ .../scripts/extract-developers.js | 113 ++++++++ .../unit-tests/extract-developers.test.ts | 255 ++++++++++++++++++ 12 files changed, 435 insertions(+), 89 deletions(-) delete mode 100644 zookeeper-website/app/pages/_landing/credits/developers.json rename zookeeper-website/app/routes/_docs/{docs.tsx => doc.tsx} (100%) delete mode 100644 zookeeper-website/public/sitemap.xml create mode 100644 zookeeper-website/scripts/extract-developers.d.ts create mode 100644 zookeeper-website/scripts/extract-developers.js create mode 100644 zookeeper-website/unit-tests/extract-developers.test.ts diff --git a/zookeeper-website/.gitignore b/zookeeper-website/.gitignore index 583284939ff..072a5536e86 100644 --- a/zookeeper-website/.gitignore +++ b/zookeeper-website/.gitignore @@ -41,3 +41,6 @@ node_modules/ /blob-report/ /playwright/.cache/ /playwright/.auth/ + +# Generated developer list (see scripts/extract-developers.js) +/app/pages/_landing/credits/developers.json diff --git a/zookeeper-website/README.md b/zookeeper-website/README.md index e772c773786..3f9dcba5ec1 100644 --- a/zookeeper-website/README.md +++ b/zookeeper-website/README.md @@ -48,10 +48,11 @@ Most landing pages store content in **Markdown (`.md`)**, and some in **JSON (`. **Examples:** -- `app/pages/_landing/credits/developers.json` - JSON data for developers - `app/pages/_landing/releases/index.tsx` - Releases page - `app/pages/_docs/docs/_mdx/...` - MDX content for documentation +The credits page (`app/pages/_landing/credits/`) is a special case: its developer list is **generated** from the project's parent [`pom.xml`](../pom.xml) `<developers>` section by [`scripts/extract-developers.js`](scripts/extract-developers.js). The generated `developers.json` is gitignored and produced as the first step of both `npm run ci` and `npm run ci-skip-tests`. To edit who appears on `/credits`, update the `<developers>` block in the parent `pom.xml`. + --- ## Development @@ -229,7 +230,6 @@ zookeeper-website/ 7. **Documentation Versions**: Three constants drive the versions surfaced by the site, all defined together for a reason: - - **`CURRENT_VERSION`** ([`app/lib/current-version.ts`](app/lib/current-version.ts)) — the version the in-tree MDX corresponds to. On `master` this is the **canary** version that the next release is being prepared for. The docs build serves it from `/doc/r<CURRENT_VERSION>/`. - **`RAW_RELEASED_DOC_VERSIONS_LIST`** (private literal in [`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts), exported as the Set `RAW_RELEASED_DOC_VERSIONS`) — every documentation version that has ever been published under `/doc/`. New entries are appended when a release ships, and the list is kept in sync with the `content/doc/r<version>/` directories on `asf-site`. `CURRENT_VERSION` is included at the end so it is exposed through the same machinery. - **`LTS_VERSIONS`** ([`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts)) — versions that are pinned to the top of the **Documentation** dropdown in the site navbar. By convention `CURRENT_VERSION` is **first** in this list, followed by the still-supported long-term branches. Everything else from `RAW_RELEASED_DOC_VERSIONS_LIST` is shown afterwards under "Older versions", sorted in descending order. @@ -325,6 +325,7 @@ When writing docs content, follow these conventions: description: "Short one-liner." --- ``` + - Internal doc links are **doc-root-relative** — write `/admin-ops/cli`, not `/docs/...` and not a hardcoded `/doc/r<version>/...`. The version prefix (`/doc/r<version>`) is added automatically at render time (`resolveDocsHref` on the live site, React Router `basename` in an archive). - Images use the static asset path `/docs-images/...` (served from `public/docs-images/`). - Use absolute `https://` URLs for off-site links (e.g. `https://zookeeper.apache.org/`). @@ -430,6 +431,11 @@ The landing site and the docs are produced as two independent Vite builds (see [ - `npm run build:landing` — landing-only build (sets `ZOOKEEPER_BUILD_TARGET=landing`), output at `build/client/` (root pages), and upserts the landing slice of the sitemap (`--scope landing`), keeping the current-docs URLs from the committed `public/sitemap.xml`. - `npm run build:docs` — pure docs build for `CURRENT_VERSION`, output at `build/doc/r<CURRENT_VERSION>/`, and upserts the docs slice of the sitemap (`--scope docs`), keeping landing URLs from the committed base. No CI checks. +Two code-generation steps feed the build: + +- **`npm run extract-developers`** writes `app/pages/_landing/credits/developers.json` from the parent [`pom.xml`](../pom.xml). It is invoked automatically by any landing-site build: `build:landing` runs it before Vite, and `build` runs it inside [`scripts/build-site.ts`](scripts/build-site.ts) before the landing step. `build:docs` does not need it (no credits route in the docs build). +- **`npm run fumadocs-init`** writes the fumadocs MDX index to `.source/`. Vite's `fumadocs-mdx/vite` plugin regenerates `.source/` at build/test time, so this step is **not** needed by any build commands. It **is** needed by `lint`, because eslint's `import/no-unresolved` resolver reads the filesystem directly and will fail on the two `@/.source` imports if `.source/` does not exist. + The docs version always comes from [`app/lib/current-version.ts`](app/lib/current-version.ts). `CURRENT_VERSION` on the **`master`** branch is **not** a released version — it is the **canary** for the next release that is being worked on. The actual released version is whatever sits at `content/doc/current/` on `asf-site`. `CURRENT_VERSION` is bumped on `master` as soon as work on the next version begins (see [Publishing a New ZooKeeper Release](#publishing-a-new-zookeeper-release)); from that point on, edits to docs on `master` accumulate under the new canary value and ship together when that release cuts. @@ -461,7 +467,7 @@ Docs builds set the `ZOOKEEPER_DOCS_ARCHIVE_BASE` env var. Vite turns it into th `ZOOKEEPER_DOCS_ARCHIVE_BASE` and `import.meta.env.BASE_URL` are the same value in two execution contexts. `ZOOKEEPER_BUILD_TARGET=landing` is the orthogonal signal that selects the landing-only build. -The docs build script packages the generated docs into a self-contained directory. React Router prerenders docs HTML under `build/client/doc/r<version>/`, while Vite and `public/` assets are emitted at the build root (`build/client/assets/`, `build/client/docs-images/`, `build/client/fonts/`, `build/client/images/`, `build/client/favicon.ico`). The script copies the docs HTML plus those known static assets into `build/doc/r<version>/` so URLs such as `/doc/r3.9.6/assets/...` exist after publishing. If new top-level static folders or asset roots are introduced, update `scripts/build-docs.ts` so they are copied, URL-rewritten if needed, and covered by docs build validation. +The docs build script packages the generated docs into a self-contained directory. React Router prerenders docs HTML under `build/client/doc/r<version>/`, while Vite and `public/` assets are emitted at the build root (`build/client/assets/`, `build/client/docs-images/`, `build/client/fonts/`, `build/client/images/`, `build/client/favicon.ico`). The script copies the docs HTML plus those known static assets into `build/doc/r<version>/` so URLs such as `/doc/r3.9.6/assets/...` exist after publishing, then deletes the intermediate `build/client/` so `build/doc/r<version>/` is the only output. If new top-level static folders or asset roots are introduced, update `scripts/build-docs.ts` so they are copied, URL-rewritten if needed, and covered by docs build validation. Docs output intentionally does not copy the live site's root-only files such as `404.html`, `robots.txt`, `__spa-fallback.html`, or the root `.htaccess`. Docs builds get their own generated `.htaccess` (described above). The docs build keeps only docs routes, `api/search`, and `llms-full.txt`. @@ -527,7 +533,7 @@ When you run `mvn site`, the website module automatically: - With test skipping (`mvn site -DskipTests`): `npm run ci-skip-tests` `npm run ci` executes: - - `npm run fumadocs-init` - Initialize Fumadocs + - `npm run fumadocs-init` - Intialize Fumadocs artifcats (`.source/`), so linter doesn't complain - `npm run lint` - ESLint code quality checks - `npm run typecheck` - TypeScript type checking - `npm run test:unit:run` - Vitest unit tests @@ -536,7 +542,6 @@ When you run `mvn site`, the website module automatically: - `npm run test:e2e` - Playwright e2e tests `npm run ci-skip-tests` executes: - - `npm run fumadocs-init` - Initialize Fumadocs - `npm run build` - Production build 5. **Build Output**: Generated files are in `build/` directory diff --git a/zookeeper-website/app/pages/_landing/credits/developers.json b/zookeeper-website/app/pages/_landing/credits/developers.json deleted file mode 100644 index f4a77670e8c..00000000000 --- a/zookeeper-website/app/pages/_landing/credits/developers.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "pmc": [ - { "id": "tdunning", "name": "Ted Dunning", "organization": "MapR Technologies", "timezone": "-8" }, - { "id": "camille", "name": "Camille Fournier", "organization": "RentTheRunway", "timezone": "-5" }, - { "id": "phunt", "name": "Patrick Hunt", "organization": "Cloudera Inc.", "timezone": "-8" }, - { "id": "fpj", "name": "Flavio Junqueira", "organization": "Confluent", "timezone": "+0" }, - { "id": "ivank", "name": "Ivan Kelly", "organization": "Midokura", "timezone": "+2" }, - { "id": "mahadev", "name": "Mahadev Konar", "organization": "Hortonworks Inc.", "timezone": "-8" }, - { "id": "michim", "name": "Michi Mutsuzaki", "organization": "Nicira", "timezone": "-8" }, - { "id": "cnauroth", "name": "Chris Nauroth", "organization": "Hortonworks Inc.", "timezone": "-8" }, - { "id": "breed", "name": "Benjamin Reed", "organization": "Facebook", "timezone": "-8" }, - { "id": "henry", "name": "Henry Robinson", "organization": "Cloudera Inc.", "timezone": "-8" }, - { "id": "rgs", "name": "Raul Gutierrez Segales", "organization": "Pinterest", "timezone": "-8" }, - { "id": "rakeshr", "name": "Rakesh Radhakrishnan", "organization": "Intel", "timezone": "+5:30" }, - { "id": "hanm", "name": "Michael Han", "organization": "Twitter", "timezone": "-8" }, - { "id": "andor", "name": "Andor Molnar", "organization": "Cloudera Inc.", "timezone": "+1" }, - { "id": "eolivelli", "name": "Enrico Olivelli", "organization": "Diennea", "timezone": "+1" }, - { "id": "symat", "name": "Mate Szalay-Beko", "organization": "Cloudera Inc.", "timezone": "+1" } - ], - "committers": [ - { "id": "camille", "name": "Camille Fournier", "organization": "RentTheRunway", "timezone": "-5" }, - { "id": "phunt", "name": "Patrick Hunt", "organization": "Cloudera Inc.", "timezone": "-8" }, - { "id": "fpj", "name": "Flavio Junqueira", "organization": "Confluent", "timezone": "+1" }, - { "id": "cnauroth", "name": "Chris Nauroth", "organization": "Hortonworks Inc.", "timezone": "-8" }, - { "id": "mahadev", "name": "Mahadev Konar", "organization": "Hortonworks Inc.", "timezone": "-8" }, - { "id": "gkesavan", "name": "Giridharan Kesavan", "organization": "Hortonworks Inc.", "timezone": "-8" }, - { "id": "akornev", "name": "Andrew Kornev", "organization": "", "timezone": "" }, - { "id": "michim", "name": "Michi Mutsuzaki", "organization": "Nicira", "timezone": "-8" }, - { "id": "breed", "name": "Benjamin Reed", "organization": "Facebook", "timezone": "-8" }, - { "id": "henry", "name": "Henry Robinson", "organization": "Cloudera Inc.", "timezone": "-8" }, - { "id": "shralex", "name": "Alex Shraer", "organization": "Apple", "timezone": "-8" }, - { "id": "thawan", "name": "Thawan Kooburat", "organization": "Facebook", "timezone": "-8" }, - { "id": "rakeshr", "name": "Rakesh Radhakrishnan", "organization": "Intel", "timezone": "+5:30" }, - { "id": "hdeng", "name": "Hongchao Deng", "organization": "CoreOS", "timezone": "-8" }, - { "id": "rgs", "name": "Raul Gutierrez Segales", "organization": "Pinterest", "timezone": "-8" }, - { "id": "hanm", "name": "Michael Han", "organization": "Twitter", "timezone": "-8" }, - { "id": "arshad", "name": "Mohammad Arshad", "organization": "Huawei", "timezone": "+5:30" }, - { "id": "afine", "name": "Abraham Fine", "organization": "IFTTT", "timezone": "-8" }, - { "id": "andor", "name": "Andor Molnar", "organization": "Cloudera Inc.", "timezone": "+1" }, - { "id": "fangmin", "name": "Fangmin Lyu", "organization": "Facebook", "timezone": "-8" }, - { "id": "eolivelli", "name": "Enrico Olivelli", "organization": "Diennea", "timezone": "+1" }, - { "id": "nkalmar", "name": "Norbert Kalmar", "organization": "Cloudera", "timezone": "+1" }, - { "id": "enixon", "name": "Brian Nixon", "organization": "Facebook", "timezone": "-8" }, - { "id": "symat", "name": "Mate Szalay-Beko", "organization": "Cloudera Inc.", "timezone": "+1" }, - { "id": "ddiederen", "name": "Damien Diederen", "organization": "Crosstwine Labs", "timezone": "+1" } - ] -} diff --git a/zookeeper-website/app/pages/_landing/credits/index.tsx b/zookeeper-website/app/pages/_landing/credits/index.tsx index 50d15ee9066..8ed130dc95f 100644 --- a/zookeeper-website/app/pages/_landing/credits/index.tsx +++ b/zookeeper-website/app/pages/_landing/credits/index.tsx @@ -16,21 +16,16 @@ // limitations under the License. // -import data from "./developers.json"; +import developers from "./developers.json"; -interface Member { +interface Developer { id: string; name: string; - organization: string; + email: string; timezone: string; } -interface Credits { - pmc: Member[]; - committers: Member[]; -} - -function MemberTable({ members }: { members: Member[] }) { +function DeveloperTable({ developers }: { developers: Developer[] }) { return ( <div className="border-border my-8 w-full overflow-x-auto rounded-lg border"> <table className="mt-0 mb-0 w-full table-fixed border-collapse text-sm"> @@ -38,22 +33,20 @@ function MemberTable({ members }: { members: Member[] }) { <tr className="border-border border-b"> <th className="px-4 py-3 text-left font-semibold">Username</th> <th className="px-4 py-3 text-left font-semibold">Name</th> - <th className="px-4 py-3 text-left font-semibold">Organization</th> + <th className="px-4 py-3 text-left font-semibold">Email</th> <th className="px-4 py-3 text-left font-semibold">Time Zone</th> </tr> </thead> <tbody> - {members.map((member) => ( + {developers.map((developer) => ( <tr - key={member.id} + key={developer.id} className="border-border hover:bg-muted/50 border-b transition-colors" > - <td className="px-4 py-3 align-top">{member.id}</td> - <td className="px-4 py-3 align-top">{member.name || "-"}</td> - <td className="px-4 py-3 align-top"> - {member.organization || "-"} - </td> - <td className="px-4 py-3 align-top">{member.timezone || "-"}</td> + <td className="px-4 py-3 align-top">{developer.id}</td> + <td className="px-4 py-3 align-top">{developer.name}</td> + <td className="px-4 py-3 align-top">{developer.email}</td> + <td className="px-4 py-3 align-top">{developer.timezone}</td> </tr> ))} </tbody> @@ -63,8 +56,6 @@ function MemberTable({ members }: { members: Member[] }) { } export function CreditsPage() { - const credits = data as Credits; - return ( <div className="container mx-auto px-4 py-12"> <article className="prose prose-slate dark:prose-invert max-w-none"> @@ -79,24 +70,15 @@ export function CreditsPage() { </p> <h2 className="mt-12 mb-4 scroll-mt-28 text-3xl font-semibold tracking-tight md:text-4xl"> - PMC Members - </h2> - - <p className="mb-4 text-base leading-7"> - ZooKeeper's active PMC members are listed below. - </p> - - <MemberTable members={credits.pmc} /> - - <h2 className="mt-12 mb-4 scroll-mt-28 text-3xl font-semibold tracking-tight md:text-4xl"> - Committers + Developers </h2> <p className="mb-4 text-base leading-7"> - ZooKeeper's active committers are listed below. + ZooKeeper's developers, as listed in the project's parent{" "} + <code>pom.xml</code>, are shown below. </p> - <MemberTable members={credits.committers} /> + <DeveloperTable developers={developers} /> <h2 className="mt-12 mb-4 scroll-mt-28 text-3xl font-semibold tracking-tight md:text-4xl"> Contributors diff --git a/zookeeper-website/app/routes/_docs/docs.tsx b/zookeeper-website/app/routes/_docs/doc.tsx similarity index 100% rename from zookeeper-website/app/routes/_docs/docs.tsx rename to zookeeper-website/app/routes/_docs/doc.tsx diff --git a/zookeeper-website/package.json b/zookeeper-website/package.json index 5b537ab1cfb..e5774491a65 100644 --- a/zookeeper-website/package.json +++ b/zookeeper-website/package.json @@ -7,7 +7,7 @@ }, "scripts": { "build": "tsx scripts/build-site.ts", - "build:landing": "npm run fumadocs-init && ZOOKEEPER_BUILD_TARGET=landing react-router build && npm run generate-sitemap -- --scope landing", + "build:landing": "npm run extract-developers && ZOOKEEPER_BUILD_TARGET=landing react-router build && npm run generate-sitemap -- --scope landing", "build:docs": "tsx scripts/build-docs.ts", "dev": "react-router dev", "start": "vite preview --port 5173", @@ -25,8 +25,9 @@ "test:e2e:debug": "playwright test --debug", "fumadocs-init": "fumadocs-mdx", "generate-sitemap": "tsx scripts/generate-sitemap.ts", + "extract-developers": "node scripts/extract-developers.js", "ci": "npm run fumadocs-init && npm run lint && npm run typecheck && npm run test:unit:run && npm run build && npx playwright install && npm run test:e2e", - "ci-skip-tests": "npm run fumadocs-init && npm run build" + "ci-skip-tests": "npm run build" }, "dependencies": { "@hookform/resolvers": "^3.10.0", diff --git a/zookeeper-website/public/sitemap.xml b/zookeeper-website/public/sitemap.xml deleted file mode 100644 index f487fa926a6..00000000000 --- a/zookeeper-website/public/sitemap.xml +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://zookeeper.apache.org/</loc></url><url><loc>https://zookeeper.apache.org/bylaws/</loc></url><url><loc>https://zookeeper.apache.org/credits/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/administration/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/best-practices/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/commands/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/communication-using-the-netty-framework/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/configuration-parameters/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/data-file-management/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/administrators-guide/deployment/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/cli/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/dynamic-reconfiguration/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/jmx/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/monitor-and-audit-logs/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/observers-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/quorums/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/quota-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/snapshot-and-restore-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/admin-ops/tools/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/basic-tutorial/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/java-example/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/access-control-using-acls/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/bindings/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/building-blocks-a-guide-to-zookeeper-operations/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/consistency-guarantees/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/data-model/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/gotchas-common-problems-and-troubleshooting/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/pluggable-authentication/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/sessions/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/programmers-guide/watches/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/recipes/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/developer/use-cases/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/miscellaneous/internals/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/overview/quick-start/</loc></url><url><loc>https://zookeeper.apache.org/doc/r3.10.0/overview/release-notes/</loc></url><url><loc>https://zookeeper.apache.org/events/</loc></url><url><loc>https://zookeeper.apache.org/irc/</loc></url><url><loc>https://zookeeper.apache.org/mailing-lists/</loc></url><url><loc>https://zookeeper.apache.org/news/</loc></url><url><loc>https://zookeeper.apache.org/releases/</loc></url><url><loc>https://zookeeper.apache.org/security/</loc></url><url><loc>https://zookeeper.apache.org/version-control/</loc></url></urlset> \ No newline at end of file diff --git a/zookeeper-website/scripts/build-docs.ts b/zookeeper-website/scripts/build-docs.ts index dfc3c4717a4..ebff5794f8d 100644 --- a/zookeeper-website/scripts/build-docs.ts +++ b/zookeeper-website/scripts/build-docs.ts @@ -274,7 +274,6 @@ export async function buildDocs(version: string): Promise<string> { }; console.log(`Building docs for ${docsBase}`); - runCommand("npm", ["run", "fumadocs-init"], env); runCommand("npx", ["react-router", "build"], env); const outputDir = await copyDocsOutput(version); @@ -296,6 +295,11 @@ export async function main() { ["run", "generate-sitemap", "--", "--scope", "docs"], process.env ); + + // react-router build emits to build/client/, then buildDocs() assembles the + // self-contained deliverable at build/doc/r<v>/. The leftover build/client/ + // is just intermediate output — drop it so build/doc/ is the only root. + await rm(BUILD_CLIENT_DIR, { recursive: true, force: true }); } if (process.argv[1] === fileURLToPath(import.meta.url)) { diff --git a/zookeeper-website/scripts/build-site.ts b/zookeeper-website/scripts/build-site.ts index 28b6b2b075c..f046c3cb322 100644 --- a/zookeeper-website/scripts/build-site.ts +++ b/zookeeper-website/scripts/build-site.ts @@ -60,6 +60,9 @@ export async function main() { await rm(stashDir, { recursive: true, force: true }); await cp(docsOutputDir, stashDir, { recursive: true }); + console.log("Extracting developers from parent pom.xml"); + runCommand("npm", ["run", "extract-developers"], process.env); + console.log("Building landing site"); runCommand("npx", ["react-router", "build"], { ...process.env, diff --git a/zookeeper-website/scripts/extract-developers.d.ts b/zookeeper-website/scripts/extract-developers.d.ts new file mode 100644 index 00000000000..bd4d7e47598 --- /dev/null +++ b/zookeeper-website/scripts/extract-developers.d.ts @@ -0,0 +1,28 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export interface Developer { + id: string; + name: string; + email: string; + timezone: string; +} + +export function extractField(block: string, fieldName: string): string; +export function parseDevelopers(pomContent: string): Developer[]; +export function main(): void; diff --git a/zookeeper-website/scripts/extract-developers.js b/zookeeper-website/scripts/extract-developers.js new file mode 100644 index 00000000000..c3a6835577a --- /dev/null +++ b/zookeeper-website/scripts/extract-developers.js @@ -0,0 +1,113 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +/** + * Extracts a field value from a developer XML block + * @param {string} block - The XML block containing developer information + * @param {string} fieldName - The name of the field to extract + * @returns {string} The field value or '-' if not present + */ +export function extractField(block, fieldName) { + const fieldRegex = new RegExp(`<${fieldName}>(.*?)<\/${fieldName}>`, 's'); + const fieldMatch = block.match(fieldRegex); + return fieldMatch ? fieldMatch[1].trim() : '-'; +} + +/** + * Parses developers from POM XML content + * @param {string} pomContent - The content of the pom.xml file + * @returns {Array<Object>} Array of developer objects + * @throws {Error} If no developers section is found + */ +export function parseDevelopers(pomContent) { + // Extract developers using regex + const developersMatch = pomContent.match(/<developers>([\s\S]*?)<\/developers>/); + if (!developersMatch) { + throw new Error('No developers section found in pom.xml'); + } + + const developersXml = developersMatch[1]; + // Match each developer block + const developerBlockRegex = /<developer>([\s\S]*?)<\/developer>/gs; + + const developers = []; + let match; + + while ((match = developerBlockRegex.exec(developersXml)) !== null) { + const block = match[1]; + + const id = extractField(block, 'id'); + const name = extractField(block, 'name'); + const email = extractField(block, 'email'); + const timezone = extractField(block, 'timezone'); + + developers.push({ + id, + name, + email, + timezone, + }); + } + + return developers; +} + +/** + * Main function to extract developers and write to JSON file + */ +export function main() { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + // Read the parent pom.xml + const pomPath = path.join(__dirname, '..', '..', 'pom.xml'); + const pomContent = fs.readFileSync(pomPath, 'utf-8'); + + let developers; + try { + developers = parseDevelopers(pomContent); + } catch (error) { + console.error(error.message); + process.exit(1); + } + + console.log(`Extracted ${developers.length} developers from pom.xml`); + + // Write to JSON file + const outputPath = path.join( + __dirname, + '..', + 'app', + 'pages', + '_landing', + 'credits', + 'developers.json', + ); + fs.writeFileSync(outputPath, JSON.stringify(developers, null, 2)); + + console.log(`Developers data written to ${outputPath}`); +} + +// Run main if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/zookeeper-website/unit-tests/extract-developers.test.ts b/zookeeper-website/unit-tests/extract-developers.test.ts new file mode 100644 index 00000000000..ea55dbfe375 --- /dev/null +++ b/zookeeper-website/unit-tests/extract-developers.test.ts @@ -0,0 +1,255 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { describe, it, expect } from "vitest"; +import { + extractField, + parseDevelopers +} from "../scripts/extract-developers.js"; + +describe("extract-developers script", () => { + const createPomXml = ( + developersXml: string + ) => `<?xml version="1.0" encoding="UTF-8"?> +<project> + <developers> +${developersXml} + </developers> +</project>`; + + describe("extractField", () => { + it("should extract existing field value", () => { + const block = "<id>johndoe</id><name>John Doe</name>"; + expect(extractField(block, "id")).toBe("johndoe"); + expect(extractField(block, "name")).toBe("John Doe"); + }); + + it('should return "-" for missing field', () => { + const block = "<id>johndoe</id><email>john@example.com</email>"; + expect(extractField(block, "name")).toBe("-"); + expect(extractField(block, "timezone")).toBe("-"); + }); + + it("should trim whitespace from field values", () => { + const block = "<id> johndoe </id><name> John Doe </name>"; + expect(extractField(block, "id")).toBe("johndoe"); + expect(extractField(block, "name")).toBe("John Doe"); + }); + + it("should handle multiline field values", () => { + const block = "<name>John\nDoe</name>"; + const result = extractField(block, "name"); + expect(result).toContain("John"); + expect(result).toContain("Doe"); + }); + }); + + describe("parseDevelopers", () => { + it("should extract developers with all fields present", () => { + const pomXml = createPomXml(` <developer> + <id>johndoe</id> + <name>John Doe</name> + <email>johndoe@apache.org</email> + <timezone>-8</timezone> + </developer> + <developer> + <id>janedoe</id> + <name>Jane Doe</name> + <email>janedoe@apache.org</email> + <timezone>+1</timezone> + </developer>`); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(2); + expect(developers[0]).toEqual({ + id: "johndoe", + name: "John Doe", + email: "johndoe@apache.org", + timezone: "-8" + }); + expect(developers[1]).toEqual({ + id: "janedoe", + name: "Jane Doe", + email: "janedoe@apache.org", + timezone: "+1" + }); + }); + + it("should handle missing name field", () => { + const pomXml = createPomXml(` <developer> + <id>tianjy</id> + <email>tianjy@apache.org</email> + <timezone>+8</timezone> + </developer>`); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0]).toEqual({ + id: "tianjy", + name: "-", + email: "tianjy@apache.org", + timezone: "+8" + }); + }); + + it("should handle missing email field", () => { + const pomXml = createPomXml(` <developer> + <id>testuser</id> + <name>Test User</name> + <timezone>+0</timezone> + </developer>`); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0]).toEqual({ + id: "testuser", + name: "Test User", + email: "-", + timezone: "+0" + }); + }); + + it("should handle multiple missing fields", () => { + const pomXml = createPomXml(` <developer> + <id>minimaluser</id> + <email>minimal@apache.org</email> + </developer>`); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0]).toEqual({ + id: "minimaluser", + name: "-", + email: "minimal@apache.org", + timezone: "-" + }); + }); + + it("should handle mixed complete and incomplete developer entries", () => { + const pomXml = createPomXml(` <developer> + <id>complete</id> + <name>Complete User</name> + <email>complete@apache.org</email> + <timezone>+1</timezone> + </developer> + <developer> + <id>incomplete</id> + <email>incomplete@apache.org</email> + <timezone>+2</timezone> + </developer> + <developer> + <id>another</id> + <name>Another User</name> + <email>another@apache.org</email> + <timezone>-5</timezone> + </developer>`); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(3); + expect(developers[0]).toEqual({ + id: "complete", + name: "Complete User", + email: "complete@apache.org", + timezone: "+1" + }); + expect(developers[1]).toEqual({ + id: "incomplete", + name: "-", + email: "incomplete@apache.org", + timezone: "+2" + }); + expect(developers[2]).toEqual({ + id: "another", + name: "Another User", + email: "another@apache.org", + timezone: "-5" + }); + }); + + it("should trim whitespace from field values", () => { + const pomXml = createPomXml(` <developer> + <id> spacey </id> + <name> Spacey User </name> + <email> spacey@apache.org </email> + <timezone> +3 </timezone> + </developer>`); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0]).toEqual({ + id: "spacey", + name: "Spacey User", + email: "spacey@apache.org", + timezone: "+3" + }); + }); + + it("should handle fields in different order", () => { + const pomXml = createPomXml(` <developer> + <timezone>+5</timezone> + <name>Reordered User</name> + <id>reordered</id> + <email>reordered@apache.org</email> + </developer>`); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0]).toEqual({ + id: "reordered", + name: "Reordered User", + email: "reordered@apache.org", + timezone: "+5" + }); + }); + + it("should handle empty developers section", () => { + const pomXml = createPomXml(""); + + const developers = parseDevelopers(pomXml); + expect(developers).toHaveLength(0); + }); + + it("should throw when developers section is missing", () => { + expect(() => parseDevelopers("<project></project>")).toThrow( + /No developers section found/ + ); + }); + + it("should handle special characters in field values", () => { + const pomXml = createPomXml(` <developer> + <id>special&user</id> + <name>User <Name></name> + <email>user@example.org</email> + <timezone>+0</timezone> + </developer>`); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0].id).toBe("special&user"); + expect(developers[0].name).toBe("User <Name>"); + }); + }); +}); From 37314965b7ba1219cfe3d748d0689cdec88a54d0 Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk <yurapalamarchuk111@gmail.com> Date: Fri, 26 Jun 2026 13:17:46 +0200 Subject: [PATCH 11/14] Optimize build process --- zookeeper-website/.gitignore | 3 +++ zookeeper-website/README.md | 4 ++-- zookeeper-website/app/routes.ts | 6 +++--- zookeeper-website/app/routes/_docs/doc-redirect.tsx | 9 +++++++-- zookeeper-website/app/routes/_docs/doc.tsx | 4 ++-- zookeeper-website/package.json | 2 +- zookeeper-website/scripts/build-docs.ts | 7 ------- zookeeper-website/scripts/generate-sitemap.ts | 6 +++--- 8 files changed, 21 insertions(+), 20 deletions(-) diff --git a/zookeeper-website/.gitignore b/zookeeper-website/.gitignore index 072a5536e86..66bf0a90734 100644 --- a/zookeeper-website/.gitignore +++ b/zookeeper-website/.gitignore @@ -44,3 +44,6 @@ node_modules/ # Generated developer list (see scripts/extract-developers.js) /app/pages/_landing/credits/developers.json + +# Generated sitemap (see scripts/generate-sitemap.ts) +/public/sitemap.xml diff --git a/zookeeper-website/README.md b/zookeeper-website/README.md index 3f9dcba5ec1..03a8402b20b 100644 --- a/zookeeper-website/README.md +++ b/zookeeper-website/README.md @@ -428,8 +428,8 @@ Generated files are located under the `build/` directory. The landing site and the docs are produced as two independent Vite builds (see [One Codebase, Two Independent Builds](#key-principles)). `npm run build` orchestrates them via [`scripts/build-site.ts`](scripts/build-site.ts): - `npm run build` — builds the current docs (`/doc/r<CURRENT_VERSION>/`), then the landing site (base `/`), then merges the docs into `build/client/doc/r<CURRENT_VERSION>/` and regenerates the full sitemap (`--scope all`). The merged `build/client/` is what `npm run start`, `vite preview`, and the e2e tests serve. -- `npm run build:landing` — landing-only build (sets `ZOOKEEPER_BUILD_TARGET=landing`), output at `build/client/` (root pages), and upserts the landing slice of the sitemap (`--scope landing`), keeping the current-docs URLs from the committed `public/sitemap.xml`. -- `npm run build:docs` — pure docs build for `CURRENT_VERSION`, output at `build/doc/r<CURRENT_VERSION>/`, and upserts the docs slice of the sitemap (`--scope docs`), keeping landing URLs from the committed base. No CI checks. +- `npm run build:landing` — landing-only build (sets `ZOOKEEPER_BUILD_TARGET=landing`), output at `build/client/` (root pages). +- `npm run build:docs` — pure docs build for `CURRENT_VERSION`, output at `build/doc/r<CURRENT_VERSION>/`. No CI checks. Two code-generation steps feed the build: diff --git a/zookeeper-website/app/routes.ts b/zookeeper-website/app/routes.ts index 46884abddb4..57ba702aa2b 100644 --- a/zookeeper-website/app/routes.ts +++ b/zookeeper-website/app/routes.ts @@ -53,8 +53,8 @@ const docRedirectRoute = route("doc", "routes/_docs/doc-redirect.tsx"); const docsBuildRoutes = [ layout("./pages/_docs/docs-layout.tsx", [ - index("routes/_docs/docs.tsx", { id: "docs-index" }), - route("*", "routes/_docs/docs.tsx", { id: "docs-splat" }) + index("routes/_docs/doc.tsx", { id: "docs-index" }), + route("*", "routes/_docs/doc.tsx", { id: "docs-splat" }) ]), route("api/search", "routes/_api/search.ts"), route("llms-full.txt", "routes/_api/llms-full.ts") @@ -69,7 +69,7 @@ const devCombinedRoutes = [ landingRoutes, docRedirectRoute, layout("./pages/_docs/docs-layout.tsx", [ - route(`doc/r${CURRENT_VERSION}/*`, "routes/_docs/docs.tsx") + route(`doc/r${CURRENT_VERSION}/*`, "routes/_docs/doc.tsx") ]), route("api/search", "routes/_api/search.ts"), route("llms-full.txt", "routes/_api/llms-full.ts") diff --git a/zookeeper-website/app/routes/_docs/doc-redirect.tsx b/zookeeper-website/app/routes/_docs/doc-redirect.tsx index c2cab7f9c0f..2ca1855b637 100644 --- a/zookeeper-website/app/routes/_docs/doc-redirect.tsx +++ b/zookeeper-website/app/routes/_docs/doc-redirect.tsx @@ -16,9 +16,14 @@ // limitations under the License. // -import { Navigate } from "react-router"; +import { redirect } from "react-router"; import { CURRENT_DOCS_PATH } from "@/lib/docs-paths"; +// /doc -> /doc/r<CURRENT_VERSION>/. +export function loader() { + return redirect(`${CURRENT_DOCS_PATH}/`); +} + export default function DocRedirect() { - return <Navigate to={`${CURRENT_DOCS_PATH}/`} replace />; + return null; } diff --git a/zookeeper-website/app/routes/_docs/doc.tsx b/zookeeper-website/app/routes/_docs/doc.tsx index 2a473845609..0ff30537aa8 100644 --- a/zookeeper-website/app/routes/_docs/doc.tsx +++ b/zookeeper-website/app/routes/_docs/doc.tsx @@ -16,7 +16,7 @@ // limitations under the License. // -import type { Route } from "./+types/docs"; +import type { Route } from "./+types/doc"; import { source } from "@/lib/source"; import { DocsPage } from "@/pages/_docs/docs"; @@ -31,6 +31,6 @@ export async function loader({ params }: Route.LoaderArgs) { }; } -export default function Docs(props: Route.ComponentProps) { +export default function Doc(props: Route.ComponentProps) { return <DocsPage {...props} />; } diff --git a/zookeeper-website/package.json b/zookeeper-website/package.json index e5774491a65..54367988a52 100644 --- a/zookeeper-website/package.json +++ b/zookeeper-website/package.json @@ -7,7 +7,7 @@ }, "scripts": { "build": "tsx scripts/build-site.ts", - "build:landing": "npm run extract-developers && ZOOKEEPER_BUILD_TARGET=landing react-router build && npm run generate-sitemap -- --scope landing", + "build:landing": "npm run extract-developers && ZOOKEEPER_BUILD_TARGET=landing react-router build", "build:docs": "tsx scripts/build-docs.ts", "dev": "react-router dev", "start": "vite preview --port 5173", diff --git a/zookeeper-website/scripts/build-docs.ts b/zookeeper-website/scripts/build-docs.ts index ebff5794f8d..3392c2da501 100644 --- a/zookeeper-website/scripts/build-docs.ts +++ b/zookeeper-website/scripts/build-docs.ts @@ -289,13 +289,6 @@ export async function main() { await buildDocs(CURRENT_VERSION); - // The current docs build owns the docs slice of the sitemap. - runCommand( - "npm", - ["run", "generate-sitemap", "--", "--scope", "docs"], - process.env - ); - // react-router build emits to build/client/, then buildDocs() assembles the // self-contained deliverable at build/doc/r<v>/. The leftover build/client/ // is just intermediate output — drop it so build/doc/ is the only root. diff --git a/zookeeper-website/scripts/generate-sitemap.ts b/zookeeper-website/scripts/generate-sitemap.ts index e67efc45fb8..fc02747e150 100644 --- a/zookeeper-website/scripts/generate-sitemap.ts +++ b/zookeeper-website/scripts/generate-sitemap.ts @@ -45,9 +45,9 @@ const EXCLUDED_HTML_PATHS = new Set([ "404.html", "404/index.html", "__spa-fallback.html", - // The /doc redirect is a client-side <Navigate> with no redirect marker in its - // prerendered HTML, so it must be excluded explicitly (and it only exists in - // builds that include landing, which would otherwise diverge from docs builds). + // /doc is a loader-driven redirect to /doc/r<CURRENT_VERSION>/. Its meta- + // refresh marker would already be caught by REDIRECT_PAGE_PATTERNS, but we + // exclude it explicitly so landing-inclusive and docs-only builds agree. "doc/index.html" ]); From b6707341fa16c39a982a26ecb3a7c2b9e383b493 Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk <yurapalamarchuk111@gmail.com> Date: Fri, 26 Jun 2026 13:46:24 +0200 Subject: [PATCH 12/14] Remove readme --- zookeeper-website/README.md | 837 ------------------------------------ 1 file changed, 837 deletions(-) delete mode 100644 zookeeper-website/README.md diff --git a/zookeeper-website/README.md b/zookeeper-website/README.md deleted file mode 100644 index 03a8402b20b..00000000000 --- a/zookeeper-website/README.md +++ /dev/null @@ -1,837 +0,0 @@ -<!-- -Licensed to the Apache Software Foundation (ASF) under one -or more contributor license agreements. See the NOTICE file -distributed with this work for additional information -regarding copyright ownership. The ASF licenses this file -to you under the Apache License, Version 2.0 (the -"License"); you may not use this file except in compliance -with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -# Apache ZooKeeper Website - -The official website for Apache ZooKeeper, built with modern web technologies to provide a fast, accessible, and maintainable web presence. - ---- - -## Table of Contents - -- [Content Editing](#content-editing) -- [Development](#development) - - [Prerequisites](#prerequisites) - - [Technology Stack](#technology-stack) - - [Project Architecture](#project-architecture) - - [Getting Started](#getting-started) - - [Development Workflow](#development-workflow) - - [Testing](#testing) - - [Building for Production](#building-for-production) - - [How the Docs Build Works](#how-the-docs-build-works) - - [Continuous Integration](#continuous-integration) - - [Maven Integration](#maven-integration) - - [Deployment](#deployment) - - [Publishing a New ZooKeeper Release](#publishing-a-new-zookeeper-release) - - [Troubleshooting](#troubleshooting) - ---- - -## Content Editing - -Most landing pages store content in **Markdown (`.md`)**, and some in **JSON (`.json`)**, files located in `app/pages/_landing/[page-name]/`. Docs content lives under `app/pages/_docs/` and content is stored in `.mdx` (it's just extended markdown). - -**Examples:** - -- `app/pages/_landing/releases/index.tsx` - Releases page -- `app/pages/_docs/docs/_mdx/...` - MDX content for documentation - -The credits page (`app/pages/_landing/credits/`) is a special case: its developer list is **generated** from the project's parent [`pom.xml`](../pom.xml) `<developers>` section by [`scripts/extract-developers.js`](scripts/extract-developers.js). The generated `developers.json` is gitignored and produced as the first step of both `npm run ci` and `npm run ci-skip-tests`. To edit who appears on `/credits`, update the `<developers>` block in the parent `pom.xml`. - ---- - -## Development - -### Prerequisites - -Before you begin, ensure you have the following installed: - -- **Node.js — 22, 23, or 24** (JavaScript runtime; like the JVM for Java) - - Download from [nodejs.org](https://nodejs.org/) - - Verify installation: `node --version` (must satisfy `^22.12.0 || ^23.0.0 || ^24.0.0`; see [`zookeeper-website/package.json`](package.json) `engines.node`) - - **Node 25 and 26 are not supported** — some tests fail under those releases. The `engines` field in `package.json` advertises the allowed range; npm prints a warning when it does not match but still installs. Treat the warning as blocking and downgrade to a supported Node line before running the CI pipeline. - -- **NPM** - Node Package Manager (like Maven for Java) - - Comes bundled with Node.js - - Verify installation: `npm --version` - -### Technology Stack - -This website uses modern web technologies. Here's what each one does (with Java analogies): - -#### Core Framework - -- **React Router** - Full-stack web framework with Server-Side Generation (SSG) - - Handles routing (like Spring MVC controllers) - - Provides server-side rendering for better performance and SEO - - Enables progressive enhancement (see below) - - [Documentation](https://reactrouter.com/) - -#### Documentation Framework - -- **Fumadocs** - Documentation framework used for the docs section - - Provides MDX-based docs structure and navigation - - Lives alongside the landing pages in the same React Router app - - [Documentation](https://fumadocs.com/) - -#### Progressive Enhancement - -The website uses **progressive enhancement** ([learn more](https://reactrouter.com/explanation/progressive-enhancement)), which means: - -- **With JavaScript enabled**: Users get a Single Page Application (SPA) experience - - Fast page transitions without full page reloads - - Smooth animations and interactive features - - Enhanced user experience - -- **Without JavaScript**: Core content and navigation remain accessible via server-rendered HTML - - Primary page content and most links work without client-side routing - - Critical flows such as the "Older docs" menu include no-JS fallbacks - - Some features (search, theme toggle, interactive menus) don't work since they require JavaScript - -This approach keeps the site usable without JavaScript while still providing a richer experience when it is available. - -#### UI Components - -- **shadcn/ui** - Pre-built, accessible UI components - - Similar to a component library like PrimeFaces or Vaadin in Java - - Provides buttons, cards, navigation menus, etc. - - [Documentation](https://ui.shadcn.com/) - -#### Styling - -- **TailwindCSS** - Utility-first CSS framework, aka Bootstrap on steroids - - Instead of writing CSS files, you apply classes directly in components - - Example: `className="text-blue-500 font-bold"` makes blue, bold text - -#### Code Quality Tools - -- **TypeScript** - Typed superset of JavaScript - - Similar to Java's type system - - Catches errors at compile-time instead of runtime - - Provides autocomplete and better IDE support - -- **ESLint + Prettier** - Code linting and formatting (like Checkstyle) - - ESLint analyzes code for potential errors and enforces coding standards - - Prettier handles automatic code formatting (spacing, indentation, etc.) - - Integrated together: `npm run lint:fix` handles both linting and formatting - - Configuration: `eslint.config.js` and `prettier.config.js` - -### Project Architecture - -The project follows a clear directory structure with separation of concerns: - -``` -zookeeper-website/ -├── app/ # Application source code -│ ├── ui/ # Reusable UI components (no business logic) -│ │ ├── button.tsx # Generic button component -│ │ ├── card.tsx # Card container component -│ │ └── ... # Other UI primitives -│ │ -│ ├── components/ # Reusable components WITH business logic -│ │ ├── site-navbar.tsx # Website navigation bar -│ │ ├── site-footer.tsx # Website footer -│ │ ├── theme-toggle.tsx # Dark/light mode toggle -│ │ -│ ├── pages/ # Complete pages (composed of ui + components) -│ │ ├── _landing/ # Landing pages + layout -│ │ │ ├── home/ # Home page -│ │ │ │ ├── index.tsx # Main page component (exported) -│ │ │ │ ├── hero.tsx # Hero section (not exported) -│ │ │ │ ├── features.tsx # Features section (not exported) -│ │ │ │ └── ... -│ │ │ ├── news/ # Landing page content -│ │ │ └── ... -│ │ ├── _docs/ # Documentation (Fumadocs) -│ │ │ ├── docs/ # MDX content and structure -│ │ │ ├── docs-layout.tsx # Fumadocs layout wrapper -│ │ │ └── ... -│ │ -│ ├── routes/ # Route definitions and metadata -│ │ ├── _landing/ # Landing page routes -│ │ │ ├── home.tsx # Home route configuration -│ │ │ ├── news.tsx # News route configuration -│ │ │ └── ... -│ │ └── _docs/ # Docs route configuration -│ │ -│ ├── lib/ # Utility functions and integrations -│ │ ├── utils.ts # Helper functions -│ │ └── theme-provider.tsx # Theme management -│ │ -│ ├── routes.ts # Main routing configuration -│ ├── root.tsx # Root layout component -│ └── app.css # Global styles -│ -├── build/ # Generated files (DO NOT EDIT) -│ ├── client/ # Browser-side assets -│ │ ├── index.html # HTML files for each page -│ │ ├── assets/ # JavaScript, CSS bundles -│ │ └── images/ # Optimized images -│ -├── public/ # Static files (copied as-is to build/) -│ ├── favicon.ico # Website icon -│ ├── images/ # Image assets -│ ├── docs-images/ # Docs image assets -│ └── ... -│ -├── node_modules/ # Dependencies (like Maven's .m2 directory) -├── package.json # Project metadata and dependencies (like pom.xml) -├── tsconfig.json # TypeScript configuration -└── react-router.config.ts # React Router framework configuration -``` - -#### Key Principles - -1. **UI Components (`/ui`)**: Pure, reusable components with no business logic - - Can be used anywhere in the application - - Only concerned with appearance and basic interaction - -2. **Business Components (`/components`)**: Reusable across pages - - May contain business logic specific to ZooKeeper website - - Examples: navigation, footer, theme toggle - -3. **Pages (`/pages`)**: Complete pages combining ui and components - - Each page has its own directory - - Only `index.tsx` is exported - - Internal components stay within the page directory - - If a component needs to be reused, move it to `/components` - -4. **Routes (`/routes`)**: Define routing and metadata - - Maps URLs to pages - - Sets page titles, meta tags, etc. - -5. **One Codebase, Two Independent Builds**: - - **Landing pages** live under `app/pages/_landing/` and use the landing layout; they are served from the site root (`/`). - - **Docs pages** live under `app/pages/_docs/` and use Fumadocs layouts; they are served from a versioned base (`/doc/r<version>/`). - - They share one codebase but are produced as two **separate** builds (landing at base `/`, docs at base `/doc/r<version>/`). A docs change therefore never rewrites landing assets, and vice versa, keeping `asf-site` diffs clean. `npm run build` runs both and merges them; `npm run dev` serves a combined site so local authoring is unaffected. - - The build context is selected by environment variables read in [`app/routes.ts`](app/routes.ts) and the Vite/React Router configs: - - `ZOOKEEPER_DOCS_ARCHIVE_BASE=/doc/rX/` → **docs build** (any version, including the current one): docs catch-all + `api/search` + `llms-full.txt`, served under `/doc/rX/`. - - `ZOOKEEPER_BUILD_TARGET=landing` → **landing build**: landing pages + the `/doc` redirect only. - - neither (default, e.g. `react-router dev`) → **combined** site for local development. - -6. **Documentation Structure**: - - **Docs** live under `app/pages/_docs/docs/_mdx/` and are the source of truth. - -7. **Documentation Versions**: - - Three constants drive the versions surfaced by the site, all defined together for a reason: - - **`CURRENT_VERSION`** ([`app/lib/current-version.ts`](app/lib/current-version.ts)) — the version the in-tree MDX corresponds to. On `master` this is the **canary** version that the next release is being prepared for. The docs build serves it from `/doc/r<CURRENT_VERSION>/`. - - **`RAW_RELEASED_DOC_VERSIONS_LIST`** (private literal in [`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts), exported as the Set `RAW_RELEASED_DOC_VERSIONS`) — every documentation version that has ever been published under `/doc/`. New entries are appended when a release ships, and the list is kept in sync with the `content/doc/r<version>/` directories on `asf-site`. `CURRENT_VERSION` is included at the end so it is exposed through the same machinery. - - **`LTS_VERSIONS`** ([`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts)) — versions that are pinned to the top of the **Documentation** dropdown in the site navbar. By convention `CURRENT_VERSION` is **first** in this list, followed by the still-supported long-term branches. Everything else from `RAW_RELEASED_DOC_VERSIONS_LIST` is shown afterwards under "Older versions", sorted in descending order. - - When releasing a new version, both `RAW_RELEASED_DOC_VERSIONS_LIST` and `LTS_VERSIONS` typically need to be touched — see the [release flow](#publishing-a-new-zookeeper-release). For routine non-release edits, neither file changes. - -#### Important Conventions - -##### Custom Link Component - -**Always use the custom Link component from `@/components/link` instead of importing Link directly from `react-router`.** - -The ZooKeeper website includes pages that are not part of this React Router application (e.g., documentation pages, API docs). The custom Link component automatically determines whether a link should trigger a hard reload or use React Router's client-side navigation: - -**Usage:** - -```typescript -// ✅ CORRECT - Use custom Link component -import { Link } from "@/components/link"; - -export const MyComponent = () => ( - <Link to="/news">News</Link> -); -``` - -```typescript -// ❌ WRONG - Do not import Link from react-router -import { Link } from "react-router"; - -export const MyComponent = () => ( - <Link to="/news">News</Link> -); -``` - -The ESLint configuration includes a custom rule (`custom/no-react-router-link`) that will throw an error if you attempt to import `Link` from `react-router`, helping enforce this convention automatically. - -### Getting Started - -#### 1. Install Dependencies - -Think of this as `mvn install`: - -```bash -npm ci -``` - -`npm ci` (short for "clean install") downloads all required packages from npm (similar to Maven Central). **Prefer `ci` over `npm install` for any reproducible build** — your local dev tree, contributor checkouts, the release publish flow, CI: - -- It installs the **exact** versions pinned in `package-lock.json`. `npm install` is allowed to upgrade dependencies within the semver ranges in `package.json` and to rewrite the lock file; `npm ci` never touches the lock file. -- It wipes `node_modules/` first, so the resulting tree always matches the lock file rather than carrying over stale packages from a previous install. -- It is also typically faster than `npm install` in a fresh checkout. - -#### 2. Start Development Server - -```bash -npm run dev -``` - -This starts a local development server with: - -- **Hot Module Replacement (HMR)**: Code changes appear instantly without full page reload -- **Live at**: `http://localhost:5173` - -### Development Workflow - -#### Making Changes - -1. **Edit code** in the `app/` directory -2. **Save the file** - changes appear automatically in the browser -3. **Check for errors** in the terminal where `npm run dev` is running and in browser console - -#### Common Tasks - -**Add a new landing page:** - -1. Create a directory in `app/pages/_landing/my-new-page/` -2. Create `index.tsx` in that directory and export the page component -3. Create a route file in `app/routes/_landing/my-new-page.tsx` with `meta()` and a default export -4. Register the route in `app/routes.ts` inside the `_landing` layout block - -**Add a new documentation page:** - -1. Create a new `.mdx` file in `app/pages/_docs/docs/_mdx/` (for example `my-topic.mdx`). -2. Add the new file to the relevant `meta.json` in the same section folder so it appears in navigation. - -When writing docs content, follow these conventions: - -- Each MDX file starts with a YAML frontmatter block declaring at minimum `title` and `description`. These power the page `<title>`, the sidebar label, and the meta description: - - ```mdx - --- - title: "My topic" - description: "Short one-liner." - --- - ``` - -- Internal doc links are **doc-root-relative** — write `/admin-ops/cli`, not `/docs/...` and not a hardcoded `/doc/r<version>/...`. The version prefix (`/doc/r<version>`) is added automatically at render time (`resolveDocsHref` on the live site, React Router `basename` in an archive). -- Images use the static asset path `/docs-images/...` (served from `public/docs-images/`). -- Use absolute `https://` URLs for off-site links (e.g. `https://zookeeper.apache.org/`). - -**Update content:** - -- Edit the appropriate `.md`, `.mdx`, or `.json` file -- Changes appear automatically - -**Add a UI component:** - -- Check if shadcn/ui has what you need first -- Only create custom components if necessary - -**Update the 404 page:** - -There are two separate 404 mechanisms: - -- **Static Apache 404** (`public/404.html`): Served by Apache for requests that never reach the SPA (missing static files, direct URL hits). Configure handling in `public/.htaccess` (`ErrorDocument 404 /404.html`). Keep `public/robots.txt` disallowing `/404.html` so the error page is not indexed. -- **In-app error boundary** (`app/root.tsx`): Handles unknown routes after the React app loads (for example, invalid docs paths). Edit `ErrorBoundary` in `root.tsx` to change that experience. - -**Check code quality:** - -```bash -npm run lint -``` - -**Fix linting and formatting issues:** - -```bash -npm run lint:fix -``` - -### Testing - -The project uses [Vitest](https://vitest.dev/) and [Playwright](http://playwright.dev/) for testing. Vitest is for unit testing, while Playwright is for e2e testing. - -**Note:** Playwright tests are configured to run against the production build. Therefore, you must build the project (`npm run build`) before running the e2e tests locally. - -**Run tests:** - -```bash -# Run all tests (unit + e2e) -# Note: e2e tests require a production build first (`npm run build`) -npm test - -# Run unit tests once (for CI/CD) -npm run test:unit:run - -# Run unit tests with UI -npm run test:unit:ui - -# Run e2e tests (requires `npm run build` first) -npm run test:e2e - -# Run e2e tests with UI (requires `npm run build` first) -npm run test:e2e:ui -``` - -**Writing new tests:** - -Use the `renderWithProviders` utility in `unit-tests/utils.tsx` to ensure components have access to routing and theme context: - -```typescript -import { renderWithProviders, screen } from "./utils"; -import { MyComponent } from "@/components/my-component"; - -describe("MyComponent", () => { - it("renders correctly", () => { - renderWithProviders(<MyComponent />); - expect(screen.getByText("Hello World")).toBeInTheDocument(); - }); -}); -``` - -### Building for Production - -**CI/CD Workflow:** - -Before merging or deploying, run the full CI pipeline: - -```bash -npm run ci -``` - -This command runs all quality checks and builds the project. All checks must pass before code is considered ready. - -If you need the CI flow without unit/e2e test suites, use: - -```bash -npm run ci-skip-tests -``` - -This runs docs initialization and the production build (without lint/typecheck and without unit/e2e test suites). - -Generated files are located under the `build/` directory. - -**Build commands:** - -The landing site and the docs are produced as two independent Vite builds (see [One Codebase, Two Independent Builds](#key-principles)). `npm run build` orchestrates them via [`scripts/build-site.ts`](scripts/build-site.ts): - -- `npm run build` — builds the current docs (`/doc/r<CURRENT_VERSION>/`), then the landing site (base `/`), then merges the docs into `build/client/doc/r<CURRENT_VERSION>/` and regenerates the full sitemap (`--scope all`). The merged `build/client/` is what `npm run start`, `vite preview`, and the e2e tests serve. -- `npm run build:landing` — landing-only build (sets `ZOOKEEPER_BUILD_TARGET=landing`), output at `build/client/` (root pages). -- `npm run build:docs` — pure docs build for `CURRENT_VERSION`, output at `build/doc/r<CURRENT_VERSION>/`. No CI checks. - -Two code-generation steps feed the build: - -- **`npm run extract-developers`** writes `app/pages/_landing/credits/developers.json` from the parent [`pom.xml`](../pom.xml). It is invoked automatically by any landing-site build: `build:landing` runs it before Vite, and `build` runs it inside [`scripts/build-site.ts`](scripts/build-site.ts) before the landing step. `build:docs` does not need it (no credits route in the docs build). -- **`npm run fumadocs-init`** writes the fumadocs MDX index to `.source/`. Vite's `fumadocs-mdx/vite` plugin regenerates `.source/` at build/test time, so this step is **not** needed by any build commands. It **is** needed by `lint`, because eslint's `import/no-unresolved` resolver reads the filesystem directly and will fail on the two `@/.source` imports if `.source/` does not exist. - -The docs version always comes from [`app/lib/current-version.ts`](app/lib/current-version.ts). - -`CURRENT_VERSION` on the **`master`** branch is **not** a released version — it is the **canary** for the next release that is being worked on. The actual released version is whatever sits at `content/doc/current/` on `asf-site`. `CURRENT_VERSION` is bumped on `master` as soon as work on the next version begins (see [Publishing a New ZooKeeper Release](#publishing-a-new-zookeeper-release)); from that point on, edits to docs on `master` accumulate under the new canary value and ship together when that release cuts. - -Because each docs version and the landing site are separate Vite builds, they have independent asset hashes: editing docs never changes landing output, and vice versa. This is what makes it possible to **ship landing-page updates and docs updates independently**: - -- A typo fix or new section in the landing pages can be published to `asf-site` without rebuilding or re-publishing the current docs directory. -- A docs-only change (a fix in MDX content, or a new archived docs version) can be published without touching the landing files. -- The [basic workflow](#basic-workflow) below describes the combined rebuild. - -> Note: React Router cleans the entire `build/` directory at the start of every build, so the orchestrator stashes the docs output outside `build/` before running the landing build, then restores it into `build/client/`. - -#### How the Docs Build Works - -`npm run build:docs` produces a self-contained docs tree at `build/doc/r<CURRENT_VERSION>/`, ready to copy to `asf-site` at `content/doc/r<CURRENT_VERSION>/`. It validates the output before exiting — every source MDX docs page must have a corresponding HTML page. - -The docs build uses React Router's `basename` and Vite's `base` to serve docs from `/doc/r<version>/`. URL shape: - -- `/doc/r3.9.6/` -- `/doc/r3.9.6/overview/quick-start` -- `/doc/r3.9.6/developer/programmers-guide` - -It uses the docs route set from `app/routes.ts`: the docs catch-all, `api/search`, and `llms-full.txt`. Non-doc archive-local requests such as `/doc/r3.9.6/news` are redirected by the generated `.htaccess` file to the matching live-site path (`/news`). - -Docs builds set the `ZOOKEEPER_DOCS_ARCHIVE_BASE` env var. Vite turns it into the app `base`, which the browser reads back as `import.meta.env.BASE_URL`. Rule of thumb for the build context: - -- Node-side code (vite/react-router configs, scripts) reads `ZOOKEEPER_DOCS_ARCHIVE_BASE` and `ZOOKEEPER_BUILD_TARGET` from `process.env` (via `getDocsArchiveBase()` / `getBuildTarget()` in [`app/lib/docs-archive.ts`](app/lib/docs-archive.ts)). -- Bundled app code reads the docs base back as `import.meta.env.BASE_URL`. - -`ZOOKEEPER_DOCS_ARCHIVE_BASE` and `import.meta.env.BASE_URL` are the same value in two execution contexts. `ZOOKEEPER_BUILD_TARGET=landing` is the orthogonal signal that selects the landing-only build. - -The docs build script packages the generated docs into a self-contained directory. React Router prerenders docs HTML under `build/client/doc/r<version>/`, while Vite and `public/` assets are emitted at the build root (`build/client/assets/`, `build/client/docs-images/`, `build/client/fonts/`, `build/client/images/`, `build/client/favicon.ico`). The script copies the docs HTML plus those known static assets into `build/doc/r<version>/` so URLs such as `/doc/r3.9.6/assets/...` exist after publishing, then deletes the intermediate `build/client/` so `build/doc/r<version>/` is the only output. If new top-level static folders or asset roots are introduced, update `scripts/build-docs.ts` so they are copied, URL-rewritten if needed, and covered by docs build validation. - -Docs output intentionally does not copy the live site's root-only files such as `404.html`, `robots.txt`, `__spa-fallback.html`, or the root `.htaccess`. Docs builds get their own generated `.htaccess` (described above). The docs build keeps only docs routes, `api/search`, and `llms-full.txt`. - -#### Continuous Integration - -GitHub Actions runs the website build on every push and pull request via [`.github/workflows/website.yaml`](../.github/workflows/website.yaml). The workflow: - -1. Sets up JDK 11 and Node.js 22 -2. Runs `npm ci` and installs Playwright browsers with system dependencies -3. Executes `mvn -pl zookeeper-website site` (same checks as local `npm run ci`) - -Pull requests should pass the **Website / website-site** check before merge. - -### Maven Integration - -The website is integrated into the Apache ZooKeeper Maven build through two POMs: - -- The repository root `pom.xml` includes `zookeeper-website` as a Maven module. -- `zookeeper-website/pom.xml` owns the website-specific build and configures `frontend-maven-plugin`. - -The frontend plugin is intentionally bound only to the Maven `site` lifecycle: - -- `pre-site` installs Node.js/npm and runs `npm ci` -- `site` runs the website CI command - -Because of that, the website is built **only during site generation** (`mvn site`) and is skipped during regular Maven lifecycle phases such as `mvn clean install`. - -#### When the Website Builds - -The website build is triggered **only** when you run: - -```bash -mvn site -``` - -The website will **NOT** build during regular commands like: - -- `mvn clean install` -- `mvn package` -- `mvn compile` - -This keeps regular ZooKeeper builds fast while still allowing the website to be generated when needed. - -#### What the Website POM Does During `mvn site` - -When you run `mvn site`, the website module automatically: - -1. **Cleans previous build artifacts** - - Removes `build/` directory - - Removes `node_modules/` directory - - Ensures a fresh build environment - -2. **Installs Node.js v22.20.0 and npm 11.6.2** (if not already available) - - Installed to `target/` directory - - Does not affect your system Node/npm installation - -3. **Runs `npm ci`** to install all dependencies - - Reads from `package.json` and pins to `package-lock.json` - - Installs to `node_modules/` - -4. **Runs a website CI command**: - - Default (`mvn site`): `npm run ci` - - With test skipping (`mvn site -DskipTests`): `npm run ci-skip-tests` - - `npm run ci` executes: - - `npm run fumadocs-init` - Intialize Fumadocs artifcats (`.source/`), so linter doesn't complain - - `npm run lint` - ESLint code quality checks - - `npm run typecheck` - TypeScript type checking - - `npm run test:unit:run` - Vitest unit tests - - `npm run build` - Production build - - `npx playwright install` - Installs Playwright browsers - - `npm run test:e2e` - Playwright e2e tests - - `npm run ci-skip-tests` executes: - - `npm run build` - Production build - -5. **Build Output**: Generated files are in `build/` directory - -#### Maven Commands - -**Build ZooKeeper WITHOUT the Website (default):** - -```bash -# From ZooKeeper root directory -mvn clean install -``` - -**Build the Website:** - -```bash -# From ZooKeeper root directory -mvn site -``` - -This generates the full ZooKeeper website including documentation and the React-based website. - -**Build the Website While Skipping Test Suites:** - -```bash -# From ZooKeeper root or zookeeper-website directory -mvn site -DskipTests -``` - -This runs `npm run ci-skip-tests` for the website module. - -**Build Website Only:** - -```bash -# From ZooKeeper root directory -mvn -pl zookeeper-website site - -# Or from the website module directory -cd zookeeper-website -mvn site -``` - -### Deployment - -The website source lives on the **`master`** branch of the [apache/zookeeper](https://github.com/apache/zookeeper) repository under `zookeeper-website/`. The live production site at [zookeeper.apache.org](https://zookeeper.apache.org) is served from the **`asf-site`** branch. Any commit pushed to `asf-site` is immediately reflected on the live site. - -#### Basic workflow - -Use this flow for any update to an already-published version: landing edits, docs fixes, news, maintenance. For shipping a new version, see [Publishing a New ZooKeeper Release](#publishing-a-new-zookeeper-release). - -The end-to-end flow has two halves: **source changes land on `master` via PR**, then **the rebuilt site is published to `asf-site` by hand**. Because the landing site and the current docs are independent builds with independent asset hashes, they live in separate directories under `content/` on `asf-site` (landing at the root, current docs under `content/doc/r<CURRENT_VERSION>/`). If a part's source did not change, its rebuilt output is byte-identical and produces no `content/` diff, so seeing changes in only landing or only docs after a rebuild is expected. The current docs directory is self-contained (its own assets, `.htaccess`, and `llms-full.txt`); only the Maven-generated `apidocs/` is layered back in during publish. - -The steps: - -1. **Clone the repo** and check out `master`. -2. **Install dependencies** with `npm ci`. -3. **Make changes** on a feature branch based off `master`. -4. **Verify locally** by running the full CI pipeline (`npm run ci`). -5. **Open a PR and merge to `master`** after review. -6. **Build the production bundle** on `master` and stage `build/client/` into `/tmp`. The output is what will land under `content/` on `asf-site`: landing files at the root of `build/client/` and current docs under `build/client/doc/r<CURRENT_VERSION>/`. -7. **Switch to `asf-site`** and wipe everything not tracked by that branch with `git clean -fxd`. -8. **Stage the current version's `apidocs/`** out to `/tmp`. It is published by the Maven build (not by the website CI pipeline), so it must be preserved across the docs replace in step 10. -9. **Replace the landing files** at the root of `content/` from `/tmp`, leaving `content/doc/` untouched so every archived version stays in place. -10. **Replace the current docs directory** (`content/doc/r<CURRENT_VERSION>/`) with the freshly built one, then layer the staged `apidocs/` back in. -11. **Commit and push `asf-site`**, then clean up the staging directories under `/tmp`. - -Step-by-step, in shell: - -```bash -# Set this to the current docs version from app/lib/current-version.ts (the canary on master) -DOC_VERSION=3.9.5 - -# Step 1: Clone and check out master -git clone https://github.com/apache/zookeeper.git -cd zookeeper -git checkout master - -# Step 2: Install dependencies -cd zookeeper-website -npm ci - -# Step 3: Make changes -git checkout -b <your-branch> -# Edit content in zookeeper-website/, then continue... - -# Step 4: Verify locally -npm run ci - -# Step 5: Push, open a PR, merge to master -git add <changed files> -git commit -m "Update website content" -git push origin <your-branch> -# ...open a PR, get review, merge to master, then pull master locally before step 6. - -# Step 6: Stage the build output to /tmp BEFORE switching branches -# Preventive measure to ensure no conflicts appear. -rm -rf /tmp/zookeeper-site-build -cp -R build/client /tmp/zookeeper-site-build - -# Step 7: Switch to asf-site and clean -cd .. -git checkout asf-site -# Drop every untracked file left over from the master checkout -git clean -fxd - -# Step 8: Preserve the current version's API docs -# Same preemptive-wipe pattern as above. -rm -rf /tmp/zookeeper-apidocs-r${DOC_VERSION} -cp -R content/doc/r${DOC_VERSION}/apidocs /tmp/zookeeper-apidocs-r${DOC_VERSION} - -# Step 9: Replace landing files (everything in content/ except content/doc/) -find content -mindepth 1 -maxdepth 1 ! -name doc -exec rm -rf {} + -find /tmp/zookeeper-site-build -mindepth 1 -maxdepth 1 ! -name doc -exec cp -R {} content/ \; - -# Step 10: Replace the current docs version, then restore its API docs -rm -rf content/doc/r${DOC_VERSION} -mkdir -p content/doc/r${DOC_VERSION} -cp -R /tmp/zookeeper-site-build/doc/r${DOC_VERSION}/. content/doc/r${DOC_VERSION}/ -cp -R /tmp/zookeeper-apidocs-r${DOC_VERSION} content/doc/r${DOC_VERSION}/apidocs - -# Step 11: Commit, push, clean up -git add content -git commit -m "Publish website <date>" -git push origin asf-site - -rm -rf /tmp/zookeeper-site-build /tmp/zookeeper-apidocs-r${DOC_VERSION} -``` - -Once `asf-site` is pushed the updates are live within minutes. - ---- - -### Publishing a New ZooKeeper Release - -A ZooKeeper release follows the established Apache "cut a release branch off the line branch" pattern, and the website piggybacks on it. The big picture is: - -1. **Prepare** `master` for the release — update landing content, version constants, MDX docs, etc. `CURRENT_VERSION` should already match the version being released. -2. **Cut the release branch** — e.g. `branch-3.9` -> `branch-3.9.6`, push. -3. **Build the docs from the release branch** so the rendered HTML pins to that tag rather than to whatever is on `master`. -4. **Publish on `asf-site`** — add `content/doc/r3.9.6/`, repoint `content/doc/current` to it, replace the landing pages in the root directory. - -Example: current release is **3.9.5**, new release is **3.9.6**. The line branch is **`branch-3.9`**. Throughout this section: `OUTGOING=3.9.5`, `CURRENT=3.9.6`. - -#### Step 1 — Prepare landing + docs source on `master` - -Done on the `master` branch, merged through normal PRs **before** the release branch is cut. - -1. Confirm `CURRENT_VERSION` in [`app/lib/current-version.ts`](app/lib/current-version.ts) already equals the release being prepared (e.g. `"3.9.6"`). The bump should have happened when work on this version started. If it doesn't, do the bump now as a normal PR (see [Bumping `CURRENT_VERSION`](#bumping-current_version-canary)). -2. Update landing pages that reference the version: `releases`, `news`, anywhere else that calls out the latest version by string. -3. Revisit `LTS_VERSIONS` in [`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts) if this release changes long-term-support membership. By convention `CURRENT_VERSION` is the **first** entry — these versions get pinned to the top of the navbar's **Documentation** dropdown. `RAW_RELEASED_DOC_VERSIONS_LIST` already terminates with `CURRENT_VERSION`, so the new version is exposed automatically. -4. Update MDX under `app/pages/_docs/docs/_mdx/` for the new release (release notes, version-specific instructions, etc.). -5. Run `npm ci && npm run ci` locally and open a PR. Merge once green. - -#### Step 2 — Cut the release branch - -This is the standard ZooKeeper release-branching step (e.g. `branch-3.9` → `branch-3.9.6`). - -#### Step 3 — Build docs and landing from the release branch - -```bash -CURRENT=3.9.6 - -# Working from branch-3.9.6 on a clean checkout -cd zookeeper-website -npm ci -npm run ci - -# Stage the build output before we ever switch branches. -rm -rf /tmp/zookeeper-site-build -cp -R build/client /tmp/zookeeper-site-build - -# Stage the Java API docs generated by the Maven build for this release. -cd .. -rm -rf /tmp/zookeeper-apidocs-r${CURRENT} -cp -R <path-to-generated-apidocs> /tmp/zookeeper-apidocs-r${CURRENT} -``` - -The fresh `build/client/doc/r${CURRENT}/` directory is the contents of `doc/r3.9.6/` that will be added to `asf-site` in Step 4. - -#### Step 4 — Publish on `asf-site` - -```bash -git checkout asf-site -git clean -fxd - -# 4a. Add the new docs version -mkdir -p content/doc/r${CURRENT} -cp -R /tmp/zookeeper-site-build/doc/r${CURRENT}/. content/doc/r${CURRENT}/ -cp -R /tmp/zookeeper-apidocs-r${CURRENT} content/doc/r${CURRENT}/apidocs - -# 4b. Repoint content/doc/current at the new release. -# `content/doc/current` is a symlink (NOT a directory) whose target is -# the current `r<version>` directory. Update it in place so URLs under -# /doc/current/... resolve to the new release. -ln -sfn r${CURRENT} content/doc/current - -# 4c. Replace the landing pages (everything in content/ except content/doc/). -find content -mindepth 1 -maxdepth 1 ! -name doc -exec rm -rf {} + -find /tmp/zookeeper-site-build -mindepth 1 -maxdepth 1 ! -name doc -exec cp -R {} content/ \; - -git add content -git commit -m "Publish ZooKeeper ${CURRENT}" -git push origin asf-site - -# Cleanup -rm -rf \ - /tmp/zookeeper-site-build \ - /tmp/zookeeper-apidocs-r${CURRENT} -``` - -Once `asf-site` is pushed the live site reflects the release within minutes. - -#### Bumping `CURRENT_VERSION` (canary) - -`CURRENT_VERSION` on `master` is bumped **once per upcoming release**, at the moment the team starts working on that version. - -Files to edit: - -- **[`app/lib/current-version.ts`](app/lib/current-version.ts)** — bump `CURRENT_VERSION` to the new canary (e.g. `"3.9.7"`). -- **[`app/lib/released-docs-versions.ts`](app/lib/released-docs-versions.ts)** — append the version that just shipped to `RAW_RELEASED_DOC_VERSIONS_LIST` (it now has a published `content/doc/r<version>/` directory on `asf-site`), and revisit `LTS_VERSIONS`: the new `CURRENT_VERSION` should be first, and the rest of the list may need entries added, removed, or swapped depending on which lines are still being supported. - -```bash -git checkout master -git pull origin master -git checkout -b bump-current-version - -# Edit the two files described above. - -cd zookeeper-website -npm ci -npm run ci -cd .. - -git commit -am "Bump CURRENT_VERSION canary to 3.9.7" -git push -u origin bump-current-version -# open a PR, merge. -``` - -After this lands, every subsequent docs edit on `master` accumulates under the new canary until the next release branch is cut. - -### Troubleshooting - -#### TypeScript Types Are Broken - -If you see type errors related to React Router's `+types`, regenerate them: - -```bash -npx react-router typegen -``` - -#### Port Already in Use - -If `npm run dev` fails because port 5173 is in use: - -```bash -# Kill the process using the port -lsof -ti:5173 | xargs kill -9 - -# Or change the port in vite.config.ts -``` - -#### Build Fails - -1. **Clear generated files:** - - ```bash - rm -rf build/ node_modules/ .vite/ .react-router/ .source/ - ``` - -2. **Reinstall dependencies:** - - ```bash - npm ci - ``` - -3. **Try building again:** - ```bash - npm run build - ``` - ---- - -## Additional Resources - -- **React Router Documentation**: https://reactrouter.com/ -- **Progressive Enhancement Explained**: https://reactrouter.com/explanation/progressive-enhancement -- **shadcn/ui Components**: https://ui.shadcn.com/ -- **TailwindCSS Docs**: https://tailwindcss.com/ -- **TypeScript Handbook**: https://www.typescriptlang.org/docs/ - ---- - -Built with ❤️ for the Apache ZooKeeper community. From 1cd89d8b486c942b96d640e5b1f08ad26b3ac7cb Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk <yurapalamarchuk111@gmail.com> Date: Fri, 26 Jun 2026 15:58:31 +0200 Subject: [PATCH 13/14] Drop IRC, show Slack instead --- zookeeper-website/app/components/links.ts | 4 ++-- .../app/pages/_landing/home/community.tsx | 9 +++++---- .../app/pages/_landing/{irc => slack}/content.md | 12 +++++++----- .../app/pages/_landing/{irc => slack}/index.tsx | 2 +- zookeeper-website/app/routes.ts | 2 +- .../app/routes/_landing/{irc.tsx => slack.tsx} | 12 ++++++------ 6 files changed, 22 insertions(+), 19 deletions(-) rename zookeeper-website/app/pages/_landing/{irc => slack}/content.md (60%) rename zookeeper-website/app/pages/_landing/{irc => slack}/index.tsx (96%) rename zookeeper-website/app/routes/_landing/{irc.tsx => slack.tsx} (76%) diff --git a/zookeeper-website/app/components/links.ts b/zookeeper-website/app/components/links.ts index 36073b14a4d..97fa3d8be3a 100644 --- a/zookeeper-website/app/components/links.ts +++ b/zookeeper-website/app/components/links.ts @@ -89,8 +89,8 @@ export const documentationLinks: (LinkType | NestedLinkType)[] = [ external: true }, { - label: "IRC Channel", - to: "/irc" + label: "Slack Channel", + to: "/slack" } ] } diff --git a/zookeeper-website/app/pages/_landing/home/community.tsx b/zookeeper-website/app/pages/_landing/home/community.tsx index 186f2a3fd64..82cf9d74f48 100644 --- a/zookeeper-website/app/pages/_landing/home/community.tsx +++ b/zookeeper-website/app/pages/_landing/home/community.tsx @@ -67,16 +67,17 @@ export function CommunitySection() { <ArrowUpRight className="text-muted-foreground group-hover:text-primary absolute top-6 right-6 size-5 transition-colors" /> </Link> </li> - <li id="irc" className="relative p-0"> + <li id="slack" className="relative p-0"> <Link - to="/irc" + to="/slack" className="group border-border/60 bg-card/50 hover:bg-card hover:border-primary/50 focus-visible:ring-primary block rounded-2xl border p-6 shadow-sm transition-all duration-200 hover:shadow-md focus-visible:ring-2 focus-visible:outline-none" > <span className="text-foreground text-lg font-semibold"> - IRC Channel + Slack Channel </span> <p className="text-muted-foreground mt-1 text-base"> - Chat with the community on #zookeeper at irc.libera.chat. + Chat with the community on #zookeeper in the ASF Slack + workspace. </p> <ArrowUpRight className="text-muted-foreground group-hover:text-primary absolute top-6 right-6 size-5 transition-colors" /> </Link> diff --git a/zookeeper-website/app/pages/_landing/irc/content.md b/zookeeper-website/app/pages/_landing/slack/content.md similarity index 60% rename from zookeeper-website/app/pages/_landing/irc/content.md rename to zookeeper-website/app/pages/_landing/slack/content.md index abc652be2f0..fd2f0bdce1b 100644 --- a/zookeeper-website/app/pages/_landing/irc/content.md +++ b/zookeeper-website/app/pages/_landing/slack/content.md @@ -16,15 +16,17 @@ See the License for the specific language governing permissions and limitations under the License. --> -# IRC Channel +# Slack Channel -The Apache ZooKeeper community uses IRC for real-time chat and informal discussion. +The Apache ZooKeeper community uses the ASF Slack workspace for real-time chat and informal discussion. ## Channel -**Server:** `irc.libera.chat` +**Workspace:** [the-asf.slack.com](https://the-asf.slack.com/) **Channel:** `#zookeeper` -## Connecting +## Joining -The IRC channel can be used for online discussion about zookeeper related stuff, but developers should be careful to transfer all the official decisions or useful discussions to the issue tracking system. +See the [ASF Infrastructure guide to Slack](https://infra.apache.org/slack.html) for instructions on how to join the workspace. + +The Slack channel can be used for online discussion about ZooKeeper related topics, but developers should be careful to transfer all official decisions or useful discussions to the issue tracking system. diff --git a/zookeeper-website/app/pages/_landing/irc/index.tsx b/zookeeper-website/app/pages/_landing/slack/index.tsx similarity index 96% rename from zookeeper-website/app/pages/_landing/irc/index.tsx rename to zookeeper-website/app/pages/_landing/slack/index.tsx index e20bde1ad2b..ee662ec4628 100644 --- a/zookeeper-website/app/pages/_landing/irc/index.tsx +++ b/zookeeper-website/app/pages/_landing/slack/index.tsx @@ -19,6 +19,6 @@ import { MdLayout } from "@/components/mdx-components"; import Content from "./content.md"; -export function IrcPage() { +export function SlackPage() { return <MdLayout Content={Content} className="mt-12" />; } diff --git a/zookeeper-website/app/routes.ts b/zookeeper-website/app/routes.ts index 57ba702aa2b..3dfb62c1e16 100644 --- a/zookeeper-website/app/routes.ts +++ b/zookeeper-website/app/routes.ts @@ -45,7 +45,7 @@ const landingRoutes = layout("./pages/_landing/landing-layout.tsx", [ route("bylaws", "routes/_landing/bylaws.tsx"), route("mailing-lists", "routes/_landing/mailing-lists.tsx"), route("security", "routes/_landing/security.tsx"), - route("irc", "routes/_landing/irc.tsx"), + route("slack", "routes/_landing/slack.tsx"), route("version-control", "routes/_landing/version-control.tsx") ]); diff --git a/zookeeper-website/app/routes/_landing/irc.tsx b/zookeeper-website/app/routes/_landing/slack.tsx similarity index 76% rename from zookeeper-website/app/routes/_landing/irc.tsx rename to zookeeper-website/app/routes/_landing/slack.tsx index 4a2243c1ef8..c9130774d6e 100644 --- a/zookeeper-website/app/routes/_landing/irc.tsx +++ b/zookeeper-website/app/routes/_landing/slack.tsx @@ -16,19 +16,19 @@ // limitations under the License. // -import type { Route } from "./+types/irc"; -import { IrcPage } from "@/pages/_landing/irc"; +import type { Route } from "./+types/slack"; +import { SlackPage } from "@/pages/_landing/slack"; export function meta({}: Route.MetaArgs) { return [ - { title: "IRC Channel - Apache ZooKeeper" }, + { title: "Slack Channel - Apache ZooKeeper" }, { name: "description", - content: "Connect with the Apache ZooKeeper community on IRC." + content: "Connect with the Apache ZooKeeper community on Slack." } ]; } -export default function Irc() { - return <IrcPage />; +export default function Slack() { + return <SlackPage />; } From 591e7603f100ab59325d6810e3b16bd9fcd5eaf1 Mon Sep 17 00:00:00 2001 From: Yurii Palamarchuk <yurapalamarchuk111@gmail.com> Date: Fri, 26 Jun 2026 16:00:10 +0200 Subject: [PATCH 14/14] Return Old docs menu --- .../app/components/site-navbar.tsx | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/zookeeper-website/app/components/site-navbar.tsx b/zookeeper-website/app/components/site-navbar.tsx index 64638562622..044c7fe2c0b 100644 --- a/zookeeper-website/app/components/site-navbar.tsx +++ b/zookeeper-website/app/components/site-navbar.tsx @@ -21,6 +21,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, @@ -36,6 +37,11 @@ import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { Link } from "@/components/link"; import { asfLinks, documentationLinks, projectLinks } from "./links"; +import { OlderDocsVersionList } from "@/components/docs/older-docs-picker"; +import { + getReleasedDocUrl, + getReleasedDocVersions +} from "@/lib/released-docs-versions"; import { ThemeToggle } from "./theme-toggle"; const navLinkClass = @@ -157,6 +163,17 @@ function ProjectMenu() { ); } +function OlderDocsSubMenu() { + return ( + <DropdownMenuSub> + <DropdownMenuSubTrigger>Older docs</DropdownMenuSubTrigger> + <DropdownMenuSubContent className="w-56 p-0"> + <OlderDocsVersionList /> + </DropdownMenuSubContent> + </DropdownMenuSub> + ); +} + function DocsMenu() { return ( <DropdownMenu> @@ -200,6 +217,8 @@ function DocsMenu() { </DropdownMenuSub> ) )} + <DropdownMenuSeparator /> + <OlderDocsSubMenu /> </DropdownMenuContent> </DropdownMenu> ); @@ -336,6 +355,20 @@ function MobileMenu() { ); } +function MobileOlderDocsSection() { + return ( + <Collapsible className="w-full"> + <CollapsibleTrigger className="text-muted-foreground hover:text-foreground flex w-full items-center justify-between py-1.5 text-left text-sm"> + Older docs + <ChevronRight className="h-3 w-3 rotate-90 transition-transform group-data-[state=closed]:rotate-0" /> + </CollapsibleTrigger> + <CollapsibleContent className="w-full pt-1 pl-3"> + <OlderDocsVersionList /> + </CollapsibleContent> + </Collapsible> + ); +} + function MobileDocsSection({ onLinkClick }: { onLinkClick: () => void }) { return ( <Collapsible className="w-full"> @@ -381,6 +414,7 @@ function MobileDocsSection({ onLinkClick }: { onLinkClick: () => void }) { </Collapsible> ) )} + <MobileOlderDocsSection /> </CollapsibleContent> </Collapsible> ); @@ -494,6 +528,31 @@ function NoJSDocsMenu() { </details> ) )} + <NoJSOlderDocsSubMenu /> + </div> + </details> + ); +} + +function NoJSOlderDocsSubMenu() { + const versions = getReleasedDocVersions(); + + return ( + <details className="group/sub relative"> + <summary className="hover:bg-accent hover:text-accent-foreground flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 text-sm outline-none select-none"> + Older docs + <ChevronRight className="ml-2 h-4 w-4" /> + </summary> + <div className="bg-popover text-popover-foreground absolute top-0 left-full z-50 ml-1 max-h-72 min-w-[12rem] overflow-y-auto rounded-md border p-1 shadow-md"> + {versions.map((version) => ( + <a + key={version} + href={getReleasedDocUrl(version)} + className="hover:bg-accent hover:text-accent-foreground relative flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none" + > + {version} + </a> + ))} </div> </details> ); @@ -619,6 +678,31 @@ function NoJSMobileDocsSection() { </details> ) )} + <NoJSMobileOlderDocsSection /> + </div> + </details> + ); +} + +function NoJSMobileOlderDocsSection() { + const versions = getReleasedDocVersions(); + + return ( + <details className="w-full"> + <summary className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center justify-between py-1.5 text-left text-sm"> + Older docs + <ChevronRight className="h-3 w-3" /> + </summary> + <div className="max-h-72 w-full space-y-1 overflow-y-auto pt-1 pl-3"> + {versions.map((version) => ( + <a + key={version} + href={getReleasedDocUrl(version)} + className="text-muted-foreground hover:text-foreground flex cursor-pointer items-center py-1 text-xs" + > + {version} + </a> + ))} </div> </details> );