From d576ea577d157cd79ec245265a0f54a1cb02348d Mon Sep 17 00:00:00 2001
From: Alan Agius <17563226+alan-agius4@users.noreply.github.com>
Date: Wed, 14 Jan 2026 08:35:38 +0000
Subject: [PATCH] fix(docs-infra): remove trailing slash from sitemap URLs
Fixes an issue where a trailing slash was sometimes added to the end of URLs in the sitemap, causing unnecessary redirects.
Example of problematic URL:
```xml
https://angular.dev/
```
(cherry picked from commit 710a4bf02ba1a50d440bc55b6734956010873105)
---
.../actions/deploy-docs-site/lib/sitemap.mts | 58 ++++++++++++------
.github/actions/deploy-docs-site/main.js | 60 ++++++++++++-------
2 files changed, 76 insertions(+), 42 deletions(-)
diff --git a/.github/actions/deploy-docs-site/lib/sitemap.mts b/.github/actions/deploy-docs-site/lib/sitemap.mts
index 19bec71e4e1..d453afdf748 100644
--- a/.github/actions/deploy-docs-site/lib/sitemap.mts
+++ b/.github/actions/deploy-docs-site/lib/sitemap.mts
@@ -1,33 +1,53 @@
import {Deployment} from './deployments.mjs';
-import {join} from 'path';
-import {readFileSync, writeFileSync} from 'fs';
-
-export async function generateSitemap(deployment: Deployment, distDir: string) {
- const servingUrlWithoutEndingSlash = deployment.servingUrl.endsWith('/')
- ? deployment.servingUrl.slice(0, -1)
- : deployment.servingUrl;
+import {join} from 'node:path';
+import {readFile, writeFile} from 'node:fs/promises';
+export async function generateSitemap(deployment: Deployment, distDir: string): Promise {
/** Timestamp string used to of the last file update. */
const lastModifiedTimestamp = new Date().toISOString();
/** An object containing all of the routes available within the application. */
- const routes = JSON.parse(readFileSync(join(distDir, 'prerendered-routes.json'), 'utf-8'));
+ const {routes} = JSON.parse(await readFile(join(distDir, 'prerendered-routes.json'), 'utf-8'));
+ const routePaths = Object.keys(routes);
+
/** The generated sitemap string. */
const sitemap = `
- ${Object.keys(routes.routes)
- .map((route) => {
- const routeWithoutLeadingSlash = route.startsWith('/') ? route.slice(1) : route;
- return `
-
- ${servingUrlWithoutEndingSlash}/${routeWithoutLeadingSlash}
+ ${routePaths
+ .map(
+ (route) => `
+ ${joinUrlParts(deployment.servingUrl, route)}
${lastModifiedTimestamp}
daily
1.0
-
- `;
- })
+ `,
+ )
.join('')}
`;
- writeFileSync(join(distDir, 'browser', 'sitemap.xml'), sitemap, 'utf-8');
- console.log(`Generated sitemap with ${Object.keys(routes.routes).length} entries.`);
+
+ await writeFile(join(distDir, 'browser', 'sitemap.xml'), sitemap, 'utf-8');
+
+ console.log(`Generated sitemap with ${routePaths.length} entries.`);
+}
+
+function joinUrlParts(...parts: string[]): string {
+ const normalizeParts: string[] = [];
+ for (const part of parts) {
+ if (part === '') {
+ // Skip any empty parts
+ continue;
+ }
+
+ let normalizedPart = part;
+ if (part[0] === '/') {
+ normalizedPart = normalizedPart.slice(1);
+ }
+ if (part.at(-1) === '/') {
+ normalizedPart = normalizedPart.slice(0, -1);
+ }
+ if (normalizedPart !== '') {
+ normalizeParts.push(normalizedPart);
+ }
+ }
+
+ return normalizeParts.join('/');
}
diff --git a/.github/actions/deploy-docs-site/main.js b/.github/actions/deploy-docs-site/main.js
index 5cb2752ae46..ad397cc72ba 100644
--- a/.github/actions/deploy-docs-site/main.js
+++ b/.github/actions/deploy-docs-site/main.js
@@ -1500,7 +1500,7 @@ var require_summary = __commonJS({
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
var os_1 = __require("os");
var fs_1 = __require("fs");
- var { access, appendFile, writeFile: writeFile3 } = fs_1.promises;
+ var { access, appendFile, writeFile: writeFile4 } = fs_1.promises;
exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
var Summary = class {
@@ -1558,7 +1558,7 @@ var require_summary = __commonJS({
return __awaiter(this, void 0, void 0, function* () {
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
const filePath = yield this.filePath();
- const writeFunc = overwrite ? writeFile3 : appendFile;
+ const writeFunc = overwrite ? writeFile4 : appendFile;
yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
return this.emptyBuffer();
});
@@ -9220,7 +9220,7 @@ var require_lockfile = __commonJS({
}
const file = _ref22;
if (yield exists(file)) {
- return readFile2(file);
+ return readFile3(file);
}
}
return null;
@@ -9239,7 +9239,7 @@ var require_lockfile = __commonJS({
})();
let readJsonAndFile = exports2.readJsonAndFile = (() => {
var _ref24 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (loc) {
- const file = yield readFile2(loc);
+ const file = yield readFile3(loc);
try {
return {
object: (0, (_map || _load_map()).default)(JSON.parse(stripBOM(file))),
@@ -9383,7 +9383,7 @@ var require_lockfile = __commonJS({
if (eol !== "\n") {
data = data.replace(/\n/g, eol);
}
- yield writeFile3(path, data);
+ yield writeFile4(path, data);
});
return function writeFilePreservingEol2(_x30, _x31) {
return _ref31.apply(this, arguments);
@@ -9395,7 +9395,7 @@ var require_lockfile = __commonJS({
const file = (_path || _load_path()).default.join(dir, filename);
const fileLink = (_path || _load_path()).default.join(dir, filename + "-link");
try {
- yield writeFile3(file, "test");
+ yield writeFile4(file, "test");
yield link(file, fileLink);
} catch (err) {
return false;
@@ -9485,7 +9485,7 @@ var require_lockfile = __commonJS({
};
})();
exports2.copy = copy;
- exports2.readFile = readFile2;
+ exports2.readFile = readFile3;
exports2.readFileRaw = readFileRaw;
exports2.normalizeOS = normalizeOS;
var _fs;
@@ -9550,7 +9550,7 @@ var require_lockfile = __commonJS({
const lockQueue = exports2.lockQueue = new (_blockingQueue || _load_blockingQueue()).default("fs lock");
const readFileBuffer = exports2.readFileBuffer = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.readFile);
const open = exports2.open = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.open);
- const writeFile3 = exports2.writeFile = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.writeFile);
+ const writeFile4 = exports2.writeFile = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.writeFile);
const readlink = exports2.readlink = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.readlink);
const realpath = exports2.realpath = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.realpath);
const readdir = exports2.readdir = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.readdir);
@@ -9584,7 +9584,7 @@ var require_lockfile = __commonJS({
});
});
}
- function readFile2(loc) {
+ function readFile3(loc) {
return _readFile(loc, "utf8").then(normalizeOS);
}
function readFileRaw(loc) {
@@ -36027,28 +36027,42 @@ async function getDeployments() {
}
// .github/actions/deploy-docs-site/lib/sitemap.mjs
-import { join as join4 } from "path";
-import { readFileSync as readFileSync4, writeFileSync } from "fs";
+import { join as join4 } from "node:path";
+import { readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
async function generateSitemap(deployment, distDir) {
- const servingUrlWithoutEndingSlash = deployment.servingUrl.endsWith("/") ? deployment.servingUrl.slice(0, -1) : deployment.servingUrl;
const lastModifiedTimestamp = (/* @__PURE__ */ new Date()).toISOString();
- const routes = JSON.parse(readFileSync4(join4(distDir, "prerendered-routes.json"), "utf-8"));
+ const { routes } = JSON.parse(await readFile2(join4(distDir, "prerendered-routes.json"), "utf-8"));
+ const routePaths = Object.keys(routes);
const sitemap = `
- ${Object.keys(routes.routes).map((route) => {
- const routeWithoutLeadingSlash = route.startsWith("/") ? route.slice(1) : route;
- return `
-
- ${servingUrlWithoutEndingSlash}/${routeWithoutLeadingSlash}
+ ${routePaths.map((route) => `
+ ${joinUrlParts(deployment.servingUrl, route)}
${lastModifiedTimestamp}
daily
1.0
-
- `;
- }).join("")}
+ `).join("")}
`;
- writeFileSync(join4(distDir, "browser", "sitemap.xml"), sitemap, "utf-8");
- console.log(`Generated sitemap with ${Object.keys(routes.routes).length} entries.`);
+ await writeFile3(join4(distDir, "browser", "sitemap.xml"), sitemap, "utf-8");
+ console.log(`Generated sitemap with ${routePaths.length} entries.`);
+}
+function joinUrlParts(...parts) {
+ const normalizeParts = [];
+ for (const part of parts) {
+ if (part === "") {
+ continue;
+ }
+ let normalizedPart = part;
+ if (part[0] === "/") {
+ normalizedPart = normalizedPart.slice(1);
+ }
+ if (part.at(-1) === "/") {
+ normalizedPart = normalizedPart.slice(0, -1);
+ }
+ if (normalizedPart !== "") {
+ normalizeParts.push(normalizedPart);
+ }
+ }
+ return normalizeParts.join("/");
}
// .github/actions/deploy-docs-site/lib/main.mts