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

PDavid pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/master by this push:
     new fc5b991f733 HBASE-30127 Improve Website's SEO (#8157)
fc5b991f733 is described below

commit fc5b991f733b62a33508cce86edb0e9dcf1dae4f
Author: Yurii Palamarchuk <[email protected]>
AuthorDate: Thu Jun 4 16:20:05 2026 +0200

    HBASE-30127 Improve Website's SEO (#8157)
    
    This change improves SEO (Search Engine Optimization) by adding proper 
cache headers, good looking 404 page, and sitemap.
    
    Signed-off-by: Dávid Paksy <[email protected]>
---
 hbase-website/.gitignore                           |   1 +
 hbase-website/README.md                            |  15 +-
 .../_mdx/(multi-page)/upgrading/version-number.mdx |  13 +-
 hbase-website/app/root.tsx                         |  47 +++-
 hbase-website/package-lock.json                    | 166 +++++++++++++-
 hbase-website/package.json                         |   6 +-
 hbase-website/public/404.html                      | 245 +++++++++++++++++++++
 hbase-website/public/robots.txt                    |   5 +
 hbase-website/scripts/generate-sitemap.ts          | 116 ++++++++++
 hbase-website/unit-tests/generate-sitemap.test.ts  |  90 ++++++++
 pom.xml                                            |   2 +
 11 files changed, 677 insertions(+), 29 deletions(-)

