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

moonming pushed a commit to branch fix/versioned-docs-canonical
in repository https://gitbox.apache.org/repos/asf/apisix-website.git

commit 33b37360650ee725b73f27f4ce614a0408fa856e
Author: Ming Wen <[email protected]>
AuthorDate: Mon Jul 13 09:54:33 2026 +0800

    fix: canonicalize versioned doc pages to the version-less latest URL
    
    Versioned doc pages (/docs/<project>/<version>/...) self-canonicalize by
    default, so Google indexes each version as an independent page competing
    with the version-less latest URL. GSC shows the resulting traffic
    migration: e.g. /docs/apisix/plugins/cors/ clicks fell 69 -> 14 while
    /docs/apisix/3.10/plugins/cors/ rose 0 -> 38 in the last 28 days.
    
    Wrap theme LayoutHead to re-point canonical (and og:url) of versioned
    and /next/ doc pages at the version-less URL. Precedence is preserved by
    react-helmet's last-wins rule: a canonical embedded in the doc markdown
    itself (e.g. the docs.api7.ai links on latest pages) still overrides
    this wrapper, and version-less pages are untouched.
    
    Verified on a production build:
    - /docs/apisix/3.16/plugins/cors/ -> canonical 
https://apisix.apache.org/docs/apisix/plugins/cors/
    - /zh/docs/apisix/3.16/plugins/cors/ -> canonical 
https://apisix.apache.org/zh/docs/apisix/plugins/cors/
    - /docs/apisix/plugins/cors/ (latest) -> canonical 
https://docs.api7.ai/hub/cors (unchanged)
    - exactly one canonical link per page; og:url matches
---
 doc/src/theme/LayoutHead/index.tsx | 50 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

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