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>
138 lines
4.2 KiB
JavaScript
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);
|
|
});
|