Refactor anchor link handling and enhance scrolling functionality. Update markdown processing to support GitHub-style anchor links and improve user experience with smooth scrolling to headings. Add debugging utilities for anchor links in development.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 `<h${level} id="${id}">${text}</h${level}>`;
|
||||
};
|
||||
|
||||
@@ -68,7 +146,12 @@ export async function getPostBySlug(slug: string): Promise<Post> {
|
||||
|
||||
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, {
|
||||
|
||||
Reference in New Issue
Block a user