Skip to content

Commit 3fe683a

Browse files
committed
fix(ui): normalize README heading fragments to lowercase slugs
- Decode and slugify in-page README anchors before prefixing - Keep already prefixed `user-content` fragments unchanged - Add coverage for mixed-case heading links
1 parent a2254fd commit 3fe683a

File tree

2 files changed

+24
-2
lines changed

2 files changed

+24
-2
lines changed

server/utils/readme.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,14 @@ function toUserContentHash(value: string): string {
298298
return `#${withUserContentPrefix(value)}`
299299
}
300300

301+
function decodeHashFragment(value: string): string {
302+
try {
303+
return decodeURIComponent(value)
304+
} catch {
305+
return value
306+
}
307+
}
308+
301309
function normalizePreservedAnchorAttrs(attrs: string): string {
302310
const cleanedAttrs = attrs
303311
.replace(/\s+href\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '')
@@ -333,8 +341,15 @@ function resolveUrl(url: string, packageName: string, repoInfo?: RepositoryInfo)
333341
if (!url) return url
334342
if (url.startsWith('#')) {
335343
// Prefix anchor links to match heading IDs (avoids collision with page IDs)
336-
// Idempotent: don't double-prefix if already prefixed
337-
return toUserContentHash(url.slice(1))
344+
// Normalize markdown-style heading fragments to the same slug format used
345+
// for generated README heading IDs, but leave already-prefixed values as-is.
346+
const fragment = url.slice(1)
347+
if (fragment.startsWith(USER_CONTENT_PREFIX)) {
348+
return `#${fragment}`
349+
}
350+
351+
const normalizedFragment = slugify(decodeHashFragment(fragment))
352+
return toUserContentHash(normalizedFragment || fragment)
338353
}
339354
// Absolute paths (e.g. /package/foo from a previous npmjs redirect) are already resolved
340355
if (url.startsWith('/')) return url

test/unit/server/utils/readme.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,13 @@ describe('Markdown File URL Resolution', () => {
293293

294294
expect(result.html).toContain('href="#user-content-installation"')
295295
})
296+
297+
it('normalizes mixed-case heading fragments to lowercase slugs', async () => {
298+
const markdown = `[Associations section](#Associations)`
299+
const result = await renderReadmeHtml(markdown, 'test-pkg')
300+
301+
expect(result.html).toContain('href="#user-content-associations"')
302+
})
296303
})
297304

298305
describe('different git providers', () => {

0 commit comments

Comments
 (0)