#!/usr/bin/env node /** * 밍글 스튜디오 DevLog 빌드 스크립트 * 마크다운 파일을 HTML로 변환하여 DevLog 페이지 생성 * * 사용법: node build-blog.js * * 디렉토리 구조: * blog/posts/{slug}/ * ko.md ← 마크다운 * images/ ← 글별 이미지 * * 출력: * devlog/{slug}.html ← 글 페이지 * devlog/index.json ← 목록용 메타데이터 */ const fs = require('fs'); const path = require('path'); const matter = require('gray-matter'); const { marked } = require('marked'); const POSTS_DIR = path.join(__dirname, 'blog', 'posts'); const SITE_URL = 'https://minglestudio.co.kr'; // marked 설정 marked.use({ renderer: { image(token) { const src = token.href; const alt = token.text || ''; const title = token.title ? ` title="${token.title}"` : ''; return `
${alt}
${alt}
`; } } }); function build() { if (!fs.existsSync(POSTS_DIR)) { console.log('blog/posts/ 디렉토리가 없습니다.'); return; } const slugs = fs.readdirSync(POSTS_DIR).filter(f => fs.statSync(path.join(POSTS_DIR, f)).isDirectory() ); if (slugs.length === 0) { console.log('빌드할 글이 없습니다.'); return; } const allPosts = []; for (const slug of slugs) { const postDir = path.join(POSTS_DIR, slug); const mdFile = path.join(postDir, 'ko.md'); if (!fs.existsSync(mdFile)) { console.warn(`[SKIP] ${slug}/ko.md 없음`); continue; } const { data, content } = matter(fs.readFileSync(mdFile, 'utf-8')); const title = data.title || slug; const description = data.description || ''; const date = data.date || ''; const thumbnail = data.thumbnail || ''; const category = data.category || ''; // 이미지 복사 const imgSrcDir = path.join(postDir, 'images'); if (fs.existsSync(imgSrcDir)) { copyDirSync(imgSrcDir, path.join(__dirname, 'devlog', slug, 'images')); } // HTML 생성 const htmlContent = marked(content); const postHtml = buildPostPage({ slug, title, description, date, thumbnail, category, htmlContent, mdContent: content }); const outDir = path.join(__dirname, 'devlog'); fs.mkdirSync(path.join(outDir, slug), { recursive: true }); fs.writeFileSync(path.join(outDir, `${slug}.html`), postHtml, 'utf-8'); console.log(`[OK] devlog/${slug}.html`); allPosts.push({ slug, date, category, thumbnail, title, description }); } // 날짜 내림차순 정렬 allPosts.sort((a, b) => (b.date || '').localeCompare(a.date || '')); // index.json 생성 const indexPath = path.join(__dirname, 'devlog', 'index.json'); fs.writeFileSync(indexPath, JSON.stringify(allPosts, null, 2), 'utf-8'); console.log(`[OK] devlog/index.json (${allPosts.length}건)`); } // FAQ 구조화 데이터 추출 function extractFAQ(mdContent) { const faq = []; const lines = mdContent.split('\n'); let i = 0; while (i < lines.length) { const qMatch = lines[i].match(/^\*\*Q\.\s*(.+?)\*\*$/); if (qMatch) { const question = qMatch[1]; const answerLines = []; i++; while (i < lines.length && !lines[i].match(/^\*\*Q\./) && !lines[i].match(/^---/) && !lines[i].match(/^## /)) { if (lines[i].trim()) answerLines.push(lines[i].trim()); i++; } if (answerLines.length > 0) { faq.push({ question, answer: answerLines.join(' ') }); } } else { i++; } } return faq; } function buildPostPage({ slug, title, description, date, thumbnail, category, htmlContent, mdContent }) { const pageTitle = `${title} - 밍글 스튜디오 DevLog`; const pageUrl = `${SITE_URL}/devlog/${slug}`; const thumbUrl = thumbnail ? `${SITE_URL}/blog/posts/${slug}/${thumbnail}` : `${SITE_URL}/images/logo/mingle-OG.png`; const y = date ? new Date(date + 'T00:00:00').getFullYear() : ''; const m = date ? new Date(date + 'T00:00:00').getMonth() + 1 : ''; const d = date ? new Date(date + 'T00:00:00').getDate() : ''; const dateFormatted = date ? `${y}년 ${m}월 ${d}일` : ''; return ` ${escapeHtml(pageTitle)} ${(() => { const faq = extractFAQ(mdContent || ''); if (faq.length === 0) return ''; return ` `; })()} 본문 바로가기
← 목록으로 ${category ? `${escapeHtml(category)}` : ''}

${escapeHtml(title)}

${htmlContent}
`; } function escapeHtml(str) { return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function copyDirSync(src, dest) { fs.mkdirSync(dest, { recursive: true }); for (const entry of fs.readdirSync(src)) { const srcPath = path.join(src, entry); const destPath = path.join(dest, entry); if (fs.statSync(srcPath).isDirectory()) { copyDirSync(srcPath, destPath); } else { fs.copyFileSync(srcPath, destPath); } } } build();