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

LiteSun 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 87991d1472f fix: canonicalize versioned doc pages to the version-less 
latest URL (#2070)
87991d1472f is described below

commit 87991d1472f263c41c04a23cd3559b2273b85993
Author: Ming Wen <[email protected]>
AuthorDate: Mon Jul 13 14:15:19 2026 +0800

    fix: canonicalize versioned doc pages to the version-less latest URL (#2070)
---
 .github/workflows/deploy.yml       | 11 +++++++++
 doc/src/theme/LayoutHead/index.tsx | 50 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 61 insertions(+)

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index ba4e2b39827..b3a1f20b5dc 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -134,6 +134,17 @@ jobs:
           retention-days: 7
           if-no-files-found: warn
 
+      # Guards the canonical contract of versioned doc pages (see
+      # doc/src/theme/LayoutHead/index.tsx): versioned pages must carry exactly
+      # one canonical pointing at the version-less latest URL, and canonicals
+      # embedded in the doc markdown itself must still win.
+      - name: Assert canonical contract
+        run: |
+          f=$(ls website/build/docs/apisix/3.*/plugins/cors/index.html | head 
-1)
+          test "$(grep -o 'rel="canonical"' "$f" | wc -l)" -eq 1
+          grep -q 'rel="canonical" 
href="https://apisix.apache.org/docs/apisix/plugins/cors/";' "$f"
+          grep -q 'rel="canonical" href="https://docs.api7.ai/hub/cors";' 
website/build/docs/apisix/plugins/cors/index.html
+
       - name: Update sitemap.xml
         run: |
           yarn update-sitemap && git status
diff --git a/doc/src/theme/LayoutHead/index.tsx 
b/doc/src/theme/LayoutHead/index.tsx
new file mode 100644
index 00000000000..e2e5300a38d
--- /dev/null
+++ b/doc/src/theme/LayoutHead/index.tsx
@@ -0,0 +1,50 @@
+/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved */
+import type { FC } from 'react';
+import React from 'react';
+import Head from '@docusaurus/Head';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore: swizzle-wrapper alias has no published types
+import OriginalLayoutHead from '@theme-original/LayoutHead';
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+import { useLocation } from '@docusaurus/router';
+
+/**
+ * Matches the version segment of versioned doc URLs, e.g.
+ *   /docs/apisix/3.10/plugins/cors/          -> 3.10
+ *   /docs/ingress-controller/2.0.0/...       -> 2.0.0
+ *   /docs/docker/apisix-2.10.0/...           -> apisix-2.10.0
+ *   /docs/apisix/next/...                    -> next
+ * Keeps the same version-segment pattern as scripts/update-sitemap-loc.js.
+ */
+const versionedDocPath = 
/^((?:\/zh)?\/docs\/[\w-]+\/)(?:(?:[\w-]+-)?\d+\.\d+(?:\.\d+)?|next)(\/.+)$/;
+
+/**
+ * Versioned doc pages (/docs/<project>/<version>/) self-canonicalize by
+ * default, so Google indexes them as independent pages competing with the
+ * version-less "latest" URLs. This wrapper re-points their canonical to the
+ * latest URL. Rendering order keeps the precedence right (react-helmet:
+ * last <Head> wins):
+ *   1. default self-canonical (original LayoutHead)
+ *   2. this wrapper's latest-URL canonical (versioned pages only)
+ *   3. canonical embedded in the doc markdown itself, if any
+ */
+const LayoutHead: FC<{ [key: string]: unknown }> = (props) => {
+  const { siteConfig: { url: siteUrl } } = useDocusaurusContext();
+  const { pathname } = useLocation();
+  const match = pathname.match(versionedDocPath);
+  const latestUrl = match ? `${siteUrl}${match[1].replace(/\/$/, 
'')}${match[2]}` : null;
+
+  return (
+    <>
+      <OriginalLayoutHead {...props} />
+      {latestUrl && (
+        <Head>
+          <meta property="og:url" content={latestUrl} />
+          <link rel="canonical" href={latestUrl} />
+        </Head>
+      )}
+    </>
+  );
+};
+
+export default LayoutHead;

Reply via email to