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:
2025-06-21 22:12:12 +02:00
parent 1cc864e4f0
commit 69e6336d5c
5 changed files with 556 additions and 148 deletions

View File

@@ -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, {