blob: b99271a755c4ed642fd60ee7f81fcb724e2c7d0c [file]
// 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.
// Generates static/api-sitemap.xml from the contents of static/api/.
// One entry per API library (admin, client, cpp, js, pulsar-functions,
// python) — the latest non-disallowed version. Crawlers walk deeper from
// each index page. Versions matched by a Disallow line in static/robots.txt
// are excluded before "latest" is picked, so robots.txt remains the single
// source of truth for which API versions are excluded from indexing.
//
// Run manually after adding a new API version:
// node scripts/generate-api-sitemap.js
// Then add a `Sitemap: https://pulsar.apache.org/api-sitemap.xml` line to
// static/robots.txt if not already present, and commit both files.
"use strict";
const fs = require("fs");
const path = require("path");
const SITE_URL = "https://pulsar.apache.org";
const STATIC_DIR = path.resolve(__dirname, "..", "static");
const API_DIR = path.join(STATIC_DIR, "api");
const ROBOTS_FILE = path.join(STATIC_DIR, "robots.txt");
const OUTPUT_FILE = path.join(STATIC_DIR, "api-sitemap.xml");
function parseDisallowPatterns(robotsTxt) {
// Translate robots.txt path patterns to anchored RegExps.
// '*' matches any sequence; '$' anchors end-of-path; everything else is literal.
const patterns = [];
for (const line of robotsTxt.split(/\r?\n/)) {
const match = line.match(/^\s*Disallow:\s*(\S+)\s*$/i);
if (!match) continue;
const raw = match[1];
if (!raw || raw === "/") continue;
let regex = "";
for (const ch of raw) {
if (ch === "*") regex += ".*";
else if (ch === "$") regex += "$";
else regex += ch.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
}
patterns.push(new RegExp("^" + regex));
}
return patterns;
}
function isDisallowed(urlPath, patterns) {
return patterns.some((p) => p.test(urlPath));
}
const VERSION_RE = /^(\d+)\.(\d+)\./;
// Sort key: higher major/minor first; non-version-shaped names sort last.
function versionSortKey(name) {
const m = name.match(VERSION_RE);
return m ? [-Number(m[1]), -Number(m[2])] : [Infinity, Infinity];
}
function compareVersions(a, b) {
const [a1, a2] = versionSortKey(a);
const [b1, b2] = versionSortKey(b);
return a1 - b1 || a2 - b2 || a.localeCompare(b);
}
// Returns {libName: [version, ...]} for all libraries under static/api/.
function collectLibraryVersions() {
const libs = {};
if (!fs.existsSync(API_DIR)) return libs;
for (const lib of fs.readdirSync(API_DIR, {withFileTypes: true})) {
if (!lib.isDirectory() || lib.name.startsWith(".")) continue;
const libDir = path.join(API_DIR, lib.name);
libs[lib.name] = fs.readdirSync(libDir, {withFileTypes: true})
.filter((v) => v.isDirectory() && !v.name.startsWith("."))
.map((v) => v.name);
}
return libs;
}
function buildSitemap(urls) {
const items = urls
.map((p) =>
` <url>
<loc>${SITE_URL}${p}</loc>
<changefreq>weekly</changefreq>
</url>`,
)
.join("\n");
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${items}
</urlset>
`;
}
function main() {
const robotsTxt = fs.readFileSync(ROBOTS_FILE, "utf8");
const patterns = parseDisallowPatterns(robotsTxt);
const libs = collectLibraryVersions();
const urls = [];
const skipped = [];
for (const lib of Object.keys(libs).sort()) {
const candidates = libs[lib]
.filter((v) => !isDisallowed(`/api/${lib}/${v}/`, patterns))
.sort(compareVersions);
if (candidates.length === 0) {
skipped.push(lib);
continue;
}
urls.push(`/api/${lib}/${candidates[0]}/`);
}
fs.writeFileSync(OUTPUT_FILE, buildSitemap(urls));
const rel = path.relative(process.cwd(), OUTPUT_FILE);
console.log(
`[generate-api-sitemap] wrote ${urls.length} URL(s) to ${rel}` +
(skipped.length ? ` (no allowed version for: ${skipped.join(", ")})` : ""),
);
}
main();