Skip to content

Commit dbdd35f

Browse files
authored
swizzle the code that build the version menu to find other versions of the doc using slug, if id do not work (#3291)
1 parent 22ad45d commit dbdd35f

1 file changed

Lines changed: 202 additions & 0 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* Swizzled DocsVersionDropdownNavbarItem
3+
*
4+
* Reason: Docusaurus matches alternate version docs by `doc.id` only.
5+
* When a doc file is moved/renamed across versions (different id but same slug),
6+
* the version dropdown can't find the equivalent page in other versions.
7+
*
8+
* Fix: after the standard id-based lookup, fall back to matching by path suffix
9+
* (the slug portion of the URL after stripping the version path prefix).
10+
*/
11+
12+
import React from 'react';
13+
import { useLocation } from '@docusaurus/router';
14+
import {
15+
useVersions,
16+
useActiveDocContext,
17+
useDocsVersionCandidates,
18+
useDocsPreferredVersion,
19+
} from '@docusaurus/plugin-content-docs/client';
20+
import { translate } from '@docusaurus/Translate';
21+
import { useHistorySelector } from '@docusaurus/theme-common';
22+
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
23+
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
24+
25+
function getVersionItems(versions, configs) {
26+
if (configs) {
27+
const versionMap = new Map(
28+
versions.map((version) => [version.name, version]),
29+
);
30+
const toVersionItem = (name, config) => {
31+
const version = versionMap.get(name);
32+
if (!version) {
33+
throw new Error(
34+
`No docs version exist for name '${name}', please verify your 'docsVersionDropdown' navbar item versions config.\nAvailable version names:\n- ${versions.map((v) => `${v.name}`).join('\n- ')}`,
35+
);
36+
}
37+
return { version, label: config?.label ?? version.label };
38+
};
39+
if (Array.isArray(configs)) {
40+
return configs.map((name) => toVersionItem(name, undefined));
41+
} else {
42+
return Object.entries(configs).map(([name, config]) =>
43+
toVersionItem(name, config),
44+
);
45+
}
46+
} else {
47+
return versions.map((version) => ({ version, label: version.label }));
48+
}
49+
}
50+
51+
function useVersionItems({ docsPluginId, configs }) {
52+
const versions = useVersions(docsPluginId);
53+
return getVersionItems(versions, configs);
54+
}
55+
56+
function getVersionMainDoc(version) {
57+
return version.docs.find((doc) => doc.id === version.mainDocId);
58+
}
59+
60+
/**
61+
* Extract the slug portion of a doc path by stripping the version path prefix.
62+
* e.g. doc.path="/docs/20/commands/foo", version.path="/docs/20" => "/commands/foo"
63+
*/
64+
function getDocSlug(doc, version) {
65+
let slug = doc.path;
66+
if (slug.startsWith(version.path)) {
67+
slug = slug.slice(version.path.length);
68+
}
69+
// Normalize: strip leading slash for consistent comparison
70+
// (version.path may or may not have a trailing slash)
71+
return slug.replace(/^\//, '');
72+
}
73+
74+
/**
75+
* Enhanced version of getVersionTargetDoc that adds a path-suffix fallback.
76+
* 1. First tries the standard id-based alternateDocVersions lookup
77+
* 2. If not found, computes the current doc's slug (path minus version prefix)
78+
* and searches for a doc with the same slug in the target version
79+
* 3. Falls back to the version's main doc
80+
*/
81+
function getVersionTargetDoc(version, activeDocContext, versions, pathname) {
82+
const activeDoc = activeDocContext.activeDoc;
83+
const activeVersion = activeDocContext.activeVersion;
84+
85+
// Standard id-based match (original Docusaurus behavior)
86+
const idMatch = activeDocContext.alternateDocVersions[version.name];
87+
if (idMatch) {
88+
return idMatch;
89+
}
90+
91+
// Path-suffix fallback for moved/renamed docs
92+
// Compute slug from activeDoc if available, otherwise from pathname
93+
let currentSlug;
94+
if (activeDoc && activeVersion) {
95+
currentSlug = getDocSlug(activeDoc, activeVersion);
96+
} else if (activeVersion) {
97+
// activeDoc is missing but we know the version — extract slug from pathname
98+
currentSlug = pathname.startsWith(activeVersion.path)
99+
? pathname.slice(activeVersion.path.length).replace(/^\//, '')
100+
: pathname.replace(/^\//, '');
101+
}
102+
103+
if (currentSlug) {
104+
const slugMatch = version.docs.find(
105+
(doc) => getDocSlug(doc, version) === currentSlug,
106+
);
107+
if (slugMatch) {
108+
return slugMatch;
109+
}
110+
}
111+
112+
// Ultimate fallback: main doc of the version
113+
return getVersionMainDoc(version);
114+
}
115+
116+
function useDisplayedVersionItem({ docsPluginId, versionItems }) {
117+
const candidates = useDocsVersionCandidates(docsPluginId);
118+
const candidateItems = candidates
119+
.map((candidate) => versionItems.find((vi) => vi.version === candidate))
120+
.filter((vi) => vi !== undefined);
121+
return candidateItems[0] ?? versionItems[0];
122+
}
123+
124+
export default function DocsVersionDropdownNavbarItem({
125+
mobile,
126+
docsPluginId,
127+
dropdownActiveClassDisabled,
128+
dropdownItemsBefore,
129+
dropdownItemsAfter,
130+
versions: configs,
131+
...props
132+
}) {
133+
const { pathname } = useLocation();
134+
const search = useHistorySelector((history) => history.location.search);
135+
const hash = useHistorySelector((history) => history.location.hash);
136+
const activeDocContext = useActiveDocContext(docsPluginId);
137+
const { savePreferredVersionName } = useDocsPreferredVersion(docsPluginId);
138+
const versionItems = useVersionItems({ docsPluginId, configs });
139+
const allVersions = useVersions(docsPluginId);
140+
const displayedVersionItem = useDisplayedVersionItem({
141+
docsPluginId,
142+
versionItems,
143+
});
144+
145+
function versionItemToLink({ version, label }) {
146+
const targetDoc = getVersionTargetDoc(version, activeDocContext, allVersions, pathname);
147+
return {
148+
label,
149+
// preserve ?search#hash suffix on version switches
150+
to: `${targetDoc.path}${search}${hash}`,
151+
isActive: () => version === activeDocContext.activeVersion,
152+
onClick: () => savePreferredVersionName(version.name),
153+
};
154+
}
155+
156+
const items = [
157+
...dropdownItemsBefore,
158+
...versionItems.map(versionItemToLink),
159+
...dropdownItemsAfter,
160+
];
161+
162+
const dropdownLabel =
163+
mobile && items.length > 1
164+
? translate({
165+
id: 'theme.navbar.mobileVersionsDropdown.label',
166+
message: 'Versions',
167+
description:
168+
'The label for the navbar versions dropdown on mobile view',
169+
})
170+
: displayedVersionItem.label;
171+
const dropdownTo =
172+
mobile && items.length > 1
173+
? undefined
174+
: getVersionTargetDoc(
175+
displayedVersionItem.version,
176+
activeDocContext,
177+
allVersions,
178+
pathname,
179+
).path;
180+
181+
if (items.length <= 1) {
182+
return (
183+
<DefaultNavbarItem
184+
{...props}
185+
mobile={mobile}
186+
label={dropdownLabel}
187+
to={dropdownTo}
188+
isActive={dropdownActiveClassDisabled ? () => false : undefined}
189+
/>
190+
);
191+
}
192+
return (
193+
<DropdownNavbarItem
194+
{...props}
195+
mobile={mobile}
196+
label={dropdownLabel}
197+
to={dropdownTo}
198+
items={items}
199+
isActive={dropdownActiveClassDisabled ? () => false : undefined}
200+
/>
201+
);
202+
}

0 commit comments

Comments
 (0)