From ad4d08fc1ccd791a8071b9fd5574fc18940f5a09 Mon Sep 17 00:00:00 2001 From: bhuvan-somisetty Date: Sun, 2 Aug 2026 17:34:01 +0530 Subject: [PATCH 1/2] fix: scope changelog plugin state per loadContent() and derive figure locale from pathname MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit changelog-plugin: publishTimes and authorsMap were declared as module-level globals. During a multi-locale build Docusaurus runs loadContent() sequentially for each locale (en, zh) within the same Node process. As a result the zh pass found all en timestamps already in publishTimes and the dedup loop decremented every hour offset by the number of en releases, causing zh changelog post dates and RSS/Atom timestamps to shift backward relative to en. Fix: declare publishTimes and authorsMap inside loadContent() so every locale pass starts with empty state. imageFigureNumber.js: the client module fired three staggered setTimeouts and read document.documentElement.lang to choose the Figure/图 prefix. The html[lang] attribute is updated by Docusaurus after the route transition completes; early timeout callbacks read the previous locale value, producing English captions on Chinese pages or vice versa. Overlapping timeouts without idempotency guards also created duplicate figcaption elements under fast navigation. Fix: replace triple setTimeout with a single requestAnimationFrame (Docusaurus has already committed the incoming page to the DOM by the time onRouteDidUpdate fires), derive locale from location.pathname, and always overwrite the figcaption text rather than skipping already-annotated figures. Fixes #699 Signed-off-by: bhuvan-somisetty --- src/client/imageFigureNumber.js | 78 +++++++++++++++------------------ src/plugins/changelog/index.js | 23 ++++++---- 2 files changed, 51 insertions(+), 50 deletions(-) diff --git a/src/client/imageFigureNumber.js b/src/client/imageFigureNumber.js index ee50aa658..f32df45a4 100644 --- a/src/client/imageFigureNumber.js +++ b/src/client/imageFigureNumber.js @@ -2,28 +2,48 @@ * Client module to automatically add figure numbers and captions to images * based on their alt text in blog posts and docs. */ -export function onRouteDidUpdate({ location, previousLocation }) { + +/** + * Derive the locale from the current pathname so the caption prefix is always + * consistent with the page the user navigated *to*, not with whatever stale + * value `document.documentElement.lang` happens to hold during the transition. + * + * @param {string} pathname + * @returns {"zh" | "en"} + */ +function localeFromPathname(pathname) { + return pathname.startsWith("/zh/") || pathname === "/zh" ? "zh" : "en"; +} + +export function onRouteDidUpdate({ location }) { // Only run on blog and doc pages if (!location.pathname.match(/\/blog\//) && !location.pathname.match(/\/docs\//)) { return; } - // Wait for DOM to be ready with multiple attempts - setTimeout(() => addFigureNumbers(), 100); - setTimeout(() => addFigureNumbers(), 500); - setTimeout(() => addFigureNumbers(), 1000); + const locale = localeFromPathname(location.pathname); + + // A single rAF tick is sufficient: Docusaurus has already committed the new + // page content to the DOM by the time onRouteDidUpdate fires. Using rAF + // instead of multiple staggered setTimeouts avoids duplicate caption + // injections when routes are navigated quickly. + requestAnimationFrame(() => addFigureNumbers(locale)); } -function addFigureNumbers() { +/** + * @param {"zh" | "en"} locale + */ +function addFigureNumbers(locale) { // Find all images in markdown content const articleContent = document.querySelector("article"); if (!articleContent) { return; } - // Find all images that are not in header or footer + const prefix = locale === "zh" ? "图" : "Figure"; + + // Find all images that are not logos or small icons const images = Array.from(articleContent.querySelectorAll("img")).filter((img) => { - // Filter out logos, avatars, icons const parentClass = img.parentElement?.className || ""; const isLogo = parentClass.includes("logo") || img.alt.includes("logo"); const isIcon = img.width < 100 || img.height < 100; @@ -33,26 +53,14 @@ function addFigureNumbers() { let figureCount = 0; images.forEach((img) => { - // Get alt text const altText = img.getAttribute("alt") || ""; if (!altText.trim()) { return; } - // Check if already has figcaption with figure number - const existingFigure = img.closest("figure"); - if (existingFigure) { - const existingFigcaption = existingFigure.querySelector("figcaption"); - if (existingFigcaption && existingFigcaption.textContent.match(/^(图|Figure)\d+:/)) { - figureCount++; - return; - } - } - - // Increment counter figureCount++; - // Create figure wrapper if it doesn't exist + // Wrap in
if not already wrapped let figure = img.closest("figure"); if (!figure) { figure = document.createElement("figure"); @@ -60,30 +68,16 @@ function addFigureNumbers() { figure.appendChild(img); } - // Remove existing figcaption if any (but only if it doesn't have figure number) - const existingFigcaption = figure.querySelector("figcaption"); - if (existingFigcaption && !existingFigcaption.textContent.match(/^(图|Figure)\d+:/)) { - existingFigcaption.remove(); - } else if (existingFigcaption) { - // Update existing figcaption with figure number - const currentLang = document.documentElement.lang || "en"; - const prefix = currentLang.startsWith("zh") ? "图" : "Figure"; - existingFigcaption.textContent = `${prefix}${figureCount}: ${altText}`; - return; + // Find or create the
, always overwriting its text so that + // navigating between locales corrects stale captions from a previous run. + let figcaption = figure.querySelector("figcaption"); + if (!figcaption) { + figcaption = document.createElement("figcaption"); + figure.appendChild(figcaption); } - - // Create figcaption with number and alt text - const figcaption = document.createElement("figcaption"); - const currentLang = document.documentElement.lang || "en"; - - // Use appropriate prefix based on language - const prefix = currentLang.startsWith("zh") ? "图" : "Figure"; figcaption.textContent = `${prefix}${figureCount}: ${altText}`; - // Append figcaption to figure - figure.appendChild(figcaption); - - // Add styling class + // Apply styles (idempotent — repeated assignment is harmless) figure.style.cssText = ` margin: 2em 0; text-align: center; diff --git a/src/plugins/changelog/index.js b/src/plugins/changelog/index.js index c5a6b6091..2bbd23e4a 100644 --- a/src/plugins/changelog/index.js +++ b/src/plugins/changelog/index.js @@ -13,17 +13,18 @@ import {aliasedSitePath, docuHash, normalizeUrl} from '@docusaurus/utils'; /** * Multiple versions may be published on the same day, causing the order to be * the reverse. Therefore, our publish time has a "fake hour" to order them. + * + * NOTE: Both sets are passed in from loadContent() so that each build pass + * (en, zh, …) starts with a clean slate and locale runs cannot interfere with + * each other's timestamps or author maps. */ -const publishTimes = new Set(); -/** - * @type {Record} - */ -const authorsMap = {}; /** * @param {string} section + * @param {Set} publishTimes per-invocation dedup set + * @param {Record} authorsMap per-invocation authors accumulator */ -function processSection(section) { +function processSection(section, publishTimes, authorsMap) { const title = section .match(/\n## .*/)?.[0] .trim() @@ -94,7 +95,7 @@ function processSection(section) { publishTimes.add(`${date}T${hour}:00`); return { - title: title.replace(/ \(.*\)/, ''), + title: title.replace(/ \(.*\)/, ""), content: `--- mdx: format: md @@ -138,10 +139,16 @@ export default async function ChangelogPlugin(context, options) { ...blogPlugin, name: 'changelog-plugin', async loadContent() { + // Create fresh state per loadContent() call so that sequential locale + // build passes (en → zh) cannot see each other's timestamps or authors. + const publishTimes = new Set(); + /** @type {Record} */ + const authorsMap = {}; + const fileContent = (await fs.readFile(changelogPath, 'utf-8')).replace(/\r\n/g, '\n'); const sections = fileContent .split(/(?=\n## )/) - .map(processSection) + .map((section) => processSection(section, publishTimes, authorsMap)) .filter(Boolean); await Promise.all( sections.map((section) => From 176e8acfe6d61f10da810f0a7ce28e283b9abaab Mon Sep 17 00:00:00 2001 From: bhuvan-somisetty Date: Sun, 2 Aug 2026 17:44:45 +0530 Subject: [PATCH 2/2] fix(media): handle root doc routes and preserve anchor wrappers around images Update route regex in onRouteDidUpdate to match root document paths such as /docs and /zh/docs. Also detect when an image is wrapped inside an anchor tag and move the anchor into the figure element so image hyperlinks remain functional. Signed-off-by: bhuvan-somisetty --- src/client/imageFigureNumber.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/client/imageFigureNumber.js b/src/client/imageFigureNumber.js index f32df45a4..81e09f006 100644 --- a/src/client/imageFigureNumber.js +++ b/src/client/imageFigureNumber.js @@ -16,8 +16,8 @@ function localeFromPathname(pathname) { } export function onRouteDidUpdate({ location }) { - // Only run on blog and doc pages - if (!location.pathname.match(/\/blog\//) && !location.pathname.match(/\/docs\//)) { + // Only run on blog and doc pages (including root doc paths like /docs or /zh/docs) + if (!/(?:^|\/)(?:blog|docs)(?:$|\/)/.test(location.pathname)) { return; } @@ -60,12 +60,15 @@ function addFigureNumbers(locale) { figureCount++; - // Wrap in
if not already wrapped + // Wrap in
if not already wrapped. If the image is inside an anchor link, + // move the anchor element into the figure to preserve clickable links. let figure = img.closest("figure"); if (!figure) { figure = document.createElement("figure"); - img.parentNode.insertBefore(figure, img); - figure.appendChild(img); + const targetElement = + img.parentElement && img.parentElement.tagName === "A" ? img.parentElement : img; + targetElement.parentNode.insertBefore(figure, targetElement); + figure.appendChild(targetElement); } // Find or create the
, always overwriting its text so that