This is an automated email from the ASF dual-hosted git repository.
moonming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-website.git
The following commit(s) were added to refs/heads/master by this push:
new 69613ea12e7 fix(seo): curate sitemap index entries (#2085)
69613ea12e7 is described below
commit 69613ea12e7a6d0cb41c0db34e4fa5de60e28e46
Author: Ming Wen <[email protected]>
AuthorDate: Thu Jul 30 09:05:14 2026 +0800
fix(seo): curate sitemap index entries (#2085)
---
.github/workflows/deploy.yml | 13 +++-
.github/workflows/lint.yml | 3 +
next/package-lock.json | 3 +-
next/package.json | 2 +-
next/scripts/generate-sitemaps.mjs | 48 ++++++++++----
next/scripts/generate-sitemaps.test.mjs | 109 ++++++++++++++++++++++++++++++++
website/static/llms.txt | 5 --
7 files changed, 163 insertions(+), 20 deletions(-)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index c527d6d2bfd..f1696a9d5f3 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -165,6 +165,7 @@ jobs:
working-directory: next
run: |
npm ci
+ npm run test:sitemap
# Project docs live in the upstream Apache repos. Clone the SAME ref
# Docusaurus does (scripts/sync-docs.js): the newest *release*
branch,
# never the default branch — master is what production publishes at
@@ -279,7 +280,7 @@ jobs:
missing=$(git ls-tree -r FETCH_HEAD --name-only -- "$docs_root" \
| grep '/index\.html$' \
| sed "s|^$docs_root/||" \
- | grep -vE '(^|/)([0-9]+\.[0-9]+|next|v[0-9][^/]*)/' \
+ | grep -vE
'^([^/]+/([0-9]+(\.[0-9]+)+|next|v[0-9][^/]*)|docker/apisix-[0-9]+(\.[0-9]+)+)/'
\
| grep -vE '(^|/)tags/index\.html$' \
| sort | while read -r f; do
[ -f "$ROOT/next/dist/$docs_root/$f" ] || echo "$docs_root/$f"
@@ -444,6 +445,16 @@ jobs:
test -f website/build/img/integrations/icon-prometheus.svg
test -f website/build/img/architecture.svg
+ - name: Generate final sitemaps
+ run: |
+ node next/scripts/generate-sitemaps.mjs --dist website/build
+ grep -q
'<loc>https://apisix.apache.org/learning-center/mcp-protocol-ai-gateway/</loc>'
website/build/sitemap.xml
+ grep -q
'<loc>https://apisix.apache.org/zh/learning-center/what-is-an-api-gateway/</loc>'
website/build/zh/sitemap.xml
+ if grep -Eq
'<loc>https://apisix\.apache\.org/(zh/)?(blog/(tags|page|archive)|learning-center/(tags|page|archive)|articles/(page|archive)|events/archive)/'
website/build/sitemap.xml website/build/zh/sitemap.xml; then
+ echo 'Low-value aggregation URLs remain in the final sitemap.'
+ exit 1
+ fi
+
- name: Test the final overlaid site on desktop and mobile
working-directory: next
run: npm run test:e2e
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 08125d3d702..35174299020 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -17,6 +17,9 @@ jobs:
- name: Install dependencies
working-directory: next
run: npm ci
+ - name: Test sitemap generation
+ working-directory: next
+ run: npm run test:sitemap
- name: Sync site content
working-directory: next
run: node scripts/sync-content.mjs
diff --git a/next/package-lock.json b/next/package-lock.json
index a8cd8ba68b9..ec027ed01f8 100644
--- a/next/package-lock.json
+++ b/next/package-lock.json
@@ -11,7 +11,8 @@
"unist-util-visit": "^5.1.0"
},
"devDependencies": {
- "@playwright/test": "1.61.1"
+ "@playwright/test": "1.61.1",
+ "sax": "^1.6.0"
}
},
"node_modules/@astrojs/compiler": {
diff --git a/next/package.json b/next/package.json
index 7ce56172eaf..84c89c5ea70 100644
--- a/next/package.json
+++ b/next/package.json
@@ -1 +1 @@
-{"name":"apisix-website-astro","type":"module","private":true,"dependencies":{"astro":"^6.0.5","remark-directive":"^4.0.0","unist-util-visit":"^5.1.0"},"devDependencies":{"@playwright/test":"1.61.1"},"scripts":{"sync":"node
scripts/sync-content.mjs && node scripts/lint-content.mjs","build":"astro
build && node scripts/generate-sitemaps.mjs","dev":"astro dev","preview":"astro
preview","test:e2e":"playwright test","parity":"node
scripts/check-parity.mjs","lint":"node scripts/lint-content.m [...]
+{"name":"apisix-website-astro","type":"module","private":true,"dependencies":{"astro":"^6.0.5","remark-directive":"^4.0.0","unist-util-visit":"^5.1.0"},"devDependencies":{"@playwright/test":"1.61.1","sax":"^1.6.0"},"scripts":{"sync":"node
scripts/sync-content.mjs && node scripts/lint-content.mjs","build":"astro
build && node scripts/generate-sitemaps.mjs","dev":"astro dev","preview":"astro
preview","test:e2e":"playwright test","test:sitemap":"node
scripts/generate-sitemaps.test.mjs","par [...]
diff --git a/next/scripts/generate-sitemaps.mjs
b/next/scripts/generate-sitemaps.mjs
index f3d3ddeea25..54988d0a5b3 100644
--- a/next/scripts/generate-sitemaps.mjs
+++ b/next/scripts/generate-sitemaps.mjs
@@ -14,36 +14,60 @@ const distFlag = args.indexOf('--dist');
const dist = distFlag !== -1 ? path.resolve(args[distFlag + 1]) :
path.join(root, 'dist');
const SITE = 'https://apisix.apache.org';
const today = new Date().toISOString().slice(0, 10);
+const excludePatterns = [
+ /^\/404\/$/,
+ /^\/(?:zh\/)?docs\/[\w-]+\/(?:[\w-]+-)?\d+\.\d+(?:\.\d+)?\//,
+ /^\/(?:zh\/)?docs\/[\w./-]+\/tags\//,
+ /^\/(?:zh\/)?docs\/[\w-]+\/next\//,
+ /^\/(?:zh\/)?search\/?$/,
+ /^\/(?:zh\/)?blog\/(?:tags|page|archive)\//,
+ /^\/(?:zh\/)?learning-center\/(?:tags|page|archive)\//,
+ /^\/(?:zh\/)?(?:articles|events)\/(?:page|archive)\//,
+];
function pagesUnder(dir) {
const out = [];
(function walk(d) {
- for (const e of fs.readdirSync(d, { withFileTypes: true })) {
+ fs.readdirSync(d, { withFileTypes: true }).forEach((e) => {
const p = path.join(d, e.name);
if (e.isDirectory()) walk(p);
else if (e.name === 'index.html') {
out.push(`/${path.relative(dist,
path.dirname(p)).split(path.sep).join('/')}/`.replace('/./', '/'));
}
- }
- })(dir);
+ });
+ }(dir));
return out.map((u) => (u === '//' ? '/' : u)).sort();
}
-const all = pagesUnder(dist).filter((u) => u !== '/404/');
+const all = pagesUnder(dist).filter((u) => !excludePatterns.some((pattern) =>
pattern.test(u)));
const zh = all.filter((u) => u === '/zh/' || u.startsWith('/zh/'));
const en = all.filter((u) => !zh.includes(u));
-function meta(url) {
- if (url === '/' ) return { priority: '1.0', changefreq: 'weekly' };
- if (url.includes('/docs/') || url.includes('/learning-center/')) return {
priority: '0.7', changefreq: 'monthly' };
- return { priority: '0.5', changefreq: 'weekly' };
+function getPriority(url) {
+ if (/^\/(?:zh\/)?$/.test(url)) return '1.0';
+ if (/\/(?:ai-gateway|plugins|downloads|docs|learning-center)\/$/.test(url))
return '0.8';
+ if (url.includes('/learning-center/')) return '0.8';
+ if (/\/blog\/\d{4}\//.test(url)) return '0.6';
+ if (url.includes('/docs/')) return '0.7';
+ return '0.5';
+}
+
+function getChangefreq(url) {
+ if (/^\/(?:zh\/)?$/.test(url)) return 'weekly';
+ if (/\/blog\/\d{4}\//.test(url)) return 'monthly';
+ if (url.includes('/docs/') || url.includes('/learning-center/')) return
'monthly';
+ return 'weekly';
+}
+
+function sitemapLoc(url) {
+ return encodeURI(url)
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>');
}
function render(urls) {
- const items = urls.map((u) => {
- const m = meta(u);
- return
`<url><loc>${SITE}${u}</loc><changefreq>${m.changefreq}</changefreq><priority>${m.priority}</priority><lastmod>${today}</lastmod></url>`;
- }).join('');
+ const items = urls.map((u) =>
`<url><loc>${sitemapLoc(`${SITE}${u}`)}</loc><changefreq>${getChangefreq(u)}</changefreq><priority>${getPriority(u)}</priority><lastmod>${today}</lastmod></url>`).join('');
return `<?xml version="1.0" encoding="UTF-8"?><urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${items}</urlset>\n`;
}
diff --git a/next/scripts/generate-sitemaps.test.mjs
b/next/scripts/generate-sitemaps.test.mjs
new file mode 100644
index 00000000000..ec262bbeca8
--- /dev/null
+++ b/next/scripts/generate-sitemaps.test.mjs
@@ -0,0 +1,109 @@
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { spawnSync } from 'node:child_process';
+// Test-only XML parser; production sitemap generation has no parser
dependency.
+// eslint-disable-next-line import/no-extraneous-dependencies
+import sax from 'sax';
+
+const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
+const generator = path.join(root, 'scripts/generate-sitemaps.mjs');
+const dist = fs.mkdtempSync(path.join(os.tmpdir(), 'apisix-sitemap-'));
+
+function writePage(url) {
+ const dir = path.join(dist, url);
+ fs.mkdirSync(dir, { recursive: true });
+ fs.writeFileSync(path.join(dir, 'index.html'), '<!doctype html>');
+}
+
+const pages = [
+ '',
+ '404',
+ 'learning-center',
+ 'learning-center/mcp-protocol-ai-gateway',
+ 'learning-center/tags/ai-gateway',
+ 'learning-center/page/2',
+ 'learning-center/archive',
+ 'articles/Apache-APISIX-Incubator-Journey',
+ 'docs/general/blog/page/overview',
+ 'articles/page/2',
+ 'articles/archive',
+ 'events/archive',
+ 'blog/2026/07/28/release-notes',
+ 'blog/2026/07/28/apisix-unity-group-q&a',
+ 'blog/page/2',
+ 'blog/archive',
+ 'docs/apisix/3.17/plugins/cors',
+ 'docs/docker/apisix-3.17.0/build',
+ 'docs/apisix/next/plugins/cors',
+ 'docs/apisix/tags/plugins',
+ 'docs/apisix/upgrade-guide-from-2.15.x-to-3.0.0',
+ 'search',
+ 'zh',
+ 'zh/learning-center/what-is-an-api-gateway',
+ 'zh/learning-center/tags/api-gateway',
+ 'zh/articles/page/2',
+ 'zh/events/archive',
+ 'zh/blog/page/2',
+ 'zh/blog/2026/07/28/bi-weekly report',
+ 'zh/docs/ingress-controller/2.1.0/overview',
+ 'zh/docs/apisix/next/plugins/cors',
+ 'zh/docs/apisix/tags/plugins',
+ 'zh/search',
+];
+
+try {
+ pages.forEach(writePage);
+ const result = spawnSync(process.execPath, [generator, '--dist', dist], {
encoding: 'utf8' });
+ assert.equal(result.status, 0, result.stderr);
+
+ const en = fs.readFileSync(path.join(dist, 'sitemap.xml'), 'utf8');
+ const zh = fs.readFileSync(path.join(dist, 'zh/sitemap.xml'), 'utf8');
+ sax.parser(true).write(en).close();
+ sax.parser(true).write(zh).close();
+
+ const excluded = [
+ 'learning-center/tags/ai-gateway',
+ 'learning-center/page/2',
+ 'learning-center/archive',
+ 'articles/page/2',
+ 'articles/archive',
+ 'events/archive',
+ 'blog/page/2',
+ 'blog/archive',
+ 'zh/learning-center/tags/api-gateway',
+ 'zh/articles/page/2',
+ 'zh/events/archive',
+ 'zh/blog/page/2',
+ 'docs/apisix/3.17/plugins/cors',
+ 'docs/docker/apisix-3.17.0/build',
+ 'docs/apisix/next/plugins/cors',
+ 'docs/apisix/tags/plugins',
+ 'zh/docs/ingress-controller/2.1.0/overview',
+ 'zh/docs/apisix/next/plugins/cors',
+ 'zh/docs/apisix/tags/plugins',
+ '/search/',
+ '/zh/search/',
+ ];
+
+ assert.match(en, /learning-center\/mcp-protocol-ai-gateway/);
+ assert.match(en, /articles\/Apache-APISIX-Incubator-Journey/);
+ assert.match(en, /blog\/2026\/07\/28\/release-notes/);
+ assert.match(en, /docs\/general\/blog\/page\/overview/);
+ assert.match(en, /docs\/apisix\/upgrade-guide-from-2\.15\.x-to-3\.0\.0/);
+ assert.match(en, /apisix-unity-group-q&a/);
+ assert.match(zh, /zh\/learning-center\/what-is-an-api-gateway/);
+ assert.match(zh, /bi-weekly%20report/);
+
+
assert.ok(en.includes('<loc>https://apisix.apache.org/</loc><changefreq>weekly</changefreq><priority>1.0</priority>'));
+
assert.ok(en.includes('<loc>https://apisix.apache.org/learning-center/</loc><changefreq>monthly</changefreq><priority>0.8</priority>'));
+
assert.ok(en.includes('<loc>https://apisix.apache.org/learning-center/mcp-protocol-ai-gateway/</loc><changefreq>monthly</changefreq><priority>0.8</priority>'));
+
assert.ok(en.includes('<loc>https://apisix.apache.org/blog/2026/07/28/release-notes/</loc><changefreq>monthly</changefreq><priority>0.6</priority>'));
+
assert.ok(en.includes('<loc>https://apisix.apache.org/docs/general/blog/page/overview/</loc><changefreq>monthly</changefreq><priority>0.7</priority>'));
+ excluded.forEach((url) => assert.equal(`${en}${zh}`.includes(url), false,
url));
+ console.log('Sitemap URL filters passed.');
+} finally {
+ fs.rmSync(dist, { recursive: true, force: true });
+}
diff --git a/website/static/llms.txt b/website/static/llms.txt
index eeeebbd46b4..b97a81c4ca6 100644
--- a/website/static/llms.txt
+++ b/website/static/llms.txt
@@ -64,9 +64,4 @@
- [Apache APISIX vs Envoy: API Gateway vs Service
Proxy](https://apisix.apache.org/learning-center/apisix-vs-envoy/): Turnkey
gateway vs a proxy that needs a separate control plane; service mesh
- [Apache APISIX vs Traefik: Performance vs
Auto-Discovery](https://apisix.apache.org/learning-center/apisix-vs-traefik/):
Throughput and plugin breadth vs container-native auto-discovery and automatic
TLS
- [Apache APISIX vs Apigee: Open-Source Gateway vs API Management
Platform](https://apisix.apache.org/learning-center/apisix-vs-apigee/):
Open-source gateway vs full enterprise API management platform, scope and cost
-- [Apache APISIX vs AWS API Gateway: Self-Hosted vs
Managed](https://apisix.apache.org/learning-center/apisix-vs-aws-api-gateway/):
Open-source self-hosted gateway vs managed cloud service, cost at scale, lock-in
-- [Apache APISIX vs NGINX: API Gateway vs Reverse
Proxy](https://apisix.apache.org/learning-center/apisix-vs-nginx/): How APISIX
builds a full API gateway on top of NGINX; NGINX Plus comparison
-- [Apache APISIX vs Envoy: API Gateway vs Service
Proxy](https://apisix.apache.org/learning-center/apisix-vs-envoy/): Turnkey
gateway vs a proxy that needs a separate control plane; service mesh
-- [Apache APISIX vs Traefik: Performance vs
Auto-Discovery](https://apisix.apache.org/learning-center/apisix-vs-traefik/):
Throughput and plugin breadth vs container-native auto-discovery and automatic
TLS
-- [Apache APISIX vs Apigee: Open-Source Gateway vs API Management
Platform](https://apisix.apache.org/learning-center/apisix-vs-apigee/):
Open-source gateway vs full enterprise API management platform, scope and cost
- [MCP Protocol & AI Gateways: Managing AI Agent Traffic at
Scale](https://apisix.apache.org/learning-center/mcp-protocol-ai-gateway/): MCP
and AI gateway architecture