#!/usr/bin/env node import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, ".."); const INTERNAL_PREFIXES = ["/", "#"]; const EXTERNAL_DOMAINS = ["http://", "https://", "mailto:", "tel:"]; const TSX_FILES = []; function walkDir(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") { walkDir(full); } else if (entry.name.endsWith(".tsx")) { TSX_FILES.push(full); } } } walkDir(path.join(ROOT, "components")); let totalConverted = 0; for (const file of TSX_FILES) { // Skip Header.tsx - already uses Link if (file.endsWith("Header.tsx")) continue; let content = fs.readFileSync(file, "utf8"); let changed = false; // Convert to for internal links // And add target/rel for external links content = content.replace(/]*?)>/g, (match, attrs) => { const hrefMatch = attrs.match(/href="([^"]*)"/); if (!hrefMatch) return match; const href = hrefMatch[1]; const isExternal = EXTERNAL_DOMAINS.some(d => href.startsWith(d)); const isInternal = INTERNAL_PREFIXES.some(p => href.startsWith(p)) && !isExternal; if (isInternal) { changed = true; totalConverted++; // Remove Webflow-specific attrs let cleanAttrs = attrs .replace(/data-w-id="[^"]*"/g, "") .replace(/data-wf[^=]*="[^"]*"/g, "") .replace(/aria-current="page"/g, "") .replace(/\s+/g, " ") .trim(); return ``; } if (isExternal && !attrs.includes("target=")) { changed = true; totalConverted++; return ``; } return match; }); // Convert closing to where we converted opening tag // Simple approach: replace all in files that had changes // Actually we need to be smarter - only replace that correspond to // Let's do it line by line if (changed) { // Count Link opens vs closes to fix const lines = content.split("\n"); let linkDepth = 0; const newLines = []; for (const line of lines) { let processed = line; // Count Link opens in this line const linkOpens = (processed.match(/]*\/>/g) || []).length; linkDepth += linkOpens - selfClosing; // Replace with when inside a Link context if (linkDepth > 0 && processed.includes("")) { const closeTags = (processed.match(/<\/a>/g) || []).length; for (let i = 0; i < closeTags && linkDepth > 0; i++) { processed = processed.replace("", ""); linkDepth--; } } newLines.push(processed); } content = newLines.join("\n"); // Add Link import if (!content.includes('import Link from "next/link"') && !content.includes("import Link from 'next/link'")) { if (content.includes('import Image from "next/image"')) { content = content.replace( 'import Image from "next/image"', 'import Image from "next/image"\nimport Link from "next/link"' ); } else { const firstLine = content.indexOf("\n"); content = content.slice(0, firstLine + 1) + 'import Link from "next/link"\n' + content.slice(firstLine + 1); } } fs.writeFileSync(file, content, "utf8"); console.log(`Updated: ${path.relative(ROOT, file)}`); } } console.log(`\nTotal links converted: ${totalConverted}`);