diff --git a/apps/svelte.dev/src/routes/content.json/+server.ts b/apps/svelte.dev/src/routes/content.json/+server.ts
index 1dd2fb4898..c6795a06c3 100644
--- a/apps/svelte.dev/src/routes/content.json/+server.ts
+++ b/apps/svelte.dev/src/routes/content.json/+server.ts
@@ -1,7 +1,7 @@
import type { Tokens } from 'marked';
import { index, docs as _docs, examples } from '$lib/server/content';
import { json } from '@sveltejs/kit';
-import { transform, slugify, clean } from '@sveltejs/site-kit/markdown';
+import { transform, slugify, clean, create_slug_deduper } from '@sveltejs/site-kit/markdown';
import type { Block } from '@sveltejs/site-kit/search';
import { get_slug } from '../tutorial/[...slug]/content.server';
@@ -38,6 +38,8 @@ async function content() {
const intro = sections?.shift()?.trim()!;
const rank = +metadata.rank;
+ const dedupe = create_slug_deduper();
+
blocks.push({
breadcrumbs: [...breadcrumbs, clean(metadata.title ?? '')],
href: get_href([slug]),
@@ -53,13 +55,15 @@ async function content() {
continue;
}
+ const h2_slug = dedupe(slugify(h2));
+
const content = lines.join('\n');
const subsections = content.trim().split('## ');
const intro = subsections?.shift()?.trim();
if (intro) {
blocks.push({
breadcrumbs: [...breadcrumbs, clean(metadata.title), clean(h2)],
- href: get_href([slug, slugify(h2)]),
+ href: get_href([slug, h2_slug]),
content: await plaintext(intro),
rank
});
@@ -73,9 +77,11 @@ async function content() {
continue;
}
+ const h3_slug = dedupe(`${h2_slug}-${slugify(h3)}`);
+
blocks.push({
breadcrumbs: [...breadcrumbs, clean(metadata.title), clean(h2), clean(h3)],
- href: get_href([slug, slugify(h2) + '-' + slugify(h3)]),
+ href: get_href([slug, h3_slug]),
content: await plaintext(lines.join('\n').trim()),
rank
});
diff --git a/packages/site-kit/src/lib/markdown/index.ts b/packages/site-kit/src/lib/markdown/index.ts
index a523e1d603..59f73e1096 100644
--- a/packages/site-kit/src/lib/markdown/index.ts
+++ b/packages/site-kit/src/lib/markdown/index.ts
@@ -1,6 +1,6 @@
export { render_content_markdown } from './renderer.ts';
-export { transform, slugify, clean, strip_origin } from './utils.ts';
+export { transform, slugify, clean, strip_origin, create_slug_deduper } from './utils.ts';
// TODO none of these really belong here
export type Modules = Array<{
diff --git a/packages/site-kit/src/lib/markdown/renderer.ts b/packages/site-kit/src/lib/markdown/renderer.ts
index bf5fee6978..5c52483744 100644
--- a/packages/site-kit/src/lib/markdown/renderer.ts
+++ b/packages/site-kit/src/lib/markdown/renderer.ts
@@ -12,6 +12,7 @@ import { transformerTwoslash, rendererRich } from '@shikijs/twoslash';
import { createFileSystemTypesCache } from '@shikijs/vitepress-twoslash/cache-fs';
import { compress_and_encode_text } from 'gzip';
import {
+ create_slug_deduper,
decode_html_entities,
SHIKI_LANGUAGE_MAP,
slugify,
@@ -329,6 +330,7 @@ export async function render_content_markdown(
twoslashBanner?: TwoslashBanner
) {
const headings: string[] = [];
+ const dedupe_slug = create_slug_deduper();
const { check = true, references } = options ?? {};
interface CodeBlockFile {
@@ -598,9 +600,16 @@ export async function render_content_markdown(
const text = this.parser!.parseInline(tokens);
const html = text.replace(/<\/?code>/g, '');
- headings[depth - 1] = slugify(text);
+ const parent = headings
+ .slice(0, depth - 1)
+ .filter(Boolean)
+ .join('-');
+ const raw_segment = slugify(text);
+ const slug = dedupe_slug(parent ? `${parent}-${raw_segment}` : raw_segment);
+ const segment = parent ? slug.slice(parent.length + 1) : slug;
+
+ headings[depth - 1] = segment;
headings.length = depth;
- const slug = headings.filter(Boolean).join('-');
return `${html}`;
},
diff --git a/packages/site-kit/src/lib/markdown/utils.ts b/packages/site-kit/src/lib/markdown/utils.ts
index 3f55abe8ba..fb2ee94eb6 100644
--- a/packages/site-kit/src/lib/markdown/utils.ts
+++ b/packages/site-kit/src/lib/markdown/utils.ts
@@ -85,6 +85,15 @@ export const slugify = (str: string) => {
);
};
+export function create_slug_deduper() {
+ const seen = new Map();
+ return (slug: string) => {
+ const count = seen.get(slug) ?? 0;
+ seen.set(slug, count + 1);
+ return count > 0 ? `${slug}-${count}` : slug;
+ };
+}
+
export function smart_quotes(
str: string,
{ first = true, html = false }: { first?: boolean; html?: boolean } = {}
diff --git a/packages/site-kit/src/lib/server/content/index.ts b/packages/site-kit/src/lib/server/content/index.ts
index addaec4bf7..d765dcf566 100644
--- a/packages/site-kit/src/lib/server/content/index.ts
+++ b/packages/site-kit/src/lib/server/content/index.ts
@@ -1,4 +1,10 @@
-import { extract_frontmatter, is_in_code_block, slugify, smart_quotes } from '../../markdown/utils';
+import {
+ create_slug_deduper,
+ extract_frontmatter,
+ is_in_code_block,
+ slugify,
+ smart_quotes
+} from '../../markdown/utils';
import type { Document, Section } from '../../types';
export async function create_index(
@@ -30,6 +36,8 @@ export async function create_index(
'$1'
);
+ const dedupe = create_slug_deduper();
+
const sections = Array.from(body.matchAll(/^#{2,3}\s+(.*)$/gm)).reduce((arr, match) => {
if (is_in_code_block(body, match.index || 0)) return arr;
const title = match[1];
@@ -43,10 +51,13 @@ export async function create_index(
if (match[0].startsWith('###')) {
const section = arr.at(-1);
if (section) {
- section.subsections.push({ slug: `${section.slug}-${slug}`, title: displayed_title });
+ section.subsections.push({
+ slug: dedupe(`${section.slug}-${slug}`),
+ title: displayed_title
+ });
}
} else {
- arr.push({ slug, title: displayed_title, subsections: [] });
+ arr.push({ slug: dedupe(slug), title: displayed_title, subsections: [] });
}
return arr;