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);
+ });
+});