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('&', '&amp;')
+    .replaceAll('<', '&lt;')
+    .replaceAll('>', '&gt;');
 }
 
 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&amp;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

Reply via email to