From 51429df16740baa74b0d4421a0aad145b73f78fb Mon Sep 17 00:00:00 2001 From: TowyTowy Date: Thu, 9 Jul 2026 13:31:01 +0200 Subject: [PATCH] fix(build): respect cleanUrls in content loader render createContentLoader called md.renderAsync() without a MarkdownEnv, so the internal link plugin fell back to defaults: env.cleanUrls was undefined and internal links in the render/excerpt HTML always got a .html suffix even when cleanUrls was enabled. env.path and env.relativePath were also missing, breaking plugins that rely on them. Build the same env used during a normal page render and forward it to both the render and excerpt calls. close #4331 Co-Authored-By: Claude Fable 5 --- __tests__/unit/node/contentLoader.test.ts | 51 +++++++++++++++++++++++ src/node/contentLoader.ts | 22 ++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 __tests__/unit/node/contentLoader.test.ts diff --git a/__tests__/unit/node/contentLoader.test.ts b/__tests__/unit/node/contentLoader.test.ts new file mode 100644 index 00000000..197745f6 --- /dev/null +++ b/__tests__/unit/node/contentLoader.test.ts @@ -0,0 +1,51 @@ +import { resolveConfig } from 'node/config' +import { createContentLoader } from 'node/contentLoader' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' + +describe('node/contentLoader', () => { + let root: string | undefined + + afterEach(async () => { + if (root) { + await rm(root, { recursive: true, force: true }) + root = undefined + } + delete (global as any).VITEPRESS_CONFIG + }) + + async function setup(cleanUrls: boolean) { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-content-loader-')) + await writeFile( + path.join(root, 'index.md'), + '# Home\n\n[link](./other.md)\n' + ) + await writeFile(path.join(root, 'other.md'), '# Other\n') + + const siteConfig = await resolveConfig(root, 'build', 'production') + siteConfig.cleanUrls = cleanUrls + ;(global as any).VITEPRESS_CONFIG = siteConfig + } + + test('rendered internal links get .html when cleanUrls is false', async () => { + await setup(false) + + const data = await createContentLoader('index.md', { + render: true + }).load() + + expect(data[0].html).toContain('href="./other.html"') + }) + + test('rendered internal links are clean when cleanUrls is true', async () => { + await setup(true) + + const data = await createContentLoader('index.md', { + render: true + }).load() + + expect(data[0].html).toContain('href="./other"') + expect(data[0].html).not.toContain('./other.html') + }) +}) diff --git a/src/node/contentLoader.ts b/src/node/contentLoader.ts index 6a15ecd9..8a956c8f 100644 --- a/src/node/contentLoader.ts +++ b/src/node/contentLoader.ts @@ -5,7 +5,7 @@ import pMap from 'p-map' import { normalizePath } from 'vite' import type { SiteConfig } from './config' import { createMarkdownRenderer } from './markdown/markdown' -import type { Awaitable } from './shared' +import type { Awaitable, MarkdownEnv } from './shared' import { glob, normalizeGlob, type GlobOptions } from './utils/glob' export interface ContentOptions { @@ -127,15 +127,29 @@ export function createContentLoader( : { excerpt: renderExcerpt as any } // gray-matter types are wrong ) + const relativePath = normalizePath(path.relative(config.srcDir, file)) + const url = '/' + - normalizePath(path.relative(config.srcDir, file)) + relativePath .replace(/(^|\/)index\.md$/, '$1') .replace(/\.md$/, config.cleanUrls ? '' : '.html') - const html = options.render ? await md.renderAsync(src) : undefined + // pass a markdown env so plugins (e.g. the internal link plugin) + // resolve links, `cleanUrls` and paths the same way as during a + // normal page render instead of falling back to defaults + const env: MarkdownEnv = { + path: file, + relativePath, + cleanUrls: !!config.cleanUrls, + realPath: file + } + + const html = options.render + ? await md.renderAsync(src, env) + : undefined const renderedExcerpt = renderExcerpt - ? excerpt && (await md.renderAsync(excerpt)) + ? excerpt && (await md.renderAsync(excerpt, env)) : undefined const data: ContentData = {