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