diff --git a/components/post-layout.tsx b/components/post-layout.tsx index 582a9a2..a60accc 100644 --- a/components/post-layout.tsx +++ b/components/post-layout.tsx @@ -56,7 +56,7 @@ export function PostLayout({ children, hasToc = true, contentKey }: { children:
{isTocOpen && hasToc && (
- +
)}
@@ -67,7 +67,7 @@ export function PostLayout({ children, hasToc = true, contentKey }: { children: {isTocOpen && hasToc && (
- setIsTocOpen(false)} /> + setIsTocOpen(false)} />
)} diff --git a/components/post-toc.tsx b/components/post-toc.tsx index 2aeb254..e65495a 100644 --- a/components/post-toc.tsx +++ b/components/post-toc.tsx @@ -1,7 +1,6 @@ 'use client'; import { useEffect, useRef, useState } from 'react'; -import { usePathname } from 'next/navigation'; import { FiList } from 'react-icons/fi'; interface TocItem { @@ -10,63 +9,68 @@ interface TocItem { depth: number; } -export function PostToc({ onLinkClick }: { onLinkClick?: () => void }) { +export function PostToc({ onLinkClick, contentKey }: { onLinkClick?: () => void; contentKey?: string }) { const [items, setItems] = useState([]); const [activeId, setActiveId] = useState(null); const listRef = useRef(null); const itemRefs = useRef>({}); const [indicator, setIndicator] = useState({ top: 0, opacity: 0 }); - const pathname = usePathname(); useEffect(() => { - // Clear items immediately when pathname changes + // Clear items immediately when content changes setItems([]); setActiveId(null); let observer: IntersectionObserver | null = null; + let rafId1: number; + let rafId2: number; - // Small delay to ensure DOM has been updated with new article content - const timeoutId = setTimeout(() => { - const headings = Array.from( - document.querySelectorAll('article h2, article h3') - ); - const mapped = headings - .filter((el) => el.id) - .map((el) => ({ - id: el.id, - text: el.innerText, - depth: el.tagName === 'H3' ? 3 : 2 - })); - setItems(mapped); + // Use double requestAnimationFrame to ensure DOM has been painted + // This is more reliable than setTimeout for DOM updates + rafId1 = requestAnimationFrame(() => { + rafId2 = requestAnimationFrame(() => { + const headings = Array.from( + document.querySelectorAll('article h2, article h3') + ); + const mapped = headings + .filter((el) => el.id) + .map((el) => ({ + id: el.id, + text: el.innerText, + depth: el.tagName === 'H3' ? 3 : 2 + })); + setItems(mapped); - observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - const id = (entry.target as HTMLElement).id; - if (id) { - setActiveId(id); + observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const id = (entry.target as HTMLElement).id; + if (id) { + setActiveId(id); + } } - } - }); - }, - { - // Trigger when heading is in upper 40% of viewport - rootMargin: '0px 0px -60% 0px', - threshold: 0.1 - } - ); + }); + }, + { + // Trigger when heading is in upper 40% of viewport + rootMargin: '0px 0px -60% 0px', + threshold: 0.1 + } + ); - headings.forEach((el) => observer?.observe(el)); - }, 50); // 50ms delay to ensure DOM is updated + headings.forEach((el) => observer?.observe(el)); + }); + }); return () => { - clearTimeout(timeoutId); + cancelAnimationFrame(rafId1); + cancelAnimationFrame(rafId2); if (observer) { observer.disconnect(); } }; - }, [pathname]); + }, [contentKey]); useEffect(() => { if (!activeId || !listRef.current) {