Skip to content

Commit 8230b60

Browse files
committed
Fix: fix duplicate slugs rendered for markdown components
1 parent ec9c086 commit 8230b60

5 files changed

Lines changed: 44 additions & 9 deletions

File tree

apps/svelte.dev/src/routes/content.json/+server.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Tokens } from 'marked';
22
import { index, docs as _docs, examples } from '$lib/server/content';
33
import { json } from '@sveltejs/kit';
4-
import { transform, slugify, clean } from '@sveltejs/site-kit/markdown';
4+
import { transform, slugify, clean, create_slug_deduper } from '@sveltejs/site-kit/markdown';
55
import type { Block } from '@sveltejs/site-kit/search';
66
import { get_slug } from '../tutorial/[...slug]/content.server';
77

@@ -38,6 +38,8 @@ async function content() {
3838
const intro = sections?.shift()?.trim()!;
3939
const rank = +metadata.rank;
4040

41+
const dedupe = create_slug_deduper();
42+
4143
blocks.push({
4244
breadcrumbs: [...breadcrumbs, clean(metadata.title ?? '')],
4345
href: get_href([slug]),
@@ -53,13 +55,15 @@ async function content() {
5355
continue;
5456
}
5557

58+
const h2_slug = dedupe(slugify(h2));
59+
5660
const content = lines.join('\n');
5761
const subsections = content.trim().split('## ');
5862
const intro = subsections?.shift()?.trim();
5963
if (intro) {
6064
blocks.push({
6165
breadcrumbs: [...breadcrumbs, clean(metadata.title), clean(h2)],
62-
href: get_href([slug, slugify(h2)]),
66+
href: get_href([slug, h2_slug]),
6367
content: await plaintext(intro),
6468
rank
6569
});
@@ -73,9 +77,11 @@ async function content() {
7377
continue;
7478
}
7579

80+
const h3_slug = dedupe(`${h2_slug}-${slugify(h3)}`);
81+
7682
blocks.push({
7783
breadcrumbs: [...breadcrumbs, clean(metadata.title), clean(h2), clean(h3)],
78-
href: get_href([slug, slugify(h2) + '-' + slugify(h3)]),
84+
href: get_href([slug, h3_slug]),
7985
content: await plaintext(lines.join('\n').trim()),
8086
rank
8187
});

packages/site-kit/src/lib/markdown/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export { render_content_markdown } from './renderer.ts';
22

3-
export { transform, slugify, clean, strip_origin } from './utils.ts';
3+
export { transform, slugify, clean, strip_origin, create_slug_deduper } from './utils.ts';
44

55
// TODO none of these really belong here
66
export type Modules = Array<{

packages/site-kit/src/lib/markdown/renderer.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { transformerTwoslash, rendererRich } from '@shikijs/twoslash';
1212
import { createFileSystemTypesCache } from '@shikijs/vitepress-twoslash/cache-fs';
1313
import { compress_and_encode_text } from 'gzip';
1414
import {
15+
create_slug_deduper,
1516
decode_html_entities,
1617
SHIKI_LANGUAGE_MAP,
1718
slugify,
@@ -329,6 +330,7 @@ export async function render_content_markdown(
329330
twoslashBanner?: TwoslashBanner
330331
) {
331332
const headings: string[] = [];
333+
const dedupe_slug = create_slug_deduper();
332334
const { check = true, references } = options ?? {};
333335

334336
interface CodeBlockFile {
@@ -598,9 +600,16 @@ export async function render_content_markdown(
598600
const text = this.parser!.parseInline(tokens);
599601
const html = text.replace(/<\/?code>/g, '');
600602

601-
headings[depth - 1] = slugify(text);
603+
const parent = headings
604+
.slice(0, depth - 1)
605+
.filter(Boolean)
606+
.join('-');
607+
const raw_segment = slugify(text);
608+
const slug = dedupe_slug(parent ? `${parent}-${raw_segment}` : raw_segment);
609+
const segment = parent ? slug.slice(parent.length + 1) : slug;
610+
611+
headings[depth - 1] = segment;
602612
headings.length = depth;
603-
const slug = headings.filter(Boolean).join('-');
604613

605614
return `<h${depth} id="${slug}"><span>${html}</span><a href="#${slug}" class="permalink" aria-label="permalink"></a></h${depth}>`;
606615
},

packages/site-kit/src/lib/markdown/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ export const slugify = (str: string) => {
8585
);
8686
};
8787

88+
export function create_slug_deduper() {
89+
const seen = new Map<string, number>();
90+
return (slug: string) => {
91+
const count = seen.get(slug) ?? 0;
92+
seen.set(slug, count + 1);
93+
return count > 0 ? `${slug}-${count}` : slug;
94+
};
95+
}
96+
8897
export function smart_quotes(
8998
str: string,
9099
{ first = true, html = false }: { first?: boolean; html?: boolean } = {}

packages/site-kit/src/lib/server/content/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { extract_frontmatter, is_in_code_block, slugify, smart_quotes } from '../../markdown/utils';
1+
import {
2+
create_slug_deduper,
3+
extract_frontmatter,
4+
is_in_code_block,
5+
slugify,
6+
smart_quotes
7+
} from '../../markdown/utils';
28
import type { Document, Section } from '../../types';
39

410
export async function create_index(
@@ -30,6 +36,8 @@ export async function create_index(
3036
'<code>$1</code>'
3137
);
3238

39+
const dedupe = create_slug_deduper();
40+
3341
const sections = Array.from(body.matchAll(/^#{2,3}\s+(.*)$/gm)).reduce((arr, match) => {
3442
if (is_in_code_block(body, match.index || 0)) return arr;
3543
const title = match[1];
@@ -43,10 +51,13 @@ export async function create_index(
4351
if (match[0].startsWith('###')) {
4452
const section = arr.at(-1);
4553
if (section) {
46-
section.subsections.push({ slug: `${section.slug}-${slug}`, title: displayed_title });
54+
section.subsections.push({
55+
slug: dedupe(`${section.slug}-${slug}`),
56+
title: displayed_title
57+
});
4758
}
4859
} else {
49-
arr.push({ slug, title: displayed_title, subsections: [] });
60+
arr.push({ slug: dedupe(slug), title: displayed_title, subsections: [] });
5061
}
5162

5263
return arr;

0 commit comments

Comments
 (0)