diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ba4e2b39827d1..b3a1f20b5dcbb 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 0000000000000..e2e5300a38d47 --- /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///) 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 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 ( + <> + + {latestUrl && ( + + + + + )} + + ); +}; + +export default LayoutHead;