Skip to content

Commit 3e56486

Browse files
committed
feat(nx-dev): add TOC markdoc component for blog posts
1 parent 90e12a7 commit 3e56486

File tree

3 files changed

+85
-0
lines changed

3 files changed

+85
-0
lines changed

nx-dev/ui-markdoc/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ import { pill } from './lib/tags/pill.schema';
5656
import { fence } from './lib/nodes/fence.schema';
5757
import { FenceWrapper } from './lib/nodes/fence-wrapper.component';
5858
import { VideoPlayer, videoPlayer } from './lib/tags/video-player.component';
59+
import { TableOfContents } from './lib/tags/table-of-contents.component';
60+
import { tableOfContents } from './lib/tags/table-of-contents.schema';
5961
// TODO fix this export
6062
export { GithubRepository } from './lib/tags/github-repository.component';
6163

@@ -92,6 +94,7 @@ export const getMarkdocCustomConfig = (
9294
tab,
9395
tabs,
9496
'terminal-video': terminalVideo,
97+
toc: tableOfContents,
9598
tweet,
9699
youtube,
97100
'video-link': videoLink,
@@ -121,6 +124,7 @@ export const getMarkdocCustomConfig = (
121124
SideBySide,
122125
Tab,
123126
Tabs,
127+
TableOfContents,
124128
TerminalVideo,
125129
Tweet,
126130
YouTube,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
5+
interface TocItem {
6+
id: string;
7+
text: string;
8+
level: number;
9+
}
10+
11+
interface TableOfContentsProps {
12+
maxDepth?: number;
13+
}
14+
15+
export function TableOfContents({
16+
maxDepth = 3,
17+
}: TableOfContentsProps): JSX.Element {
18+
const [headings, setHeadings] = useState<TocItem[]>([]);
19+
20+
useEffect(() => {
21+
// Find the main content wrapper where markdown content is rendered
22+
const content = document.querySelector('[data-document="main"]');
23+
if (!content) return;
24+
25+
// Get all headings h1-h6 within the content
26+
const headingElements = content.querySelectorAll('h1, h2, h3, h4, h5, h6');
27+
28+
const items: TocItem[] = Array.from(headingElements)
29+
.map((heading) => {
30+
const level = parseInt(heading.tagName[1]);
31+
if (level > maxDepth) return null;
32+
33+
return {
34+
id: heading.id,
35+
text: heading.textContent || '',
36+
level,
37+
};
38+
})
39+
.filter((item): item is TocItem => item !== null);
40+
41+
setHeadings(items);
42+
}, [maxDepth]);
43+
44+
if (headings.length === 0) {
45+
return null;
46+
}
47+
48+
return (
49+
<nav className="toc not-prose mb-8 rounded-lg border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/50">
50+
<p className="mb-3 text-sm font-semibold text-slate-900 dark:text-slate-100">
51+
Table of Contents
52+
</p>
53+
<ul className="space-y-2 text-sm">
54+
{headings.map((heading) => (
55+
<li
56+
key={heading.id}
57+
style={{ paddingLeft: `${(heading.level - 1) * 1}rem` }}
58+
>
59+
<a
60+
href={`#${heading.id}`}
61+
className="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200"
62+
>
63+
{heading.text}
64+
</a>
65+
</li>
66+
))}
67+
</ul>
68+
</nav>
69+
);
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Schema } from '@markdoc/markdoc';
2+
3+
export const tableOfContents: Schema = {
4+
render: 'TableOfContents',
5+
attributes: {
6+
maxDepth: {
7+
type: 'Number',
8+
default: 3,
9+
},
10+
},
11+
};

0 commit comments

Comments
 (0)