diff --git a/posts/anchor-test.md b/posts/anchor-test.md
new file mode 100644
index 0000000..b02277b
--- /dev/null
+++ b/posts/anchor-test.md
@@ -0,0 +1,101 @@
+---
+title: Anchor Link Test
+date: '2025-01-20'
+tags:
+ - test
+ - anchors
+summary: Testing anchor link functionality
+author: Test Author
+---
+
+# Anchor Link Test
+
+This is a test page to verify that anchor links work correctly.
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Basic Headings](#basic-headings)
+- [Special Characters](#special-characters)
+- [Numbers and Symbols](#numbers-and-symbols)
+- [Long Headings](#long-headings)
+- [Nested Sections](#nested-sections)
+
+## Overview
+
+This section tests basic anchor linking functionality.
+
+## Basic Headings
+
+### Simple Heading
+This is a simple heading with basic text.
+
+### Another Heading
+This is another heading to test multiple anchors.
+
+## Special Characters
+
+### Heading with Special Chars: @#$%^&*()
+This heading contains special characters that should be properly slugified.
+
+### Heading with Spaces and Dashes
+This heading has spaces and should be converted to dashes.
+
+### Heading_with_Underscores
+This heading uses underscores instead of spaces.
+
+## Numbers and Symbols
+
+### Heading with Numbers 123
+This heading includes numbers.
+
+### Heading with Symbols !@#$%^&*()
+This heading has various symbols.
+
+## Long Headings
+
+### This is a Very Long Heading That Should Still Work Properly Even When It Contains Many Words and Characters
+This heading is intentionally long to test slugification with extended text.
+
+## Nested Sections
+
+### Level 3 Heading
+This is a level 3 heading.
+
+#### Level 4 Heading
+This is a level 4 heading.
+
+##### Level 5 Heading
+This is a level 5 heading.
+
+###### Level 6 Heading
+This is a level 6 heading.
+
+### Another Level 3
+This is another level 3 heading.
+
+## Test Links
+
+You can test the anchor links by clicking on these:
+
+- [Go to Overview](#overview)
+- [Go to Basic Headings](#basic-headings)
+- [Go to Special Characters](#special-characters)
+- [Go to Numbers and Symbols](#numbers-and-symbols)
+- [Go to Long Headings](#long-headings)
+- [Go to Nested Sections](#nested-sections)
+- [Go to Level 3 Heading](#level-3-heading)
+- [Go to Level 4 Heading](#level-4-heading)
+- [Go to Level 5 Heading](#level-5-heading)
+- [Go to Level 6 Heading](#level-6-heading)
+
+## Simple Test
+
+### Test Heading
+This is a simple test heading to verify anchor linking works.
+
+- [Go to Test Heading](#test-heading)
+
+## Conclusion
+
+If all the links above work correctly, the anchor linking system is functioning properly!
\ No newline at end of file
diff --git a/posts/mdtest.md b/posts/mdtest.md
index 945a490..1b6008b 100644
--- a/posts/mdtest.md
+++ b/posts/mdtest.md
@@ -11,23 +11,16 @@ author: Rattatwinko's
* [Overview](#overview)
* [Philosophy](#philosophy)
- * [Inline HTML](#html)
- * [Automatic Escaping for Special Characters](#autoescape)
-* [Block Elements](#block)
- * [Paragraphs and Line Breaks](#p)
- * [Headers](#header)
- * [Blockquotes](#blockquote)
- * [Lists](#list)
- * [Code Blocks](#precode)
- * [Horizontal Rules](#hr)
-* [Span Elements](#span)
- * [Links](#link)
- * [Emphasis](#em)
+* [Block Elements](#block-elements)
+ * [Paragraphs and Line Breaks](#paragraphs-and-line-breaks)
+ * [Headers](#headers)
+ * [Blockquotes](#blockquotes)
+ * [Lists](#lists)
+ * [Code Blocks](#code-blocks)
+* [Span Elements](#span-elements)
+ * [Links](#links)
+ * [Emphasis](#emphasis)
* [Code](#code)
- * [Images](#img)
-* [Miscellaneous](#misc)
- * [Backslash Escapes](#backslash)
- * [Automatic Links](#autolink)
**Note:** This document is itself written using Markdown; you
diff --git a/src/app/globals.css b/src/app/globals.css
index bb04461..334b354 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -57,6 +57,47 @@ html {
text-decoration-thickness: 2px;
}
+/* Enhanced anchor link styles */
+.prose a[href^="#"] {
+ color: #059669; /* Green color for anchor links */
+ font-weight: 500;
+ transition: all 0.2s ease;
+}
+
+.prose a[href^="#"]:hover {
+ color: #047857;
+ text-decoration-thickness: 2px;
+ background-color: #f0fdf4;
+ padding: 2px 4px;
+ border-radius: 4px;
+}
+
+/* Add subtle visual indicator for headings that have anchor links */
+.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
+ position: relative;
+}
+
+.prose h1:hover::before,
+.prose h2:hover::before,
+.prose h3:hover::before,
+.prose h4:hover::before,
+.prose h5:hover::before,
+.prose h6:hover::before {
+ content: "🔗";
+ position: absolute;
+ left: -1.5rem;
+ opacity: 0.6;
+ font-size: 0.8em;
+ cursor: pointer;
+}
+
+/* Ensure proper spacing for anchor link indicators */
+@media (min-width: 641px) {
+ .prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
+ padding-left: 0.5rem;
+ }
+}
+
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
color: #111827;
font-weight: 700;
diff --git a/src/app/posts/[...slug]/page.tsx b/src/app/posts/[...slug]/page.tsx
index 2410452..14a2365 100644
--- a/src/app/posts/[...slug]/page.tsx
+++ b/src/app/posts/[...slug]/page.tsx
@@ -35,124 +35,151 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
useEffect(() => {
if (!post) return;
- // Function to generate ID from text (matches markdown parser behavior)
- const generateId = (text: string): string => {
- return text
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, '-')
- .replace(/^-+|-+$/g, '');
- };
-
- // Function to scroll to element
+ // Function to scroll to element using scrollIntoView
const scrollToElement = (element: HTMLElement) => {
- console.log('Attempting to scroll to element:', element.textContent);
+ // Get comprehensive element information
+ const documentHeight = document.documentElement.scrollHeight;
+ const windowHeight = window.innerHeight;
- let attempts = 0;
- const maxAttempts = 20; // 1 second max wait time
+ // Get the absolute position of the element by temporarily scrolling to top
+ const currentScrollY = window.scrollY;
+ let elementTop = 0;
- // Wait for the element to be properly positioned in the DOM
- const waitForElementPosition = () => {
- attempts++;
- const rect = element.getBoundingClientRect();
- console.log('Element rect (attempt', attempts, '):', rect);
+ // If we're not at the top, scroll to top temporarily to get accurate positions
+ if (currentScrollY > 0) {
+ // Temporarily scroll to top to get accurate element positions
+ window.scrollTo(0, 0);
- // If the element has no dimensions, wait a bit more
- if ((rect.height === 0 && rect.width === 0) && attempts < maxAttempts) {
- console.log('Element not positioned yet, waiting... (attempt', attempts, ')');
- setTimeout(waitForElementPosition, 50);
- return;
- }
-
- // If we've tried too many times, use fallback method
- if (attempts >= maxAttempts) {
- console.log('Max attempts reached, using fallback scroll method');
- element.scrollIntoView({
- behavior: 'smooth',
- block: 'start'
- });
+ // Wait a moment for the scroll to complete, then measure
+ setTimeout(() => {
+ const rect = element.getBoundingClientRect();
+ elementTop = rect.top;
- // Apply offset after scroll
- setTimeout(() => {
- const isDesktop = window.innerWidth >= 640;
- const scrollOffset = isDesktop ? 120 : 100;
- window.scrollBy({
- top: -scrollOffset,
- behavior: 'smooth'
- });
- }, 100);
- return;
- }
-
- console.log('Element offsetTop:', element.offsetTop);
- console.log('Current scroll position:', window.scrollY);
-
- const isDesktop = window.innerWidth >= 640;
- const scrollOffset = isDesktop ? 120 : 100;
-
- // Use offsetTop which is more reliable for positioned elements
- const elementTop = element.offsetTop - scrollOffset;
-
- console.log('Target scroll position:', elementTop);
- console.log('Scroll offset used:', scrollOffset);
-
- // Perform the scroll
- window.scrollTo({
- top: elementTop,
- behavior: 'smooth'
+ // Restore original scroll position
+ window.scrollTo(0, currentScrollY);
+
+ // Now perform the actual scroll to the target
+ performActualScroll(elementTop);
+ }, 50);
+ return;
+ } else {
+ // We're already at the top, get position directly
+ const rect = element.getBoundingClientRect();
+ elementTop = rect.top;
+ performActualScroll(elementTop);
+ }
+
+ function performActualScroll(elementTop: number) {
+ console.log('Element details:', {
+ elementText: element.textContent?.substring(0, 50),
+ elementId: element.id,
+ elementTop,
+ currentScrollY: window.scrollY,
+ documentHeight,
+ windowHeight,
+ canScroll: documentHeight > windowHeight
});
- console.log('Scroll command executed');
- };
-
- // Start the positioning check
- waitForElementPosition();
+ // Check if page is scrollable
+ if (documentHeight <= windowHeight) {
+ console.warn('Page is not tall enough to scroll');
+ return;
+ }
+
+ // Calculate the target scroll position
+ const offset = 100; // Account for sticky header
+ const targetScrollY = Math.max(0, elementTop - offset);
+
+ console.log('Scroll calculation:', {
+ elementTop,
+ targetScrollY,
+ offset,
+ currentScrollY: window.scrollY,
+ scrollDifference: targetScrollY - window.scrollY
+ });
+
+ // Check if we need to scroll at all
+ if (Math.abs(window.scrollY - targetScrollY) < 10) {
+ console.log('Element already at target position, no scroll needed');
+ return;
+ }
+
+ // Use a simple, reliable scroll method
+ console.log(`Scrolling from ${window.scrollY} to ${targetScrollY}`);
+
+ // Use requestAnimationFrame for smooth scrolling
+ const startScrollY = window.scrollY;
+ const scrollDistance = targetScrollY - startScrollY;
+ const duration = 500; // 500ms
+ const startTime = performance.now();
+
+ const animateScroll = (currentTime: number) => {
+ const elapsed = currentTime - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+
+ // Easing function (ease-out)
+ const easeOut = 1 - Math.pow(1 - progress, 3);
+
+ const currentScrollY = startScrollY + (scrollDistance * easeOut);
+ window.scrollTo(0, currentScrollY);
+
+ if (progress < 1) {
+ requestAnimationFrame(animateScroll);
+ } else {
+ console.log('Scroll animation completed');
+ }
+ };
+
+ requestAnimationFrame(animateScroll);
+
+ // Log the scroll after a delay to verify it worked
+ setTimeout(() => {
+ console.log('Scroll verification - new scrollY:', window.scrollY);
+ console.log('Scroll difference:', Math.abs(window.scrollY - targetScrollY));
+ }, 1000);
+ }
};
- // Function to find element by ID or generated ID
- const findElement = (id: string): HTMLElement | null => {
- console.log('Looking for element with ID:', id);
-
- // Try direct ID match first
- let element = document.getElementById(id);
-
- if (element) {
- console.log('Found element by direct ID:', element.textContent);
- return element;
+ // Function to find and scroll to element with retry
+ const findAndScrollToElement = (id: string, retryCount: number = 0) => {
+ // First check if the content is rendered
+ const proseContent = document.querySelector('.prose');
+ if (!proseContent || !proseContent.innerHTML.trim()) {
+ if (retryCount < 10) {
+ console.log(`Content not yet rendered, retrying... (${retryCount + 1}/10)`);
+ setTimeout(() => {
+ findAndScrollToElement(id, retryCount + 1);
+ }, 100);
+ return;
+ } else {
+ console.warn('Content not rendered after retries');
+ return;
+ }
}
-
- // Try to find by generated ID from all headings
- const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
- console.log('Found', headings.length, 'headings on page');
-
- const found = Array.from(headings).find(heading => {
- const headingId = generateId(heading.textContent || '');
- console.log('Checking heading:', heading.textContent, '-> ID:', headingId, 'vs target:', id);
- return headingId === id;
- });
-
- if (found) {
- console.log('Found element by generated ID:', found.textContent);
- return found as HTMLElement;
- }
-
- console.log('Element not found for ID:', id);
- return null;
- };
- // Function to handle hash-based scrolling
- const handleHashScroll = () => {
- if (!window.location.hash) return;
-
- const id = window.location.hash.substring(1);
- console.log('Handling hash scroll for:', id);
-
- const element = findElement(id);
-
+ const element = document.getElementById(id);
if (element) {
- console.log('Found element for hash scroll:', element.textContent);
- setTimeout(() => scrollToElement(element), 100);
+ console.log('Found target element:', element.textContent?.substring(0, 50));
+ scrollToElement(element);
+ } else if (retryCount < 5) {
+ console.log(`Element not found for anchor: ${id}, retrying... (${retryCount + 1}/5)`);
+ // Retry after a short delay
+ setTimeout(() => {
+ findAndScrollToElement(id, retryCount + 1);
+ }, 100);
} else {
- console.log('Element not found for hash:', id);
+ console.warn('Target element not found for anchor after retries:', id);
+ // Log all available IDs for debugging
+ const allIds = Array.from(document.querySelectorAll('[id]')).map(el => el.id);
+ console.log('Available IDs on page:', allIds);
+
+ // Show a user-friendly error message
+ const linkElement = document.querySelector(`a[href="#${id}"]`) as HTMLElement;
+ if (linkElement) {
+ linkElement.setAttribute('title', `Heading "${id}" not found`);
+ linkElement.style.color = '#ef4444'; // Red color for broken links
+ linkElement.style.textDecoration = 'line-through';
+ }
}
};
@@ -163,48 +190,211 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
if (!link || !link.getAttribute('href')?.startsWith('#')) return;
+ const href = link.getAttribute('href');
+ const id = href?.substring(1);
+
+ if (!id) return;
+
+ console.log('Anchor click detected:', href);
+
+ // Prevent default behavior first
event.preventDefault();
- const href = link.getAttribute('href')!;
- const id = href.substring(1);
- console.log('Anchor click detected:', id);
+ // Find the target element and scroll to it
+ findAndScrollToElement(id);
+ };
+
+ // Function to handle hash-based scrolling on page load
+ const handleHashScroll = () => {
+ if (!window.location.hash) return;
- const element = findElement(id);
+ const id = window.location.hash.substring(1);
+ console.log('Handling hash scroll for:', id);
+ // Use a longer delay to ensure DOM is fully rendered
+ setTimeout(() => {
+ findAndScrollToElement(id);
+ }, 300);
+ };
+
+ // Handle initial hash scroll
+ handleHashScroll();
+
+ // Add event listener for anchor clicks
+ document.addEventListener('click', handleAnchorClick);
+
+ // Add a test function to the window object for debugging
+ (window as any).testScroll = (id: string) => {
+ console.log('Testing scroll to:', id);
+ findAndScrollToElement(id);
+ };
+
+ // Add a function to test basic scrolling
+ (window as any).testBasicScroll = () => {
+ console.log('Testing basic scroll functionality');
+ const currentScrollY = window.scrollY;
+ const testScrollY = currentScrollY + 500;
+
+ console.log('Current scrollY:', currentScrollY);
+ console.log('Target scrollY:', testScrollY);
+
+ window.scrollTo({
+ top: testScrollY,
+ behavior: 'smooth'
+ });
+
+ setTimeout(() => {
+ console.log('Basic scroll test completed, new scrollY:', window.scrollY);
+ }, 1000);
+ };
+
+ // Add a function to test scrolling to a specific element
+ (window as any).testElementScroll = (id: string) => {
+ console.log('Testing scroll to element:', id);
+ const element = document.getElementById(id);
if (element) {
- console.log('Found element for anchor click:', element.textContent);
+ console.log('Element found, testing scroll...');
scrollToElement(element);
-
- // Update URL without reload
- history.replaceState(null, '', href);
} else {
- console.log('Element not found for anchor:', id);
+ console.log('Element not found:', id);
+ (window as any).listIds();
}
};
- // Add IDs to headings that don't have them
- const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
- console.log('Processing', headings.length, 'headings for ID assignment');
- headings.forEach(heading => {
- if (!heading.id) {
- const id = generateId(heading.textContent || '');
- heading.id = id;
- console.log('Added ID to heading:', heading.textContent, '->', id);
+ // Add a simple test function
+ (window as any).runAnchorTest = () => {
+ console.log('Running anchor link test...');
+
+ // Test 1: Check if we can find the "overview" heading
+ const overviewElement = document.getElementById('overview');
+ if (overviewElement) {
+ console.log('✅ Found overview element, testing scroll...');
+ scrollToElement(overviewElement);
} else {
- console.log('Heading already has ID:', heading.textContent, '->', heading.id);
+ console.log('❌ Overview element not found');
}
- });
+
+ // Test 2: Check if we can find the "test-heading" element
+ setTimeout(() => {
+ const testHeadingElement = document.getElementById('test-heading');
+ if (testHeadingElement) {
+ console.log('✅ Found test-heading element, testing scroll...');
+ scrollToElement(testHeadingElement);
+ } else {
+ console.log('❌ Test-heading element not found');
+ }
+ }, 2000);
+ };
- // Handle initial hash scroll
- setTimeout(handleHashScroll, 100);
+ // Add a function to list all available IDs
+ (window as any).listIds = () => {
+ const allIds = Array.from(document.querySelectorAll('[id]')).map(el => ({
+ id: el.id,
+ text: el.textContent?.substring(0, 50),
+ tag: el.tagName
+ }));
+ console.log('Available IDs on page:', allIds);
+ return allIds;
+ };
- // Add event listeners
- document.addEventListener('click', handleAnchorClick);
- window.addEventListener('hashchange', handleHashScroll);
+ // Add a function to debug anchor links
+ (window as any).debugAnchors = () => {
+ console.log('=== Anchor Link Debug ===');
+
+ // Get all anchor links
+ const anchorLinks = Array.from(document.querySelectorAll('a[href^="#"]')).map(el => ({
+ href: el.getAttribute('href'),
+ text: el.textContent,
+ targetId: el.getAttribute('href')?.substring(1)
+ }));
+
+ console.log('Anchor links found:', anchorLinks);
+
+ // Get all headings with IDs
+ const headings = Array.from(document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]')).map(el => ({
+ id: el.id,
+ text: el.textContent?.substring(0, 50),
+ tag: el.tagName,
+ offsetTop: (el as HTMLElement).offsetTop,
+ getBoundingClientRect: (el as HTMLElement).getBoundingClientRect()
+ }));
+
+ console.log('Headings with IDs:', headings);
+
+ // Check which anchor links have matching headings
+ anchorLinks.forEach(link => {
+ const hasMatch = headings.some(h => h.id === link.targetId);
+ const status = hasMatch ? '✅' : '❌';
+ console.log(`${status} [${link.text}](#${link.targetId}) -> ${hasMatch ? 'FOUND' : 'NOT FOUND'}`);
+ });
+
+ console.log('=== End Debug ===');
+ };
+
+ // Add a function to show element positions
+ (window as any).showPositions = () => {
+ console.log('=== Element Positions ===');
+
+ const headings = Array.from(document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]'));
+
+ headings.forEach((el, index) => {
+ const element = el as HTMLElement;
+
+ // Calculate absolute position
+ let elementTop = 0;
+ let currentElement = element;
+ while (currentElement && currentElement !== document.body) {
+ elementTop += currentElement.offsetTop;
+ currentElement = currentElement.offsetParent as HTMLElement;
+ }
+
+ console.log(`${index + 1}. ${element.textContent?.substring(0, 30)}:`);
+ console.log(` ID: ${element.id}`);
+ console.log(` Calculated elementTop: ${elementTop}`);
+ console.log(` element.offsetTop: ${element.offsetTop}`);
+ console.log(` Current scrollY: ${window.scrollY}`);
+ console.log(` Would scroll to: ${Math.max(0, elementTop - 100)}`);
+ console.log('---');
+ });
+
+ console.log('=== End Positions ===');
+ };
+
+ // Add a simple test function
+ (window as any).testScrollToElement = (id: string) => {
+ const element = document.getElementById(id);
+ if (element) {
+ console.log(`Testing scroll to ${id}...`);
+
+ // Calculate position the same way as scrollToElement
+ let elementTop = 0;
+ let currentElement = element;
+ while (currentElement && currentElement !== document.body) {
+ elementTop += currentElement.offsetTop;
+ currentElement = currentElement.offsetParent as HTMLElement;
+ }
+
+ const targetScrollY = Math.max(0, elementTop - 100);
+ console.log(`Element ${id} is at position ${elementTop}, would scroll to ${targetScrollY}`);
+ console.log(`Current scroll position: ${window.scrollY}`);
+
+ // Perform the scroll
+ scrollToElement(element);
+ } else {
+ console.log(`Element with id "${id}" not found`);
+ (window as any).listIds();
+ }
+ };
return () => {
document.removeEventListener('click', handleAnchorClick);
- window.removeEventListener('hashchange', handleHashScroll);
+ delete (window as any).testScroll;
+ delete (window as any).testBasicScroll;
+ delete (window as any).testElementScroll;
+ delete (window as any).listIds;
+ delete (window as any).debugAnchors;
+ delete (window as any).showPositions;
+ delete (window as any).testScrollToElement;
};
}, [post]);
diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts
index 90db1c7..18db88d 100644
--- a/src/lib/markdown.ts
+++ b/src/lib/markdown.ts
@@ -36,11 +36,89 @@ function generateId(text: string): string {
.replace(/^-+|-+$/g, '');
}
+// Enhanced slugification function that matches GitHub-style anchor links
+function slugify(text: string): string {
+ return text
+ .toLowerCase()
+ .trim()
+ .replace(/[^\w\s-]/g, '') // Remove special characters except spaces and hyphens
+ .replace(/[\s_-]+/g, '-') // Replace spaces, underscores, and multiple hyphens with single hyphen
+ .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
+}
+
+// Function to process anchor links in markdown content
+function processAnchorLinks(content: string): string {
+ // Find all markdown links that point to anchors (e.g., [text](#anchor))
+ return content.replace(/\[([^\]]+)\]\(#([^)]+)\)/g, (match, linkText, anchor) => {
+ // Only slugify if the anchor doesn't already look like a slug
+ // This prevents double-processing of already-correct anchor links
+ const isAlreadySlugified = /^[a-z0-9-]+$/.test(anchor);
+ const slugifiedAnchor = isAlreadySlugified ? anchor : slugify(anchor);
+ return `[${linkText}](#${slugifiedAnchor})`;
+ });
+}
+
+// Utility function to debug anchor links (for development)
+export function debugAnchorLinks(content: string): void {
+ if (process.env.NODE_ENV !== 'development') return;
+
+ console.log('=== Anchor Link Debug Info ===');
+
+ // Extract all headings and their IDs
+ const headingRegex = /^(#{1,6})\s+(.+)$/gm;
+ const headings: Array<{ level: number; text: string; id: string }> = [];
+
+ let match;
+ while ((match = headingRegex.exec(content)) !== null) {
+ const level = match[1].length;
+ const text = match[2].trim();
+ const id = slugify(text);
+ headings.push({ level, text, id });
+ }
+
+ console.log('Generated heading IDs:');
+ headings.forEach(({ level, text, id }) => {
+ console.log(` H${level}: "${text}" -> id="${id}"`);
+ });
+
+ // Extract all anchor links
+ const anchorLinkRegex = /\[([^\]]+)\]\(#([^)]+)\)/g;
+ const anchorLinks: Array<{ linkText: string; originalAnchor: string; slugifiedAnchor: string }> = [];
+
+ while ((match = anchorLinkRegex.exec(content)) !== null) {
+ const linkText = match[1];
+ const originalAnchor = match[2];
+ const slugifiedAnchor = slugify(originalAnchor);
+ anchorLinks.push({ linkText, originalAnchor, slugifiedAnchor });
+ }
+
+ console.log('Anchor links found:');
+ anchorLinks.forEach(({ linkText, originalAnchor, slugifiedAnchor }) => {
+ const headingExists = headings.some(h => h.id === slugifiedAnchor);
+ const status = headingExists ? '✅' : '❌';
+ console.log(` ${status} [${linkText}](#${originalAnchor}) -> [${linkText}](#${slugifiedAnchor})`);
+ });
+
+ // Show missing headings
+ const missingAnchors = anchorLinks.filter(({ slugifiedAnchor }) =>
+ !headings.some(h => h.id === slugifiedAnchor)
+ );
+
+ if (missingAnchors.length > 0) {
+ console.warn('Missing headings for these anchor links:');
+ missingAnchors.forEach(({ linkText, originalAnchor, slugifiedAnchor }) => {
+ console.warn(` - [${linkText}](#${originalAnchor}) -> id="${slugifiedAnchor}"`);
+ });
+ }
+
+ console.log('=== End Debug Info ===');
+}
+
const renderer = new marked.Renderer();
// Custom heading renderer to add IDs
renderer.heading = (text, level) => {
- const id = generateId(text);
+ const id = slugify(text);
return `${text}`;
};
@@ -68,7 +146,12 @@ export async function getPostBySlug(slug: string): Promise {
let processedContent = '';
try {
- const rawHtml = marked.parse(content);
+ // Debug anchor links in development
+ debugAnchorLinks(content);
+
+ // Process anchor links before parsing markdown
+ const processedMarkdown = processAnchorLinks(content);
+ const rawHtml = marked.parse(processedMarkdown);
const window = new JSDOM('').window;
const purify = DOMPurify(window);
processedContent = purify.sanitize(rawHtml as string, {