From e2f9c9d556df925571c5b524ee5567024bc89fe2 Mon Sep 17 00:00:00 2001 From: gbanyan Date: Thu, 20 Nov 2025 23:29:17 +0800 Subject: [PATCH] Fix TOC showing headings from previous article MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TOC was displaying sections from previously viewed articles when navigating between posts. This happened because the DOM query for headings ran before Next.js finished updating the page content. Changes to components/post-toc.tsx: - Clear items and activeId immediately when pathname changes - Add 50ms delay before querying DOM for new headings - Properly handle IntersectionObserver cleanup with timeout This ensures the TOC always shows the correct headings for the current article, not the previous one. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- components/post-toc.tsx | 74 ++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/components/post-toc.tsx b/components/post-toc.tsx index c944fef..2aeb254 100644 --- a/components/post-toc.tsx +++ b/components/post-toc.tsx @@ -19,39 +19,53 @@ export function PostToc({ onLinkClick }: { onLinkClick?: () => void }) { const pathname = usePathname(); useEffect(() => { - 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); + // Clear items immediately when pathname changes + setItems([]); + setActiveId(null); - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - const id = (entry.target as HTMLElement).id; - if (id) { - setActiveId(id); + let observer: IntersectionObserver | null = null; + + // 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); + + 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 + + return () => { + clearTimeout(timeoutId); + if (observer) { + observer.disconnect(); } - ); - - headings.forEach((el) => observer.observe(el)); - - return () => observer.disconnect(); + }; }, [pathname]); useEffect(() => {