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

richardantal pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/phoenix-site.git


The following commit(s) were added to refs/heads/asf-site by this push:
     new baf4c1ad PHOENIX-7796: Update website caching (#8)
baf4c1ad is described below

commit baf4c1adf69d85abbd915e9980f31cf0901a2fbc
Author: Yurii Palamarchuk <[email protected]>
AuthorDate: Mon Apr 13 13:48:59 2026 +0200

    PHOENIX-7796: Update website caching (#8)
---
 README.md                                          |   6 +-
 output/.htaccess                                   |   8 ++
 .../apache-phoenix-reference-guide-dark-mode.pdf   | Bin 13733447 -> 13733440 
bytes
 output/books/apache-phoenix-reference-guide.pdf    | Bin 13704436 -> 13704433 
bytes
 output/robots.txt                                  |   4 +
 output/sitemap.xml                                 |   1 +
 package-lock.json                                  |  58 ++++++++++
 package.json                                       |   4 +-
 public/.htaccess                                   |   8 ++
 public/robots.txt                                  |   4 +
 scripts/generate-sitemap.ts                        | 126 +++++++++++++++++++++
 unit-tests/generate-sitemap.test.ts                | 106 +++++++++++++++++
 12 files changed, 321 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index b8de2778..89db962a 100644
--- a/README.md
+++ b/README.md
@@ -315,7 +315,7 @@ Before opening a pull request, you must run the full 
website build script:
 1. Ensures Node/npm are available (bootstraps via `nvm` when needed)
 2. Runs a clean dependency install (`npm ci`)
 3. Runs the complete CI pipeline (`npm run ci`)
-4. Copies `build/client/` into `output/`
+4. Copies `build/client/` into `output/` (including generated SEO artifacts 
like `sitemap.xml`)
 
 Current CI sequence used by `npm run ci`:
 
@@ -323,7 +323,7 @@ Current CI sequence used by `npm run ci`:
 2. `npm run fumadocs-init`
 3. `npm run lint`
 4. `npm run typecheck`
-5. `npm run build`
+5. `npm run build` (`react-router build` followed by sitemap generation)
 6. `npm run test:unit:run`
 7. `npx playwright install`
 8. `npm run test:e2e`
@@ -337,7 +337,7 @@ There is currently no remote CI/CD runner executing 
`build.sh` for this reposito
 The published website artifact is the committed `output/` directory. The 
expected publishing workflow is:
 
 1. Run `./build.sh` locally
-2. Commit source changes and updated `output/`
+2. Commit source changes and updated `output/` (including `sitemap.xml` and 
`robots.txt`)
 3. Push your branch and open a PR
 
 After merge, `output/` can be deployed to static hosting.
diff --git a/output/.htaccess b/output/.htaccess
index de377bfe..f569edb0 100644
--- a/output/.htaccess
+++ b/output/.htaccess
@@ -1 +1,9 @@
 ErrorDocument 404 /404.html
+
+<IfModule mod_headers.c>
+  Header always set Cache-Control "no-cache, must-revalidate"
+
+  <FilesMatch 
"\.(?:css|js|mjs|gif|ico|jpe?g|png|svg|webp|avif|woff2?|ttf|otf|eot|pdf)$">
+    Header always set Cache-Control "public, max-age=2592000, immutable"
+  </FilesMatch>
+</IfModule>
diff --git a/output/books/apache-phoenix-reference-guide-dark-mode.pdf 
b/output/books/apache-phoenix-reference-guide-dark-mode.pdf
index 7dcb57c3..45e905c2 100644
Binary files a/output/books/apache-phoenix-reference-guide-dark-mode.pdf and 
b/output/books/apache-phoenix-reference-guide-dark-mode.pdf differ
diff --git a/output/books/apache-phoenix-reference-guide.pdf 
b/output/books/apache-phoenix-reference-guide.pdf
index 8df8a07f..645cb452 100644
Binary files a/output/books/apache-phoenix-reference-guide.pdf and 
b/output/books/apache-phoenix-reference-guide.pdf differ
diff --git a/output/robots.txt b/output/robots.txt
new file mode 100644
index 00000000..72b118ca
--- /dev/null
+++ b/output/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://phoenix.apache.org/sitemap.xml
diff --git a/output/sitemap.xml b/output/sitemap.xml
new file mode 100644
index 00000000..8401222a
--- /dev/null
+++ b/output/sitemap.xml
@@ -0,0 +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://phoenix.apache.org/</loc><lastmod>2026-04-09T11:38:32.751Z</lastmod></url><url><loc>https://phoenix.apache.org/docs/</loc><lastmod>2026-04-09T11:38:3
 [...]
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index dfd1751d..24b15554 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -85,6 +85,7 @@
         "prettier": "^3.6.2",
         "prettier-plugin-tailwindcss": "^0.6.14",
         "serve": "^14.2.5",
+        "sitemap": "^9.0.1",
         "tailwindcss": "^4.1.13",
         "tsx": "^4.21.0",
         "tw-animate-css": "1.3.3",
@@ -4608,6 +4609,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";,
@@ -13506,6 +13517,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";,
@@ -13917,6 +13938,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";,
diff --git a/package.json b/package.json
index 9ae7231f..555a37b9 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
     "node": ">=22.12.0"
   },
   "scripts": {
-    "build": "react-router build",
+    "build": "react-router build && npm run generate-sitemap",
     "dev": "react-router dev",
     "start": "serve -s build/client -l 5173",
     "typecheck": "react-router typegen && tsc",
@@ -23,6 +23,7 @@
     "test:e2e:debug": "playwright test --debug",
     "export-pdf": "playwright test e2e-tests/export-pdf.spec.ts 
--project=chromium --workers=1",
     "generate-language": "tsx scripts/generate-language.ts",
+    "generate-sitemap": "tsx scripts/generate-sitemap.ts",
     "fumadocs-init": "fumadocs-mdx",
     "ci": "npm run generate-language && npm run fumadocs-init && npm run lint 
&& npm run typecheck && npm run build && npm run test:unit:run && npx 
playwright install && npm run test:e2e"
   },
@@ -106,6 +107,7 @@
     "prettier": "^3.6.2",
     "prettier-plugin-tailwindcss": "^0.6.14",
     "serve": "^14.2.5",
+    "sitemap": "^9.0.1",
     "tailwindcss": "^4.1.13",
     "tsx": "^4.21.0",
     "tw-animate-css": "1.3.3",
diff --git a/public/.htaccess b/public/.htaccess
index de377bfe..f569edb0 100644
--- a/public/.htaccess
+++ b/public/.htaccess
@@ -1 +1,9 @@
 ErrorDocument 404 /404.html
+
+<IfModule mod_headers.c>
+  Header always set Cache-Control "no-cache, must-revalidate"
+
+  <FilesMatch 
"\.(?:css|js|mjs|gif|ico|jpe?g|png|svg|webp|avif|woff2?|ttf|otf|eot|pdf)$">
+    Header always set Cache-Control "public, max-age=2592000, immutable"
+  </FilesMatch>
+</IfModule>
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 00000000..72b118ca
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://phoenix.apache.org/sitemap.xml
diff --git a/scripts/generate-sitemap.ts b/scripts/generate-sitemap.ts
new file mode 100644
index 00000000..fb3717c6
--- /dev/null
+++ b/scripts/generate-sitemap.ts
@@ -0,0 +1,126 @@
+//
+// 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, stat, writeFile } from "node:fs/promises";
+import { fileURLToPath } from "node:url";
+import { join } from "node:path";
+import { SitemapStream, streamToPromise } from "sitemap";
+
+const ROOT = join(import.meta.dirname, "..");
+const BUILD_DIR = join(ROOT, "build", "client");
+const SITEMAP_PATH = join(BUILD_DIR, "sitemap.xml");
+const SITE_URL = "https://phoenix.apache.org";;
+
+const EXCLUDED_HTML_PATHS = new Set([
+  "404.html",
+  "__spa-fallback.html",
+  "phoenixcon-2018/index.html",
+  "phoenixcon-archives/index.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;
+  lastmod: 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;
+    }
+
+    const { mtime } = await stat(filePath);
+
+    entries.push({
+      url: toSiteUrl(relativePath),
+      lastmod: mtime.toISOString()
+    });
+  }
+
+  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();
+  await writeFile(SITEMAP_PATH, xml);
+
+  console.log(
+    `Generated sitemap with ${entries.length} URLs at ${SITEMAP_PATH}`
+  );
+}
+
+const isDirectRun = process.argv[1] === fileURLToPath(import.meta.url);
+
+if (isDirectRun) {
+  await main();
+}
diff --git a/unit-tests/generate-sitemap.test.ts 
b/unit-tests/generate-sitemap.test.ts
new file mode 100644
index 00000000..d9de4489
--- /dev/null
+++ b/unit-tests/generate-sitemap.test.ts
@@ -0,0 +1,106 @@
+//
+// 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\\features\\metrics\\index.html")).toBe(
+      "docs/features/metrics/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/features/metrics/index.html")).toBe(
+      "/docs/features/metrics/"
+    );
+  });
+
+  it("preserves non-index html filenames", () => {
+    expect(toSiteUrl("phoenixcon-archives.html")).toBe(
+      "/phoenixcon-archives.html"
+    );
+  });
+});
+
+describe("isRedirectOnlyPage", () => {
+  it("detects javascript redirect pages", () => {
+    expect(
+      isRedirectOnlyPage(
+        '<script>window.location.replace("/phoenixcon-archives.html")</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 Phoenix</h1></body></html>")
+    ).toBe(false);
+  });
+});
+
+describe("shouldIncludeInSitemap", () => {
+  it("excludes explicitly ignored html paths", () => {
+    expect(shouldIncludeInSitemap("404.html", "<html></html>")).toBe(false);
+    expect(
+      shouldIncludeInSitemap("phoenixcon-2018/index.html", "<html></html>")
+    ).toBe(false);
+  });
+
+  it("excludes redirect-only pages even if the path is not prelisted", () => {
+    expect(
+      shouldIncludeInSitemap(
+        "phoenixcon-archives/index.html",
+        '<script>window.location.replace("/phoenixcon-archives.html")</script>'
+      )
+    ).toBe(false);
+  });
+
+  it("includes normal prerendered pages", () => {
+    expect(
+      shouldIncludeInSitemap(
+        "docs/features/metrics/index.html",
+        "<html><body><h1>Metrics</h1></body></html>"
+      )
+    ).toBe(true);
+  });
+});

Reply via email to