const http = require('http'); const fs = require('fs'); const path = require('path'); const POSTS_DIR = path.join(__dirname, 'content/posts'); const PORT = 3002; const STATUS_CLASSES = ['stable', 'corrupted', 'experimental']; function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function slugify(title) { return String(title) .toLowerCase() .replace(/[äöü]/g, m => ({ ä: 'ae', ö: 'oe', ü: 'ue' }[m])) .replace(/ß/g, 'ss') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') || 'projekt'; } function uniqueSlug(base) { let slug = base; let i = 2; while (fs.existsSync(path.join(POSTS_DIR, slug + '.md'))) { slug = base + '-' + i; i++; } return slug; } // ── FRONTMATTER PARSEN / SCHREIBEN ────────────────────────────── function parsePost(filename) { const raw = fs.readFileSync(path.join(POSTS_DIR, filename), 'utf8'); const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); const fm = { title: '', date: '', status_class: 'stable', status_label: '', thumb: '' }; let body = raw; if (match) { const fmBlock = match[1]; body = match[2].replace(/^\n+/, ''); fmBlock.split('\n').forEach(line => { const idx = line.indexOf(':'); if (idx === -1) return; const key = line.slice(0, idx).trim(); let val = line.slice(idx + 1).trim(); val = val.replace(/^['"]|['"]$/g, ''); if (key in fm) fm[key] = val; }); } return { filename, ...fm, body }; } function serializePost(entry) { const q = v => `'${String(v).replace(/'/g, "''")}'`; return `--- title: ${q(entry.title)} date: ${entry.date} status_class: ${q(entry.status_class)} status_label: ${q(entry.status_label)} thumb: ${q(entry.thumb)} --- ${entry.body.trim()} `; } function readAllPosts() { return fs.readdirSync(POSTS_DIR) .filter(f => f.endsWith('.md')) .map(parsePost) .sort((a, b) => (a.date < b.date ? 1 : -1)); } // ── HTML ───────────────────────────────────────────────────────── function html(posts) { const statusOptions = STATUS_CLASSES.map(c => ``).join(''); const rows = posts.map(p => ` ${esc(p.title)} ${esc(p.date)} ${esc(p.status_label)} ${esc(p.filename)} `).join(''); const json = JSON.stringify(posts).replace(/ PROJEKT-ARCHIV // ADMIN

PROJEKT-ARCHIV

// LOKALES ADMIN-PANEL · localhost:${PORT} · schreibt content/posts/*.md

+ NEUES PROJEKT

PROJEKTE

${posts.length} EINTRÄGE
${rows}
TITELDATUMSTATUSDATEI
`; } // ── SERVER ─────────────────────────────────────────────────────── const server = http.createServer((req, res) => { const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); }; const body = (cb) => { let b = ''; req.on('data', c => b += c); req.on('end', () => cb(b)); }; if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(html(readAllPosts())); } else if (req.method === 'POST' && req.url === '/save') { body(b => { try { const { isNew, entry } = JSON.parse(b); if (!entry.title || !entry.status_label) throw new Error('Titel und Status-Label sind Pflicht'); let filename = entry.filename; if (isNew) { filename = uniqueSlug(slugify(filename || entry.title)) + '.md'; } else if (!filename.endsWith('.md')) { filename += '.md'; } fs.writeFileSync(path.join(POSTS_DIR, filename), serializePost({ ...entry, filename }), 'utf8'); json(200, { ok: true, filename }); } catch (e) { json(400, { ok: false, error: e.message }); } }); } else if (req.method === 'POST' && req.url === '/delete') { body(b => { try { const { filename } = JSON.parse(b); const target = path.join(POSTS_DIR, filename); if (!target.startsWith(POSTS_DIR) || !filename.endsWith('.md')) { throw new Error('Ungültiger Dateiname'); } fs.unlinkSync(target); json(200, { ok: true }); } catch (e) { json(400, { ok: false, error: e.message }); } }); } else { res.writeHead(404); res.end('Not found'); } }); server.listen(PORT, '127.0.0.1', () => { console.log(`\n PROJEKT-ARCHIV ADMIN → http://localhost:${PORT}\n`); });