#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); const EXPORT_PATH = path.join(ROOT, '_wp-export.json'); const POSTS_DIR = path.join(ROOT, 'src/content/posts'); const PAGES_DIR = path.join(ROOT, 'src/content/pages'); const data = JSON.parse(fs.readFileSync(EXPORT_PATH, 'utf8')); fs.mkdirSync(POSTS_DIR, { recursive: true }); fs.mkdirSync(PAGES_DIR, { recursive: true }); const decodeEntities = (s) => s .replace(/ /g, ' ') .replace(/«/g, '«') .replace(/»/g, '»') .replace(/—/g, '—') .replace(/–/g, '–') .replace(/…/g, '…') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&'); const htmlToMd = (html) => { if (!html) return ''; let s = html; // images s = s.replace(/]*?src=["']([^"']+)["'][^>]*?(?:alt=["']([^"']*)["'])?[^>]*?\/?>/gi, (_, src, alt) => `![${alt || ''}](${src})`); // links s = s.replace(/]*?href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => `[${text.replace(/<[^>]+>/g,'').trim()}](${href})`); // bold s = s.replace(/<(strong|b)\b[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**'); // italic s = s.replace(/<(em|i)\b[^>]*>([\s\S]*?)<\/\1>/gi, '*$2*'); // blockquote s = s.replace(/]*>([\s\S]*?)<\/blockquote>/gi, (_, inner) => inner.trim().split(/\n+/).map(l => '> ' + l.trim()).join('\n')); // lists s = s.replace(/]*>([\s\S]*?)<\/li>/gi, '- $1\n'); s = s.replace(/<\/?(ul|ol)[^>]*>/gi, '\n'); // paragraphs s = s.replace(/]*>/gi, '\n\n'); s = s.replace(/<\/p>/gi, '\n\n'); // br s = s.replace(//gi, '\n'); // strip remaining span/div etc. s = s.replace(/<\/?(?:span|div|font|center)[^>]*>/gi, ''); // any remaining tags — drop s = s.replace(/<[^>]+>/g, ''); // entities s = decodeEntities(s); // collapse 3+ blank lines s = s.replace(/\n{3,}/g, '\n\n'); return s.trim(); }; const yamlEscape = (s) => { if (s == null) return '""'; const str = String(s); if (/^[\w\-.,!?:; À-￿()«»—–"']+$/.test(str) && !/^[-?:&*!|>%@`]/.test(str)) { return JSON.stringify(str); } return JSON.stringify(str); }; const fmtFrontmatter = (obj) => { const lines = ['---']; for (const [k, v] of Object.entries(obj)) { if (v === undefined || v === null) continue; if (Array.isArray(v)) { if (v.length === 0) continue; lines.push(`${k}:`); v.forEach(item => lines.push(` - ${yamlEscape(item)}`)); } else if (typeof v === 'object') { lines.push(`${k}:`); Object.entries(v).forEach(([kk, vv]) => lines.push(` ${kk}: ${yamlEscape(vv)}`)); } else { lines.push(`${k}: ${yamlEscape(v)}`); } } lines.push('---'); return lines.join('\n'); }; const authorsMap = Object.fromEntries(data.authors.map(a => [a.id, a.display_name || a.login])); // Posts let postCount = 0; for (const p of data.posts) { const fm = { title: p.title, pubDate: p.date.replace(' ', 'T') + '+03:00', updatedDate: p.modified && p.modified !== p.date ? p.modified.replace(' ','T') + '+03:00' : undefined, slug: p.slug, legacyId: p.id, author: authorsMap[p.author_id] || 'admin', categories: p.categories.map(c => c.name), categorySlugs: p.categories.map(c => c.slug), tags: p.tags.map(t => t.name), description: (htmlToMd(p.excerpt) || htmlToMd(p.content_html)).slice(0, 200).replace(/\s+/g,' ').trim(), }; const body = htmlToMd(p.content_html); const out = `${fmtFrontmatter(fm)}\n\n${body}\n`; const safeName = p.slug.replace(/[^a-z0-9а-я\-]/gi, '-').slice(0, 80) || `post-${p.id}`; fs.writeFileSync(path.join(POSTS_DIR, `${safeName}.md`), out, 'utf8'); postCount++; } // Pages let pageCount = 0; for (const pg of data.pages) { const fm = { title: pg.title, slug: pg.slug, legacyId: pg.id, menuOrder: pg.menu_order, pubDate: pg.date.replace(' ', 'T') + '+03:00', updatedDate: pg.modified !== pg.date ? pg.modified.replace(' ','T') + '+03:00' : undefined, }; const body = htmlToMd(pg.content_html); const out = `${fmtFrontmatter(fm)}\n\n${body}\n`; const safeName = pg.slug.replace(/[^a-z0-9а-я\-]/gi, '-').slice(0, 80) || `page-${pg.id}`; fs.writeFileSync(path.join(PAGES_DIR, `${safeName}.md`), out, 'utf8'); pageCount++; } // Categories dump (для построения /category// страниц и фидов) const catsOut = data.categories.map(c => ({ name: c.name, slug: c.slug, description: c.description, count: c.count })); fs.writeFileSync(path.join(ROOT, 'src/content/_categories.json'), JSON.stringify(catsOut, null, 2), 'utf8'); console.log(`Migrated ${postCount} posts → ${POSTS_DIR}`); console.log(`Migrated ${pageCount} pages → ${PAGES_DIR}`); console.log(`Categories: ${catsOut.length} → src/content/_categories.json`);