From 034fd0c754fae79acc554861d608a747e6615a00 Mon Sep 17 00:00:00 2001 From: lllleolin-max Date: Sun, 13 Sep 2026 22:02:13 +0800 Subject: [PATCH] fix(markdown): preserve queries on index page links (#5436) --- .../unit/node/markdown/plugins/link.test.ts | 49 +++++++++++++++++++ src/node/markdown/plugins/link.ts | 13 +++-- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/__tests__/unit/node/markdown/plugins/link.test.ts b/__tests__/unit/node/markdown/plugins/link.test.ts index 119132c34..83c15da08 100644 --- a/__tests__/unit/node/markdown/plugins/link.test.ts +++ b/__tests__/unit/node/markdown/plugins/link.test.ts @@ -61,6 +61,42 @@ describe('node/markdown/plugins/link', () => { expect(env.links).toEqual(['./missing']) expect(env.linkLines).toEqual([3]) }) + + test.each([false, true])( + 'preserves index page queries (cleanUrls: %s)', + async (cleanUrls) => { + for (const [source, expected] of [ + ['/guide/index.md?lang=fr', '/guide/?lang=fr'], + [ + './index.md?lang=fr&mode=full#Hello%20World', + './?lang=fr&mode=full#hello-world' + ], + [ + '/guide/index.md?next=/other/index.md#Hello%20World', + '/guide/?next=/other/index.md#hello-world' + ], + [ + '/guide/index.md?lang=fr#:~:text=Hello%20World', + '/guide/?lang=fr#:~:text=Hello%20World' + ] + ]) { + expect( + await md.renderAsync(`[link](${source})`, { cleanUrls }) + ).toContain(`href="${expected}"`) + } + } + ) + + test.each([false, true])( + 'only removes the exact index.md filename (cleanUrls: %s)', + async (cleanUrls) => { + for (const file of ['indexAmd', 'index.md-extra']) { + expect( + await md.renderAsync(`[link](/guide/${file})`, { cleanUrls }) + ).toContain(`href="/guide/${file}${cleanUrls ? '' : '.html'}"`) + } + } + ) }) describe('node/markdown/plugins/link with a relative base', () => { @@ -114,6 +150,19 @@ describe('node/markdown/plugins/link with a relative base', () => { ) }) + test.each([false, true])( + 'preserves index page queries with a relative base (cleanUrls: %s)', + async (cleanUrls) => { + expect( + await render('[link](/guide/index.md?lang=fr#Hello%20World)', { + cleanUrls + }) + ).toContain( + `href="../guide/${cleanUrls ? '' : 'index.html'}?lang=fr#hello-world"` + ) + } + ) + test('content-loader renders keep absolute links site-absolute', async () => { // content loaders set relativePath but not relativizeUrls — their html // is embedded in other pages, so the source's depth must not apply diff --git a/src/node/markdown/plugins/link.ts b/src/node/markdown/plugins/link.ts index e9c52105c..aa1a61304 100644 --- a/src/node/markdown/plugins/link.ts +++ b/src/node/markdown/plugins/link.ts @@ -16,7 +16,7 @@ import { type MarkdownEnv } from '../../shared' -const indexRE = /(^|.*\/)index.md(#?.*)$/i +const indexRE = /(^|.*\/)index\.md$/i export const linkPlugin = ( md: MarkdownItAsync, @@ -112,12 +112,11 @@ export const linkPlugin = ( // directory urls need a server to resolve them, and file:// has none const explicitIndex = isRelativeBase(base) && !env.cleanUrls - const indexMatch = url.match(indexRE) + let cleanUrl = url.replace(/[?#].*$/, '') + const indexMatch = cleanUrl.match(indexRE) if (indexMatch) { - const [, path, hash] = indexMatch - url = path + (explicitIndex ? 'index.html' : '') + normalizeHash(hash) + cleanUrl = indexMatch[1] + (explicitIndex ? 'index.html' : '') } else { - let cleanUrl = url.replace(/[?#].*$/, '') // transform foo.md -> foo[.html] if (cleanUrl.endsWith('.md')) { cleanUrl = cleanUrl.replace(/\.md$/, env.cleanUrls ? '' : '.html') @@ -133,9 +132,9 @@ export const linkPlugin = ( if (explicitIndex && cleanUrl.endsWith('/')) { cleanUrl += 'index.html' } - const parsed = new URL(url, 'http://a.com') - url = cleanUrl + parsed.search + normalizeHash(parsed.hash) } + const parsed = new URL(url, 'http://a.com') + url = cleanUrl + parsed.search + normalizeHash(parsed.hash) // ensure leading . for relative paths if (!url.startsWith('/') && !url.startsWith('./')) {