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;