diff --git a/hbase-website/.gitignore b/hbase-website/.gitignore
index 00909066a6f..76974dfd287 100644
--- a/hbase-website/.gitignore
+++ b/hbase-website/.gitignore
@@ -39,6 +39,7 @@ lerna-debug.log*
 /app/pages/_docs/docs/_mdx/(multi-page)/configuration/hbase-default.md
 /app/lib/export-pdf/hbase-version.json
 /public/books/**
+/public/sitemap.xml
 
 # Playwright
 node_modules/
diff --git a/hbase-website/README.md b/hbase-website/README.md
index 0685e87f637..cbc365b8966 100644
--- a/hbase-website/README.md
+++ b/hbase-website/README.md
@@ -476,16 +476,18 @@ When you run `mvn site`, the website module automatically:
    - `npm run extract-hbase-config` - Extract data from `hbase-default.xml` to 
`app/pages/_docs/docs/_mdx/(multi-page)/configuration/hbase-default.md`
    - `npm run extract-hbase-version` - Extract version from root `pom.xml` to 
`app/lib/export-pdf/hbase-version.json`
    - `npm run test:unit:run` - Vitest unit tests
-   - `npm run test:e2e` - Playwright e2e tests
    - `npm run build` - Production build
+   - `npm run generate-sitemap` - Generates `public/sitemap.xml` and 
`build/client/sitemap.xml`
+   - `npm run test:e2e` - Playwright e2e tests
 
    `npm run ci-skip-tests` executes:
    - `npm run extract-developers` - Extract developers from parent pom.xml
    - `npm run extract-hbase-config` - Extract data from `hbase-default.xml` to 
`app/pages/_docs/docs/_mdx/(multi-page)/configuration/hbase-default.md`
    - `npm run extract-hbase-version` - Extract version from root `pom.xml` to 
`app/lib/export-pdf/hbase-version.json`
+   - `npm run build` - Production build
+   - `npm run generate-sitemap` - Generates `public/sitemap.xml` and 
`build/client/sitemap.xml`
    - `npx playwright install` - Installs Playwright browsers
    - `npm run export-pdf` - Generates docs PDF assets through Playwright
-   - `npm run build` - Production build
 
 6. **Build Output**: Generated files are in `build/` directory
 
@@ -535,6 +537,15 @@ mvn clean install -DskipSite
 
 ### Deployment
 
+#### Update the 404 Page
+
+- Edit the standalone static page in `public/404.html`.
+- The static 404 page supports dark mode without React: it applies the saved
+  `localStorage.theme` value when present and otherwise falls back to
+  `prefers-color-scheme`.
+- `public/robots.txt` excludes `/404.html` from crawlers.
+- `scripts/generate-sitemap.ts` excludes `404.html` from generated sitemaps.
+
 #### Static Hosting
 
 Since this site uses Static Site Generation (SSG), you can deploy the 
`build/client/` directory to any static file host:
diff --git 
a/hbase-website/app/pages/_docs/docs/_mdx/(multi-page)/upgrading/version-number.mdx
 
b/hbase-website/app/pages/_docs/docs/_mdx/(multi-page)/upgrading/version-number.mdx
index 000143c2004..4735581c7d3 100644
--- 
a/hbase-website/app/pages/_docs/docs/_mdx/(multi-page)/upgrading/version-number.mdx
+++ 
b/hbase-website/app/pages/_docs/docs/_mdx/(multi-page)/upgrading/version-number.mdx
@@ -88,13 +88,18 @@ In addition to the usual API versioning considerations 
HBase has other compatibi
 - A minor upgrade requires no application/client code modification. Ideally it 
would be a drop-in replacement but client code, coprocessors, filters, etc 
might have to be recompiled if new jars are used.
 - A major upgrade allows the HBase community to make breaking changes.
 
-#### Compatibility Matrix: [^3] [!toc]
+#### Compatibility Matrix: [!toc]
+
+<Callout type="warn">
+  *Please note, this indicates what could break, not that it will break. We 
will/should add
+  specifics in our release notes.*
+</Callout>
 
 |                                           | Major  | Minor | Patch |
 | ----------------------------------------- | :----: | :---: | :---: |
 | Client-Server wire Compatibility          |   N    |   Y   |   Y   |
 | Server-Server Compatibility               |   N    |   Y   |   Y   |
-| File Format Compatibility                 | N [^4] |   Y   |   Y   |
+| File Format Compatibility                 | N [^3] |   Y   |   Y   |
 | Client API Compatibility                  |   N    |   Y   |   Y   |
 | Client Binary Compatibility               |   N    |   N   |   Y   |
 | **Server-Side Limited API Compatibility** |        |       |       |
@@ -149,6 +154,4 @@ When we say two HBase versions are compatible, we mean that 
the versions are wir
 
 [^2]: See http://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html.
 
-[^3]: Note that this indicates what could break, not that it will break. We 
will/should add specifics in our release notes.
-
-[^4]: Running an offline upgrade tool without downgrade might be needed. We 
will typically only support migrating data from major version X to major 
version X+1.
+[^3]: Running an offline upgrade tool without downgrade might be needed. We 
will typically only support migrating data from major version X to major 
version X+1.
diff --git a/hbase-website/app/root.tsx b/hbase-website/app/root.tsx
index dda5666f9b2..e80df149884 100644
--- a/hbase-website/app/root.tsx
+++ b/hbase-website/app/root.tsx
@@ -29,6 +29,7 @@ import type { Route } from "./+types/root";
 import appStyles from "./app.css?url";
 import "katex/dist/katex.css";
 import { ThemeProvider } from "./lib/theme-provider";
+import { Button } from "./ui/button";
 
 export const links: Route.LinksFunction = () => [
   {
@@ -96,12 +97,14 @@ export default function App() {
 }
 
 export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
-  let message = "Oops!";
+  let eyebrow = "Error";
+  let message = "Something went wrong";
   let details = "An unexpected error occurred.";
   let stack: string | undefined;
 
   if (isRouteErrorResponse(error)) {
-    message = error.status === 404 ? "404" : "Error";
+    eyebrow = String(error.status);
+    message = error.status === 404 ? "Page not found" : "Request failed";
     details =
       error.status === 404 ? "The requested page could not be found." : 
error.statusText || details;
   } else if (import.meta.env.DEV && error && error instanceof Error) {
@@ -110,14 +113,38 @@ export function ErrorBoundary({ error }: 
Route.ErrorBoundaryProps) {
   }
 
   return (
-    <main className="container mx-auto p-4 pt-16">
-      <h1>{message}</h1>
-      <p>{details}</p>
-      {stack && (
-        <pre className="w-full overflow-x-auto p-4">
-          <code>{stack}</code>
-        </pre>
-      )}
+    <main className="grid min-h-screen place-items-center 
bg-[radial-gradient(circle_at_top,rgba(186,22,12,0.08),transparent_32rem)] px-4 
py-16">
+      <section
+        className="mx-auto flex w-full max-w-2xl flex-col items-center 
text-center"
+        aria-labelledby="error-title"
+      >
+        <img className="mb-8 h-auto w-36" src="/images/logo.svg" alt="Apache 
HBase" />
+        <p className="text-muted-foreground text-sm font-semibold 
tracking-[0.3em] uppercase">
+          {eyebrow}
+        </p>
+        <h1
+          id="error-title"
+          className="mt-4 text-4xl font-semibold tracking-tight text-balance 
md:text-6xl"
+        >
+          {message}
+        </h1>
+        <p className="text-muted-foreground mt-5 max-w-xl text-lg leading-8 
text-pretty md:text-xl">
+          {details}
+        </p>
+        <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
+          <Button asChild size="lg">
+            <a href="/">Go back home</a>
+          </Button>
+          <Button asChild variant="outline" size="lg">
+            <a href="/docs/">Read documentation</a>
+          </Button>
+        </div>
+        {stack && (
+          <pre className="bg-muted/50 text-muted-foreground border-border mt-8 
max-h-80 w-full overflow-x-auto rounded-lg border p-4 text-left text-sm">
+            <code>{stack}</code>
+          </pre>
+        )}
+      </section>
     </main>
   );
 }
diff --git a/hbase-website/package-lock.json b/hbase-website/package-lock.json
index 59ad6848799..9b8eaef3122 100644
--- a/hbase-website/package-lock.json
+++ b/hbase-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": "*",
+        "@radix-ui/react-slot": "latest",
         "@radix-ui/react-tabs": "1.1.2",
         "@radix-ui/react-tooltip": "1.1.6",
         "@react-router/node": "^7.12.0",
@@ -84,6 +84,7 @@
         "prettier": "^3.6.2",
         "prettier-plugin-tailwindcss": "^0.6.14",
         "serve": "^14.2.6",
+        "sitemap": "^9.0.1",
         "tailwindcss": "^4.1.13",
         "tsx": "^4.21.0",
         "tw-animate-css": "1.3.3",
@@ -3573,6 +3574,7 @@
       "cpu": [
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3586,6 +3588,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3599,6 +3602,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3612,6 +3616,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3625,6 +3630,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3638,6 +3644,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3651,6 +3658,7 @@
       "cpu": [
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3664,6 +3672,7 @@
       "cpu": [
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3677,6 +3686,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3690,6 +3700,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3703,6 +3714,7 @@
       "cpu": [
         "loong64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3716,6 +3728,7 @@
       "cpu": [
         "loong64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3729,6 +3742,7 @@
       "cpu": [
         "ppc64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3742,6 +3756,7 @@
       "cpu": [
         "ppc64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3755,6 +3770,7 @@
       "cpu": [
         "riscv64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3768,6 +3784,7 @@
       "cpu": [
         "riscv64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3781,6 +3798,7 @@
       "cpu": [
         "s390x"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3794,6 +3812,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3807,6 +3826,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3820,6 +3840,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3833,6 +3854,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3846,6 +3868,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3859,6 +3882,7 @@
       "cpu": [
         "ia32"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3872,6 +3896,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -3885,6 +3910,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -4585,7 +4611,7 @@
       "version": "22.18.11",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.11.tgz";,
       "integrity": 
"sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "undici-types": "~6.21.0"
@@ -4610,6 +4636,16 @@
         "@types/react": "^19.2.0"
       }
     },
+    "node_modules/@types/sax": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz";,
+      "integrity": 
"sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/unist": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz";,
@@ -6666,7 +6702,7 @@
       "version": "2.1.2",
       "resolved": 
"https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz";,
       "integrity": 
"sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "Apache-2.0",
       "engines": {
         "node": ">=8"
@@ -7866,6 +7902,7 @@
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz";,
       "integrity": 
"sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -8863,7 +8900,7 @@
       "version": "4.12.0",
       "resolved": 
"https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz";,
       "integrity": 
"sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "resolve-pkg-maps": "^1.0.0"
@@ -10124,7 +10161,7 @@
       "version": "2.6.1",
       "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz";,
       "integrity": 
"sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "bin": {
         "jiti": "lib/jiti-cli.mjs"
@@ -10300,7 +10337,7 @@
       "version": "1.30.1",
       "resolved": 
"https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz";,
       "integrity": 
"sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
-      "devOptional": true,
+      "dev": true,
       "license": "MPL-2.0",
       "dependencies": {
         "detect-libc": "^2.0.3"
@@ -10332,6 +10369,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -10352,6 +10390,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "optional": true,
       "os": [
         "darwin"
@@ -10371,6 +10410,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "optional": true,
       "os": [
         "freebsd"
@@ -10390,6 +10430,7 @@
       "cpu": [
         "arm"
       ],
+      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -10409,6 +10450,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -10428,6 +10470,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -10447,6 +10490,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -10466,6 +10510,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -10485,6 +10530,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -10504,6 +10550,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -13380,7 +13427,7 @@
       "version": "1.0.0",
       "resolved": 
"https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz";,
       "integrity": 
"sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "funding": {
         "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1";
@@ -13518,6 +13565,16 @@
       "integrity": 
"sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
       "license": "MIT"
     },
+    "node_modules/sax": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz";,
+      "integrity": 
"sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=11.0.0"
+      }
+    },
     "node_modules/saxes": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz";,
@@ -13929,6 +13986,43 @@
       "integrity": 
"sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
       "dev": true
     },
+    "node_modules/sitemap": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz";,
+      "integrity": 
"sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "^24.9.2",
+        "@types/sax": "^1.2.1",
+        "arg": "^5.0.0",
+        "sax": "^1.4.1"
+      },
+      "bin": {
+        "sitemap": "dist/esm/cli.js"
+      },
+      "engines": {
+        "node": ">=20.19.5",
+        "npm": ">=10.8.2"
+      }
+    },
+    "node_modules/sitemap/node_modules/@types/node": {
+      "version": "24.12.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz";,
+      "integrity": 
"sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/sitemap/node_modules/undici-types": {
+      "version": "7.16.0",
+      "resolved": 
"https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz";,
+      "integrity": 
"sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/source-map": {
       "version": "0.6.1",
       "resolved": 
"https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz";,
@@ -14469,7 +14563,7 @@
       "version": "4.21.0",
       "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz";,
       "integrity": 
"sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "esbuild": "~0.27.0",
@@ -14492,6 +14586,7 @@
       "cpu": [
         "ppc64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14508,6 +14603,7 @@
       "cpu": [
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14524,6 +14620,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14540,6 +14637,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14556,6 +14654,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14572,6 +14671,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14588,6 +14688,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14604,6 +14705,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14620,6 +14722,7 @@
       "cpu": [
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14636,6 +14739,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14652,6 +14756,7 @@
       "cpu": [
         "ia32"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14668,6 +14773,7 @@
       "cpu": [
         "loong64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14684,6 +14790,7 @@
       "cpu": [
         "mips64el"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14700,6 +14807,7 @@
       "cpu": [
         "ppc64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14716,6 +14824,7 @@
       "cpu": [
         "riscv64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14732,6 +14841,7 @@
       "cpu": [
         "s390x"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14748,6 +14858,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14764,6 +14875,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14780,6 +14892,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14796,6 +14909,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14812,6 +14926,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14828,6 +14943,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14844,6 +14960,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14860,6 +14977,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14876,6 +14994,7 @@
       "cpu": [
         "ia32"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14892,6 +15011,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -14905,7 +15025,7 @@
       "version": "0.27.2",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz";,
       "integrity": 
"sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
-      "devOptional": true,
+      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
       "bin": {
@@ -15131,7 +15251,7 @@
       "version": "6.21.0",
       "resolved": 
"https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz";,
       "integrity": 
"sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/unified": {
@@ -15613,6 +15733,7 @@
       "cpu": [
         "ppc64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15629,6 +15750,7 @@
       "cpu": [
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15645,6 +15767,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15661,6 +15784,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15677,6 +15801,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15693,6 +15818,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15709,6 +15835,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15725,6 +15852,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15741,6 +15869,7 @@
       "cpu": [
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15757,6 +15886,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15773,6 +15903,7 @@
       "cpu": [
         "ia32"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15789,6 +15920,7 @@
       "cpu": [
         "loong64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15805,6 +15937,7 @@
       "cpu": [
         "mips64el"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15821,6 +15954,7 @@
       "cpu": [
         "ppc64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15837,6 +15971,7 @@
       "cpu": [
         "riscv64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15853,6 +15988,7 @@
       "cpu": [
         "s390x"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15869,6 +16005,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15885,6 +16022,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15901,6 +16039,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15917,6 +16056,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15933,6 +16073,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15949,6 +16090,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15965,6 +16107,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15981,6 +16124,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -15997,6 +16141,7 @@
       "cpu": [
         "ia32"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -16013,6 +16158,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
diff --git a/hbase-website/package.json b/hbase-website/package.json
index df0766bb765..ce3a8297765 100644
--- a/hbase-website/package.json
+++ b/hbase-website/package.json
@@ -25,11 +25,12 @@
     "extract-hbase-config": "node scripts/extract-hbase-config.js",
     "extract-hbase-version": "node scripts/extract-hbase-version.js",
     "extract-all": "node scripts/extract-developers.js && node 
scripts/extract-hbase-config.js && node scripts/extract-hbase-version.js",
+    "generate-sitemap": "tsx scripts/generate-sitemap.ts",
     "export-pdf": "playwright test e2e-tests/export-pdf.spec.ts 
--project=chromium --workers=1",
     "fumadocs-init": "fumadocs-mdx",
     "copy-pdf-to-build": "cp -r public/books build/client",
-    "ci": "npm run extract-all && npm run fumadocs-init && npm run lint && npm 
run typecheck && npm run test:unit:run && npm run build && npx playwright 
install chromium firefox && npm run test:e2e && npm run copy-pdf-to-build",
-    "ci-skip-tests": "npm run extract-all && npm run fumadocs-init && npm run 
build && npx playwright install chromium && npm run export-pdf && npm run 
copy-pdf-to-build"
+    "ci": "npm run extract-all && npm run fumadocs-init && npm run lint && npm 
run typecheck && npm run test:unit:run && npm run build && npm run 
generate-sitemap && npx playwright install chromium firefox && npm run test:e2e 
&& npm run copy-pdf-to-build",
+    "ci-skip-tests": "npm run extract-all && npm run fumadocs-init && npm run 
build && npm run generate-sitemap && npx playwright install chromium && npm run 
export-pdf && npm run copy-pdf-to-build"
   },
   "dependencies": {
     "@hookform/resolvers": "^3.10.0",
@@ -110,6 +111,7 @@
     "prettier": "^3.6.2",
     "prettier-plugin-tailwindcss": "^0.6.14",
     "serve": "^14.2.6",
+    "sitemap": "^9.0.1",
     "tailwindcss": "^4.1.13",
     "tsx": "^4.21.0",
     "tw-animate-css": "1.3.3",
diff --git a/hbase-website/public/404.html b/hbase-website/public/404.html
new file mode 100644
index 00000000000..00740830f00
--- /dev/null
+++ b/hbase-website/public/404.html
@@ -0,0 +1,245 @@
+<!doctype html>
+<!--
+  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.
+-->
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="robots" content="noindex, nofollow" />
+    <title>Page Not Found | Apache HBase</title>
+    <style>
+      @font-face {
+        font-family: Inter;
+        font-style: normal;
+        font-weight: 100 900;
+        font-display: swap;
+        src: url("/fonts/inter-latin-wght-normal.woff2") format("woff2");
+      }
+
+      :root {
+        color-scheme: light;
+        --radius: 0.625rem;
+        --background: oklch(1 0 0);
+        --foreground: oklch(0.145 0 0);
+        --muted-foreground: oklch(0.556 0 0);
+        --border: oklch(0.922 0 0);
+        --primary: oklch(50.312% 0.19572 29.534);
+        --primary-strong: oklch(50.312% 0.19572 29.534);
+        --primary-foreground: oklch(0.985 0 0);
+        --ring: oklch(0.708 0 0);
+      }
+
+      @media (prefers-color-scheme: dark) {
+        :root {
+          color-scheme: dark;
+          --background: oklch(0.17 0 0);
+          --foreground: oklch(0.92 0 0);
+          --muted-foreground: oklch(0.62 0 0);
+          --border: oklch(0.3 0 0);
+          --primary: oklch(54% 0.15 29.5);
+          --primary-strong: oklch(53% 0.17 29.5);
+          --primary-foreground: oklch(97% 0 0);
+          --ring: oklch(50.312% 0.19572 29.534);
+        }
+      }
+
+      html.light {
+        color-scheme: light;
+        --background: oklch(1 0 0);
+        --foreground: oklch(0.145 0 0);
+        --muted-foreground: oklch(0.556 0 0);
+        --border: oklch(0.922 0 0);
+        --primary: oklch(50.312% 0.19572 29.534);
+        --primary-strong: oklch(50.312% 0.19572 29.534);
+        --primary-foreground: oklch(0.985 0 0);
+        --ring: oklch(0.708 0 0);
+      }
+
+      html.dark {
+        color-scheme: dark;
+        --background: oklch(0.17 0 0);
+        --foreground: oklch(0.92 0 0);
+        --muted-foreground: oklch(0.62 0 0);
+        --border: oklch(0.3 0 0);
+        --primary: oklch(54% 0.15 29.5);
+        --primary-strong: oklch(53% 0.17 29.5);
+        --primary-foreground: oklch(97% 0 0);
+        --ring: oklch(50.312% 0.19572 29.534);
+      }
+
+      * {
+        box-sizing: border-box;
+      }
+
+      body {
+        min-height: 100vh;
+        margin: 0;
+        background:
+          radial-gradient(circle at top, rgba(186, 22, 12, 0.08), transparent 
32rem),
+          var(--background);
+        color: var(--foreground);
+        font-family:
+          Inter,
+          ui-sans-serif,
+          system-ui,
+          -apple-system,
+          BlinkMacSystemFont,
+          "Segoe UI",
+          sans-serif;
+      }
+
+      main {
+        display: grid;
+        min-height: 100vh;
+        place-items: center;
+        padding: 2rem;
+      }
+
+      .content {
+        width: min(100%, 42rem);
+        padding: clamp(2rem, 6vw, 4rem);
+        text-align: center;
+      }
+
+      .logo {
+        width: 9rem;
+        height: auto;
+        margin-bottom: 2rem;
+      }
+
+      .eyebrow {
+        margin: 0;
+        color: var(--muted-foreground);
+        font-size: 1rem;
+        font-weight: 700;
+        letter-spacing: 0.3em;
+        text-transform: uppercase;
+      }
+
+      h1 {
+        margin: 1rem 0 0;
+        font-size: clamp(2.5rem, 8vw, 4.5rem);
+        line-height: 1;
+        letter-spacing: -0.05em;
+      }
+
+      .message {
+        margin: 1.25rem auto 0;
+        max-width: 34rem;
+        color: var(--muted-foreground);
+        font-size: clamp(1.125rem, 2vw, 1.25rem);
+        line-height: 1.65;
+      }
+
+      .actions {
+        display: flex;
+        justify-content: center;
+        margin-top: 2rem;
+      }
+
+      .button {
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        gap: 0.5rem;
+        height: 2.5rem;
+        padding: 0.5rem 1.5rem;
+        border: 1px solid transparent;
+        border-radius: calc(var(--radius) - 2px);
+        background: var(--primary);
+        color: var(--primary-foreground);
+        cursor: pointer;
+        font-size: 0.875rem;
+        font-weight: 500;
+        line-height: 1.25rem;
+        text-decoration: none;
+        white-space: nowrap;
+        box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+        outline: none;
+        transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
+      }
+
+      .button:hover {
+        background: color-mix(in oklch, var(--primary) 90%, transparent);
+      }
+
+      html.dark .button {
+        background: var(--primary-strong);
+      }
+
+      html.dark .button:hover {
+        background: color-mix(in oklch, var(--primary-strong) 90%, 
transparent);
+      }
+
+      @media (prefers-color-scheme: dark) {
+        .button {
+          background: var(--primary-strong);
+        }
+
+        .button:hover {
+          background: color-mix(in oklch, var(--primary-strong) 90%, 
transparent);
+        }
+      }
+
+      html.light .button {
+        background: var(--primary);
+      }
+
+      html.light .button:hover {
+        background: color-mix(in oklch, var(--primary) 90%, transparent);
+      }
+
+      .button:focus-visible {
+        border-color: var(--ring);
+        box-shadow:
+          0 1px 2px 0 rgb(0 0 0 / 0.05),
+          0 0 0 3px color-mix(in oklch, var(--ring) 50%, transparent);
+      }
+    </style>
+    <script>
+      (function () {
+        try {
+          var theme = localStorage.getItem("theme");
+          var root = document.documentElement;
+
+          if (theme === "light" || theme === "dark") {
+            root.classList.add(theme);
+          }
+        } catch (_) {
+          // Keep the CSS prefers-color-scheme fallback if storage is 
unavailable.
+        }
+      })();
+    </script>
+  </head>
+  <body>
+    <main>
+      <section class="content" aria-labelledby="page-title">
+        <img class="logo" src="/images/logo.svg" alt="Apache HBase" />
+        <p class="eyebrow">404</p>
+        <h1 id="page-title">Page not found</h1>
+        <p class="message">
+          The website was updated recently, so the route you were trying to 
visit might have
+          changed.
+        </p>
+        <div class="actions">
+          <a class="button" href="/">Go back home</a>
+        </div>
+      </section>
+    </main>
+  </body>
+</html>
diff --git a/hbase-website/public/robots.txt b/hbase-website/public/robots.txt
new file mode 100644
index 00000000000..ddb46af05bd
--- /dev/null
+++ b/hbase-website/public/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Allow: /
+Disallow: /404.html
+
+Sitemap: https://hbase.apache.org/sitemap.xml
diff --git a/hbase-website/scripts/generate-sitemap.ts 
b/hbase-website/scripts/generate-sitemap.ts
new file mode 100644
index 00000000000..2d441776e2d
--- /dev/null
+++ b/hbase-website/scripts/generate-sitemap.ts
@@ -0,0 +1,116 @@
+//
+// 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 { access, glob, readFile, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { SitemapStream, streamToPromise } from "sitemap";
+
+const ROOT = join(import.meta.dirname, "..");
+const PUBLIC_DIR = join(ROOT, "public");
+const BUILD_DIR = join(ROOT, "build", "client");
+const PUBLIC_SITEMAP_PATH = join(PUBLIC_DIR, "sitemap.xml");
+const BUILD_SITEMAP_PATH = join(BUILD_DIR, "sitemap.xml");
+const SITE_URL = "https://hbase.apache.org";;
+
+const EXCLUDED_HTML_PATHS = new Set(["404.html", "__spa-fallback.html"]);
+
+const REDIRECT_PAGE_PATTERNS = [
+  /window\.location\.replace\(/i,
+  /http-equiv=["']refresh/i,
+  />Redirecting to .*?If it does not happen automatically,/is
+];
+
+interface SitemapEntry {
+  url: string;
+}
+
+export function normalizePath(relativePath: string): string {
+  return relativePath.replaceAll("\\", "/");
+}
+
+export function toSiteUrl(relativePath: string): string {
+  if (relativePath === "index.html") {
+    return "/";
+  }
+
+  if (relativePath.endsWith("/index.html")) {
+    return `/${relativePath.slice(0, -"/index.html".length)}/`;
+  }
+
+  return `/${relativePath}`;
+}
+
+export function isRedirectOnlyPage(html: string): boolean {
+  return REDIRECT_PAGE_PATTERNS.some((pattern) => pattern.test(html));
+}
+
+export function shouldIncludeInSitemap(relativePath: string, html: string): 
boolean {
+  if (EXCLUDED_HTML_PATHS.has(relativePath)) {
+    return false;
+  }
+
+  return !isRedirectOnlyPage(html);
+}
+
+export async function collectSitemapEntries(): Promise<SitemapEntry[]> {
+  const entries: SitemapEntry[] = [];
+
+  for await (const htmlPath of glob("**/*.html", { cwd: BUILD_DIR })) {
+    const relativePath = normalizePath(htmlPath);
+    const filePath = join(BUILD_DIR, relativePath);
+    const html = await readFile(filePath, "utf8");
+
+    if (!shouldIncludeInSitemap(relativePath, html)) {
+      continue;
+    }
+
+    entries.push({
+      url: toSiteUrl(relativePath)
+    });
+  }
+
+  entries.sort((left, right) => left.url.localeCompare(right.url));
+  return entries;
+}
+
+export async function main() {
+  await access(BUILD_DIR);
+
+  const entries = await collectSitemapEntries();
+  const sitemap = new SitemapStream({ hostname: SITE_URL });
+
+  for (const entry of entries) {
+    sitemap.write(entry);
+  }
+
+  sitemap.end();
+
+  const xml = `${(await streamToPromise(sitemap)).toString().trimEnd()}\n`;
+  await Promise.all([writeFile(PUBLIC_SITEMAP_PATH, xml), 
writeFile(BUILD_SITEMAP_PATH, xml)]);
+
+  console.log(
+    `Generated sitemap with ${entries.length} URLs at ${PUBLIC_SITEMAP_PATH} 
and ${BUILD_SITEMAP_PATH}`
+  );
+}
+
+const isDirectRun = process.argv[1] === fileURLToPath(import.meta.url);
+
+if (isDirectRun) {
+  await main();
+}
diff --git a/hbase-website/unit-tests/generate-sitemap.test.ts 
b/hbase-website/unit-tests/generate-sitemap.test.ts
new file mode 100644
index 00000000000..8dcf0e41625
--- /dev/null
+++ b/hbase-website/unit-tests/generate-sitemap.test.ts
@@ -0,0 +1,90 @@
+//
+// 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, expect, it } from "vitest";
+import {
+  isRedirectOnlyPage,
+  normalizePath,
+  shouldIncludeInSitemap,
+  toSiteUrl
+} from "../scripts/generate-sitemap";
+
+describe("normalizePath", () => {
+  it("preserves forward-slash paths", () => {
+    expect(normalizePath("docs/index.html")).toBe("docs/index.html");
+  });
+
+  it("converts windows separators to forward slashes", () => {
+    
expect(normalizePath("docs\\configuration\\index.html")).toBe("docs/configuration/index.html");
+  });
+});
+
+describe("toSiteUrl", () => {
+  it("maps the root index file to slash", () => {
+    expect(toSiteUrl("index.html")).toBe("/");
+  });
+
+  it("maps nested index files to trailing-slash URLs", () => {
+    
expect(toSiteUrl("docs/configuration/index.html")).toBe("/docs/configuration/");
+  });
+
+  it("preserves non-index html filenames", () => {
+    expect(toSiteUrl("llms-full.txt.html")).toBe("/llms-full.txt.html");
+  });
+});
+
+describe("isRedirectOnlyPage", () => {
+  it("detects javascript redirect pages", () => {
+    
expect(isRedirectOnlyPage('<script>window.location.replace("/docs/")</script>')).toBe(true);
+  });
+
+  it("detects meta refresh redirects", () => {
+    expect(isRedirectOnlyPage('<meta http-equiv="refresh" content="0; 
url=/downloads/" />')).toBe(
+      true
+    );
+  });
+
+  it("does not flag ordinary html pages", () => {
+    expect(isRedirectOnlyPage("<html><body><h1>Apache 
HBase</h1></body></html>")).toBe(false);
+  });
+});
+
+describe("shouldIncludeInSitemap", () => {
+  it("excludes explicitly ignored html paths", () => {
+    expect(shouldIncludeInSitemap("404.html", "<html></html>")).toBe(false);
+    expect(shouldIncludeInSitemap("__spa-fallback.html", 
"<html></html>")).toBe(false);
+  });
+
+  it("excludes redirect-only pages even if the path is not prelisted", () => {
+    expect(
+      shouldIncludeInSitemap(
+        "docs/redirect/index.html",
+        '<script>window.location.replace("/docs/")</script>'
+      )
+    ).toBe(false);
+  });
+
+  it("includes normal prerendered pages", () => {
+    expect(
+      shouldIncludeInSitemap(
+        "docs/configuration/index.html",
+        "<html><body><h1>Configuration</h1></body></html>"
+      )
+    ).toBe(true);
+  });
+});
diff --git a/pom.xml b/pom.xml
index b782a160311..e47de81da76 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2144,6 +2144,8 @@
               <exclude>**/public/old-book-static-files/hbase.css</exclude>
               <!-- http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT 
License -->
               
<exclude>**/public/old-book-static-files/font-awesome.css</exclude>
+              <!-- Website SEO metadata file cannot carry an ASF license 
header. -->
+              <exclude>**/public/robots.txt</exclude>
               <!-- vector graphics -->
               <exclude>**/*.vm</exclude>
               <!-- apache doxia generated -->


Reply via email to