import { readdir, readFile, copyFile, mkdir, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; const INPUT_DIR = '/Volumes/SSD/Projects/Aimpress/LinkedIn-autopost/output'; const OUTPUT_DIR = join(import.meta.dirname, '..', 'public', 'blog'); function toSlug(title) { return title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') .slice(0, 80); } function makeExcerpt(body, len = 150) { if (body.length <= len) return body; const trimmed = body.slice(0, len); const lastSpace = trimmed.lastIndexOf(' '); return (lastSpace > 0 ? trimmed.slice(0, lastSpace) : trimmed) + '...'; } function parseArticle(text, date) { const lines = text.replace(/\r\n/g, '\n').split('\n'); const title = lines[0].trim(); const slug = toSlug(title); // Find separator line const sepIdx = lines.indexOf('---'); // Body is everything between the title blank line and the separator (or end) const bodyStart = lines[1]?.trim() === '' ? 2 : 1; const bodyEnd = sepIdx > -1 ? sepIdx : lines.length; const body = lines.slice(bodyStart, bodyEnd).join('\n').trim(); // Parse source and hashtags after separator let sourceTitle = ''; let sourceUrl = ''; let hashtags = []; if (sepIdx > -1) { const afterSep = lines.slice(sepIdx + 1); for (const line of afterSep) { const trimmed = line.trim(); if (trimmed.startsWith('Source:')) { sourceTitle = trimmed.replace('Source:', '').trim(); } else if (trimmed.startsWith('http')) { sourceUrl = trimmed; } else if (trimmed.startsWith('#')) { hashtags = trimmed.split(/\s+/).map(t => t.replace(/^#/, '')).filter(Boolean); } } } const excerpt = makeExcerpt(body); return { slug, title, date, body, excerpt, sourceTitle, sourceUrl, hashtags }; } async function main() { // Clear output await rm(join(OUTPUT_DIR, 'posts'), { recursive: true, force: true }); await rm(join(OUTPUT_DIR, 'images'), { recursive: true, force: true }); await mkdir(join(OUTPUT_DIR, 'posts'), { recursive: true }); await mkdir(join(OUTPUT_DIR, 'images'), { recursive: true }); const entries = await readdir(INPUT_DIR, { withFileTypes: true }); const dateDirs = entries .filter(e => e.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(e.name)) .map(e => e.name) .sort(); const allPosts = []; const usedSlugs = new Set(); for (const dateDir of dateDirs) { const dirPath = join(INPUT_DIR, dateDir); const files = await readdir(dirPath); const articles = files.filter(f => f.startsWith('article_') && f.endsWith('.txt')); for (const articleFile of articles) { const timestamp = articleFile.match(/article_(\d+)\.txt/)?.[1]; if (!timestamp) continue; const coverFile = `cover_${timestamp}.png`; const hasCover = files.includes(coverFile); const text = await readFile(join(dirPath, articleFile), 'utf-8'); const post = parseArticle(text, dateDir); // Skip duplicate slugs (multiple versions of same article) if (usedSlugs.has(post.slug)) continue; usedSlugs.add(post.slug); // Write full post JSON const fullPost = { ...post, coverImage: hasCover ? `/blog/images/${post.slug}.png` : '', }; await writeFile( join(OUTPUT_DIR, 'posts', `${post.slug}.json`), JSON.stringify(fullPost, null, 2) ); // Copy cover image if (hasCover) { await copyFile( join(dirPath, coverFile), join(OUTPUT_DIR, 'images', `${post.slug}.png`) ); } allPosts.push({ slug: post.slug, title: post.title, date: post.date, excerpt: post.excerpt, coverImage: fullPost.coverImage, hashtags: post.hashtags, }); } } // Sort newest first allPosts.sort((a, b) => b.date.localeCompare(a.date)); await writeFile( join(OUTPUT_DIR, 'posts.json'), JSON.stringify(allPosts, null, 2) ); console.log(`Synced ${allPosts.length} blog posts to ${OUTPUT_DIR}`); } main().catch(err => { console.error(err); process.exit(1); });