Aimpress_site/scripts/sync-blog.mjs
Vadym Samoilenko 67c7ab3289 Initial commit: Aimpress website
React 19 + TypeScript SPA with Vite, mobile responsive fixes,
GitHub Actions CI/CD pipeline for automated deployment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:47:37 +00:00

138 lines
4.2 KiB
JavaScript

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);
});