Refactor TOC to use rehype-slug
- Rename package to `goodgo-docs` - Add `rehype-slug` for automatic heading ID generation - Simplify TableOfContents component by removing manual ID logic
This commit is contained in:
@@ -7,7 +7,7 @@ const withMDX = createMDX({
|
||||
options: {
|
||||
// Use string for Turbopack compatibility (Next.js 16)
|
||||
remarkPlugins: ['remark-gfm'],
|
||||
rehypePlugins: [],
|
||||
rehypePlugins: ['rehype-slug'],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
9014
apps/web-docs/package-lock.json
generated
9014
apps/web-docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "quaros-network-website",
|
||||
"name": "goodgo-docs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
@@ -22,6 +22,7 @@
|
||||
"next-intl": "^4.5.5",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"three": "^0.182.0"
|
||||
},
|
||||
|
||||
@@ -21,51 +21,19 @@ export default function TableOfContents({ locale }: TableOfContentsProps) {
|
||||
|
||||
// Extract headings from content
|
||||
useEffect(() => {
|
||||
|
||||
const generateSlug = (text: string): string => {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim();
|
||||
};
|
||||
|
||||
const extractHeadings = (): HeadingItem[] => {
|
||||
const content = document.querySelector('[data-docs-content]');
|
||||
if (!content) return [];
|
||||
|
||||
const headingElements = content.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
const headingItems: HeadingItem[] = [];
|
||||
const usedIds = new Set<string>();
|
||||
|
||||
headingElements.forEach((heading) => {
|
||||
const text = heading.textContent?.trim() || '';
|
||||
let id = heading.id || heading.getAttribute('id') || '';
|
||||
|
||||
// If no ID exists, generate one from text
|
||||
if (!id && text) {
|
||||
id = generateSlug(text);
|
||||
}
|
||||
|
||||
// Deduplicate ID
|
||||
if (id) {
|
||||
let uniqueId = id;
|
||||
let counter = 1;
|
||||
while (usedIds.has(uniqueId)) {
|
||||
uniqueId = `${id}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
id = uniqueId;
|
||||
|
||||
// Update the actual element ID so clicking TOC links works correctly
|
||||
heading.id = id;
|
||||
usedIds.add(id);
|
||||
}
|
||||
|
||||
const level = parseInt(heading.tagName.charAt(1));
|
||||
const id = heading.id;
|
||||
|
||||
if (id && text) {
|
||||
const level = parseInt(heading.tagName.charAt(1));
|
||||
headingItems.push({ id, text, level });
|
||||
}
|
||||
});
|
||||
@@ -74,21 +42,19 @@ export default function TableOfContents({ locale }: TableOfContentsProps) {
|
||||
};
|
||||
|
||||
// Function to update headings
|
||||
const updateHeadings = (): boolean => {
|
||||
const updateHeadings = () => {
|
||||
const headingItems = extractHeadings();
|
||||
setHeadings(headingItems);
|
||||
// Reset active ID if we have no headings (e.g. page transition)
|
||||
if (headingItems.length === 0) {
|
||||
setActiveId('');
|
||||
}
|
||||
return headingItems.length > 0;
|
||||
};
|
||||
|
||||
// Set up MutationObserver to watch for content changes
|
||||
const content = document.querySelector('[data-docs-content]');
|
||||
let observer: MutationObserver | null = null;
|
||||
let timers: NodeJS.Timeout[] = [];
|
||||
let foundHeadings = false;
|
||||
|
||||
const cleanup = () => {
|
||||
if (observer) {
|
||||
@@ -99,62 +65,31 @@ export default function TableOfContents({ locale }: TableOfContentsProps) {
|
||||
timers = [];
|
||||
};
|
||||
|
||||
// Function to try extracting with cleanup if successful
|
||||
const tryExtractAndCleanup = () => {
|
||||
if (updateHeadings() && !foundHeadings) {
|
||||
foundHeadings = true;
|
||||
// Keep observer running for dynamic updates, but stop timers
|
||||
timers.forEach(t => clearTimeout(t));
|
||||
timers = [];
|
||||
}
|
||||
};
|
||||
|
||||
// Try after a tiny delay to avoid synchronous setState in effect body (lint rule)
|
||||
const initialTimer = setTimeout(() => {
|
||||
tryExtractAndCleanup();
|
||||
}, 0);
|
||||
// Try extracting immediately
|
||||
const initialTimer = setTimeout(updateHeadings, 0);
|
||||
timers.push(initialTimer);
|
||||
|
||||
if (content) {
|
||||
// Watch for changes in the content
|
||||
// Watch for changes in the content structure (loading, hydration)
|
||||
observer = new MutationObserver(() => {
|
||||
tryExtractAndCleanup();
|
||||
updateHeadings();
|
||||
});
|
||||
|
||||
observer.observe(content, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['id'],
|
||||
// No longer watching attributes since we don't modify them
|
||||
});
|
||||
}
|
||||
|
||||
// Also try periodically in case content loads later (especially for dynamic MDX imports)
|
||||
const tryMultipleTimes = [100, 300, 500, 1000, 2000, 3000];
|
||||
// Try periodically for dynamic content
|
||||
const tryMultipleTimes = [100, 300, 500, 1000];
|
||||
tryMultipleTimes.forEach((delay) => {
|
||||
const timer = setTimeout(() => {
|
||||
if (!foundHeadings) {
|
||||
tryExtractAndCleanup();
|
||||
}
|
||||
}, delay);
|
||||
const timer = setTimeout(updateHeadings, delay);
|
||||
timers.push(timer);
|
||||
});
|
||||
|
||||
// Also listen for route changes (Next.js navigation)
|
||||
const handleRouteChange = () => {
|
||||
foundHeadings = false;
|
||||
// Reset and try again after a short delay
|
||||
setTimeout(() => {
|
||||
tryExtractAndCleanup();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handleRouteChange);
|
||||
|
||||
return () => {
|
||||
cleanup();
|
||||
window.removeEventListener('popstate', handleRouteChange);
|
||||
};
|
||||
return cleanup;
|
||||
}, [pathname]); // Re-run when pathname changes
|
||||
|
||||
// Set up intersection observer for active heading
|
||||
|
||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -234,6 +234,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: 19.2.0
|
||||
version: 19.2.0(react@19.2.0)
|
||||
rehype-slug:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
@@ -10500,6 +10503,10 @@ packages:
|
||||
nypm: 0.6.2
|
||||
pathe: 2.0.3
|
||||
|
||||
/github-slugger@2.0.0:
|
||||
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
|
||||
dev: false
|
||||
|
||||
/glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -10649,6 +10656,12 @@ packages:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
/hast-util-heading-rank@3.0.0:
|
||||
resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
dev: false
|
||||
|
||||
/hast-util-to-estree@3.1.3:
|
||||
resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==}
|
||||
dependencies:
|
||||
@@ -10692,6 +10705,12 @@ packages:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/hast-util-to-string@3.0.1:
|
||||
resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
dev: false
|
||||
|
||||
/hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
dependencies:
|
||||
@@ -14119,6 +14138,16 @@ packages:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/rehype-slug@6.0.0:
|
||||
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
github-slugger: 2.0.0
|
||||
hast-util-heading-rank: 3.0.0
|
||||
hast-util-to-string: 3.0.1
|
||||
unist-util-visit: 5.0.0
|
||||
dev: false
|
||||
|
||||
/remark-gfm@4.0.1:
|
||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user