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.`);