/** * Wikilink Transformation Plugin * * Transforms Obsidian wikilink syntax to standard markdown links at build time. * Handles: * - [[Page Title]] → [Page Title](/articles/page-title) * - [[Page|Display Text]] → [Display Text](/articles/page) * - ![[image.jpg]] → ![image.jpg](/img/image.jpg) * - ![[image.jpg|alt text]] → ![alt text](/img/image.jpg) * * Runs during content:file:beforeParse hook for early transformation */ export default defineNitroPlugin((nitroApp) => { nitroApp.hooks.hook('content:file:beforeParse' as any, (file: any) => { // Only process markdown files in articles collection if (file._id && file._id.endsWith('.md') && file._id.includes('/articles/')) { if (file.body) { file.body = transformWikilinks(file.body); file.body = transformImageEmbeds(file.body); } } }); }); /** * Transform Obsidian wikilinks to markdown links * Patterns: * - [[P0 - Page Title]] * - [[Page Title|Display Text]] * - [[P1 - Long Title|Short]] */ function transformWikilinks(content: string): string { // Pattern: [[P0/P1/P2 - Page Title]] or [[Page Title]] with optional |Display Text const wikilinkPattern = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g; return content.replace(wikilinkPattern, (match, pageName, displayText) => { // Clean the page name: strip P0/P1/P2 prefix const cleanName = pageName.replace(/^P[012]\s*-\s*/i, '').trim(); // Generate slug from clean name const slug = titleToSlug(cleanName); // Use display text if provided, otherwise use clean name const linkText = displayText ? displayText.trim() : cleanName; // Generate markdown link with /articles/ prefix return `[${linkText}](/articles/${slug})`; }); } /** * Transform Obsidian image embeds to markdown image syntax * Patterns: * - ![[image.jpg]] * - ![[image.jpg|alt text]] */ function transformImageEmbeds(content: string): string { // Pattern: ![[filename]] or ![[filename|alt text]] const imagePattern = /!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g; return content.replace(imagePattern, (match, filename, altText) => { // Use alt text if provided, otherwise use filename (without extension) const alt = altText?.trim() || filename.replace(/\.(jpg|jpeg|png|gif|svg|webp)$/i, ''); // Generate markdown image syntax with /img/ prefix return `![${alt}](/img/${filename})`; }); } /** * Convert a page title to a URL slug * Rules: * 1. Convert to lowercase * 2. Remove quotation marks * 3. Replace & with "and" * 4. Remove other special characters (keep alphanumeric and hyphens) * 5. Replace spaces with hyphens * 6. Collapse multiple hyphens to single hyphen * 7. Trim hyphens from start/end */ function titleToSlug(title: string): string { return title .toLowerCase() .replace(/['"]/g, '') // Remove quotes .replace(/&/g, 'and') // & → and .replace(/[^a-z0-9\s-]/g, '') // Remove special characters .replace(/\s+/g, '-') // Spaces → hyphens .replace(/-+/g, '-') // Collapse multiple hyphens .replace(/^-|-$/g, ''); // Trim hyphens from edges }