This is an automated email from the ASF dual-hosted git repository.

bzp2010 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 8b9ef359b90 fix(seo): consolidate ingress documentation redirects 
(#2099)
8b9ef359b90 is described below

commit 8b9ef359b900b2ed38bafa8e1016320888144dea
Author: Yilia Lin <[email protected]>
AuthorDate: Tue Aug 18 09:52:33 2026 +0800

    fix(seo): consolidate ingress documentation redirects (#2099)
---
 .github/workflows/lint.yml               |   3 +
 .htaccess                                |  53 +++++-----
 next/package.json                        |   2 +-
 next/scripts/check-ingress-redirects.mjs | 164 +++++++++++++++++++++++++++++++
 4 files changed, 194 insertions(+), 28 deletions(-)

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index f66b4084339..5483c08a8a5 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -20,6 +20,9 @@ jobs:
       - name: Test sitemap generation
         working-directory: next
         run: npm run test:sitemap
+      - name: Test Ingress documentation redirects
+        working-directory: next
+        run: npm run test:redirects
       - name: Sync site content
         working-directory: next
         run: node scripts/sync-content.mjs
diff --git a/.htaccess b/.htaccess
index ca349b60cbe..378962b5a74 100644
--- a/.htaccess
+++ b/.htaccess
@@ -49,7 +49,7 @@ RedirectMatch 301 "^/zh/docs/apisix/3\.2/plugins/(.*)$" 
"https://apache-apisix.n
 Redirect 301 "/docs/apisix/install" "/docs/apisix/building-apisix/"
 Redirect 301 "/docs/apisix/architecture-design/plugin/" 
"/docs/apisix/terminology/plugin-config/"
 Redirect 301 "/docs/apisix/2.13/FAQ/plugins.md/" 
"/docs/apisix/terminology/plugin-config/"
-Redirect 301 
"/docs/ingress-controller/practices/proxy-the-httpbin-service-with-ingress" 
"/docs/ingress-controller/tutorials/proxy-the-httpbin-service-with-ingress/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?practices/proxy-the-httpbin-service-with-ingress/?$"
 "$1/docs/ingress-controller/getting-started/configure-routes/"
 Redirect 301 "/docs/apisix/admin-api/terminology/plugin-config.md/" 
"/docs/apisix/terminology/plugin-config/"
 Redirect 301 "/docs/general/community/" "/docs/general/join/"
 Redirect 301 "/docs/general/subscribe-guide/" "/docs/general/join/"
@@ -65,7 +65,6 @@ Redirect 301 "/blog/2022/09/21/apache-apisix-v3-preview" 
"/blog/2022/11/02/apach
 Redirect 301 "/zh/docs/apisix/install" "/zh/docs/apisix/building-apisix/"
 Redirect 301 "/zh/docs/apisix/architecture-design/plugin/" 
"/zh/docs/apisix/terminology/plugin-config/"
 Redirect 301 "/zh/docs/apisix/2.13/FAQ/plugins.md/" 
"/zh/docs/apisix/terminology/plugin-config/"
-Redirect 301 
"/zh/docs/ingress-controller/practices/proxy-the-httpbin-service-with-ingress" 
"/zh/docs/ingress-controller/tutorials/proxy-the-httpbin-service-with-ingress/"
 Redirect 301 "/zh/docs/apisix/admin-api/terminology/plugin-config.md/" 
"/zh/docs/apisix/terminology/plugin-config/"
 Redirect 301 "/zh/docs/general/community/" "/zh/docs/general/join/"
 Redirect 301 "/zh/docs/general/subscribe-guide/" "/zh/docs/general/join/"
@@ -77,28 +76,28 @@ Redirect 301 
"/zh/blog/2021/11/30/use-apisix-ingress-in-kubesphere/httpbin.org/"
 Redirect 301 "/zh/blog/2021/08/25/Auth-with-Casbin-in-Apache-APISIX/" 
"/zh/blog/2021/08/18/auth-with-casbin-in-apache-apisix/"
 Redirect 301 "/zh/blog/2022/03/02/apisix-integration-graphql-plugin/" 
"/zh/blog/2022/03/02/apisix-integration-graphql/"
 
-# Ingress-controller pages from the pre-2.0 IA. These used to redirect to
-# 1.8.0, but only the newest release is built now, so 1.8.0 URLs 404 —
-# retarget to the closest page in the current IA. Stripped old-version URLs
-# from the rules further down land on these paths as their second hop (each
-# hop is a fresh request, re-evaluated from the top of this file).
-RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/concepts/(apisix_route|apisix_upstream|apisix_plugin_config|apisix_tls|apisix_consumer|apisix_cluster_config|apisix_global_rule)/$"
 "$1/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/concepts/annotations/$" 
"$1/docs/ingress-controller/reference/apisix-ingress-controller/annotation/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/design/$" 
"$1/docs/ingress-controller/concepts/deployment-architecture/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/composite/$" 
"$1/docs/ingress-controller/overview/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/references/(.+)$" 
"$1/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/"
-RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/tutorials/proxy-the-httpbin-service(-with-ingress)?/$"
 "$1/docs/ingress-controller/getting-started/configure-routes/"
-RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/tutorials/proxy-grpc-service/$" 
"$1/docs/ingress-controller/getting-started/configure-routes/"
-RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/tutorials/enable-authentication-and-restriction/$"
 "$1/docs/ingress-controller/getting-started/key-authentication/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/tutorials/the-hard-way/$" 
"$1/docs/ingress-controller/getting-started/get-apisix-ingress-controller/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/tutorials/(.+)$" 
"$1/docs/ingress-controller/overview/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/monitoring/$" 
"$1/docs/ingress-controller/overview/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/plugins/prometheus/$" 
"$1/docs/ingress-controller/overview/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/deployments/(.+)$" 
"$1/docs/ingress-controller/install/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/aeps/gateway-api/$" 
"$1/docs/ingress-controller/concepts/gateway-api/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/contribute/$" 
"$1/docs/ingress-controller/developer-guide/"
+# Ingress-controller pages from the pre-2.0 IA. Only the newest release is
+# built now, so known 0.x and 1.x paths must resolve directly to the closest
+# page in the current IA. The optional version segment keeps current legacy
+# slugs and archived URLs on the same one-hop redirect rules.
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?concepts/(apisix_route|apisix_upstream|apisix_plugin_config|apisix_tls|apisix_consumer|apisix_cluster_config|apisix_global_rule)/?$"
 "$1/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?concepts/annotations/?$" 
"$1/docs/ingress-controller/reference/apisix-ingress-controller/annotation/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?design/?$" 
"$1/docs/ingress-controller/concepts/deployment-architecture/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?composite/?$" 
"$1/docs/ingress-controller/overview/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?references/(.+)$" 
"$1/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?tutorials/proxy-the-httpbin-service(-with-ingress)?/?$"
 "$1/docs/ingress-controller/getting-started/configure-routes/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?tutorials/proxy-grpc-service/?$"
 "$1/docs/ingress-controller/getting-started/configure-routes/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?tutorials/enable-authentication-and-restriction/?$"
 "$1/docs/ingress-controller/getting-started/key-authentication/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?tutorials/the-hard-way/?$" 
"$1/docs/ingress-controller/getting-started/get-apisix-ingress-controller/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?tutorials/(.+)$" 
"$1/docs/ingress-controller/overview/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?monitoring/?$" 
"$1/docs/ingress-controller/overview/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?plugins/prometheus/?$" 
"$1/docs/ingress-controller/overview/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?deployments/(.+)$" 
"$1/docs/ingress-controller/install/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?aeps/gateway-api/?$" 
"$1/docs/ingress-controller/concepts/gateway-api/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?contribute/?$" 
"$1/docs/ingress-controller/developer-guide/"
+RedirectMatch 301 "^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?FAQ/?$" 
"$1/docs/ingress-controller/overview/"
 Redirect 301 
"/docs/ingress-controller/reference/apisix-ingress-controller/crd-reference/" 
"/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/"
-RedirectMatch 301 "^(/zh)?/docs/ingress-controller/upgrade/$" 
"$1/docs/ingress-controller/upgrade-guide/"
+RedirectMatch 301 
"^(/zh)?/docs/ingress-controller/(?:[01]\.\d+\.\d+/)?upgrade/?$" 
"$1/docs/ingress-controller/upgrade-guide/"
 
 Redirect 301 "/docs/ingress-controller/next/tutorials/the-hard-way/" 
"/docs/ingress-controller/getting-started/get-apisix-ingress-controller/"
 Redirect 301 
"/docs/ingress-controller/next/tutorials/configure-ingress-with-gateway-api/" 
"/docs/ingress-controller/concepts/gateway-api/"
@@ -142,10 +141,10 @@ RedirectMatch 301 "^/zh/help/?$" "/zh/"
 # --- Dead-URL cleanup (sourced from Matomo 404 inventory, 2026-06-14..07-11) 
---
 
 # Old sub-project doc versions are no longer built (only the newest release is
-# published) — strip the version segment. Renamed pages then land on the
-# retargeting rules above as a second hop. The apisix project itself is NOT
-# covered here: its 3.10+ versions are still built, and 2.x/3.x legacy
-# redirects for it are handled at the infra layer.
+# published) — strip the version segment for paths without a direct mapping
+# above. The apisix project itself is NOT covered here: its 3.10+ versions are
+# still built, and 2.x/3.x legacy redirects for it are handled at the infra
+# layer.
 RedirectMatch 301 "^(/zh)?/docs/ingress-controller/[01]\.\d+\.\d+/(.*)$" 
"$1/docs/ingress-controller/$2"
 RedirectMatch 301 
"^(/zh)?/docs/docker/(?:apisix-(?:dashboard-)?)?\d+\.\d+(?:\.\d+)?/(.+)$" 
"$1/docs/docker/$2"
 RedirectMatch 301 "^(/zh)?/docs/apisix/2\.[1-5](?:\.\d+)?/(.+)$" 
"$1/docs/apisix/$2"
diff --git a/next/package.json b/next/package.json
index 84c89c5ea70..d0d33494adb 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","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 [...]
+{"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:redirects":"node 
scripts/check-ingress-redirects.mjs"," [...]
diff --git a/next/scripts/check-ingress-redirects.mjs 
b/next/scripts/check-ingress-redirects.mjs
new file mode 100644
index 00000000000..1dc070a513e
--- /dev/null
+++ b/next/scripts/check-ingress-redirects.mjs
@@ -0,0 +1,164 @@
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
+const htaccessPath = path.resolve(scriptDirectory, '../../.htaccess');
+const directives = fs
+  .readFileSync(htaccessPath, 'utf8')
+  .split('\n')
+  .map((line) => line.trim())
+  .filter(
+    (line) => line.startsWith('Redirect 301 "') || 
line.startsWith('RedirectMatch 301 "'),
+  )
+  .map((line) => {
+    const redirectMatch = line.match(
+      /^RedirectMatch\s+(\d{3})\s+"([^"]+)"\s+"([^"]+)"$/,
+    );
+    if (redirectMatch) {
+      return {
+        type: 'match',
+        status: Number(redirectMatch[1]),
+        source: new RegExp(redirectMatch[2]),
+        destination: redirectMatch[3],
+      };
+    }
+
+    const redirect = 
line.match(/^Redirect\s+(\d{3})\s+"([^"]+)"\s+"([^"]+)"$/);
+    assert(redirect, `Unable to parse redirect directive: ${line}`);
+    return {
+      type: 'prefix',
+      status: Number(redirect[1]),
+      source: redirect[2],
+      destination: redirect[3],
+    };
+  });
+
+function firstRedirect(requestPath) {
+  for (const directive of directives) {
+    if (directive.type === 'match' && directive.source.test(requestPath)) {
+      return {
+        status: directive.status,
+        destination: requestPath.replace(directive.source, 
directive.destination),
+      };
+    }
+    const matchesPrefix =
+      directive.type === 'prefix' &&
+      (requestPath === directive.source ||
+        requestPath.startsWith(
+          directive.source.endsWith('/') ? directive.source : 
`${directive.source}/`,
+        ));
+    if (matchesPrefix) {
+      return {
+        status: directive.status,
+        destination: 
`${directive.destination}${requestPath.slice(directive.source.length)}`,
+      };
+    }
+  }
+  return null;
+}
+
+const directRedirects = [
+  [
+    '/docs/ingress-controller/concepts/apisix_route/',
+    
'/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/',
+  ],
+  [
+    '/docs/ingress-controller/1.8.0/concepts/apisix_route/',
+    
'/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/',
+  ],
+  [
+    '/docs/ingress-controller/1.8.0/concepts/apisix_route',
+    
'/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/',
+  ],
+  [
+    '/zh/docs/ingress-controller/1.1.0/concepts/apisix_route/',
+    
'/zh/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/',
+  ],
+  [
+    '/docs/ingress-controller/1.8.0/references/apisix_route_v2/',
+    
'/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/',
+  ],
+  [
+    '/docs/ingress-controller/1.6.1/design/',
+    '/docs/ingress-controller/concepts/deployment-architecture/',
+  ],
+  [
+    '/docs/ingress-controller/1.6.1/design',
+    '/docs/ingress-controller/concepts/deployment-architecture/',
+  ],
+  [
+    '/docs/ingress-controller/design/',
+    '/docs/ingress-controller/concepts/deployment-architecture/',
+  ],
+  [
+    '/zh/docs/ingress-controller/0.6.0/design/',
+    '/zh/docs/ingress-controller/concepts/deployment-architecture/',
+  ],
+  [
+    '/docs/ingress-controller/1.8.0/tutorials/proxy-the-httpbin-service/',
+    '/docs/ingress-controller/getting-started/configure-routes/',
+  ],
+  [
+    
'/docs/ingress-controller/practices/proxy-the-httpbin-service-with-ingress',
+    '/docs/ingress-controller/getting-started/configure-routes/',
+  ],
+  [
+    
'/docs/ingress-controller/practices/proxy-the-httpbin-service-with-ingress/',
+    '/docs/ingress-controller/getting-started/configure-routes/',
+  ],
+  [
+    
'/zh/docs/ingress-controller/practices/proxy-the-httpbin-service-with-ingress',
+    '/zh/docs/ingress-controller/getting-started/configure-routes/',
+  ],
+  [
+    
'/docs/ingress-controller/0.6.0/practices/proxy-the-httpbin-service-with-ingress/',
+    '/docs/ingress-controller/getting-started/configure-routes/',
+  ],
+  [
+    
'/zh/docs/ingress-controller/1.8.0/practices/proxy-the-httpbin-service-with-ingress',
+    '/zh/docs/ingress-controller/getting-started/configure-routes/',
+  ],
+  [
+    '/docs/ingress-controller/1.8.0/FAQ/',
+    '/docs/ingress-controller/overview/',
+  ],
+  [
+    
'/docs/ingress-controller/reference/apisix-ingress-controller/crd-reference/',
+    
'/docs/ingress-controller/reference/apisix-ingress-controller/api-reference/',
+  ],
+];
+
+for (const [source, expectedDestination] of directRedirects) {
+  const redirect = firstRedirect(source);
+  assert(redirect, `${source} should match a redirect`);
+  assert.equal(redirect.status, 301, `${source} should return a permanent 
redirect`);
+  assert.equal(
+    redirect.destination,
+    expectedDestination,
+    `${source} should redirect directly to its current documentation page`,
+  );
+  assert.equal(
+    firstRedirect(expectedDestination),
+    null,
+    `${expectedDestination} should not start another redirect`,
+  );
+}
+
+assert.deepEqual(
+  firstRedirect('/docs/ingress-controller/1.8.0/unmapped-page/'),
+  {
+    status: 301,
+    destination: '/docs/ingress-controller/unmapped-page/',
+  },
+  'The generic version-removal fallback should remain available',
+);
+
+assert.equal(
+  firstRedirect('/docs/apisix/install-extra'),
+  null,
+  'Redirect directives should only match complete path segments',
+);
+
+console.log(`Validated ${directRedirects.length} direct Ingress documentation 
redirects.`);

Reply via email to