import { readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises'; import path from 'node:path'; import matter from 'gray-matter'; const SITE_DIR = process.env.SITE_DIR || '/site'; const CONTENT = path.join(SITE_DIR, 'content'); // Pfad-Sicherheit: relativer Pfad innerhalb content/, nur .md. export function safeRel(rel) { if (!rel || typeof rel !== 'string') throw new Error('Pfad fehlt'); const norm = path.normalize(rel).split(path.sep).join('/'); if (norm.startsWith('..') || norm.startsWith('/') || norm.includes('../')) { throw new Error('Ungültiger Pfad'); } if (!norm.endsWith('.md')) throw new Error('Nur .md erlaubt'); return norm; } async function walk(dir) { const out = []; for (const e of await readdir(dir, { withFileTypes: true })) { const full = path.join(dir, e.name); if (e.isDirectory()) out.push(...(await walk(full))); else if (e.name.endsWith('.md')) out.push(full); } return out; } // Beitrag (archiv/
/.md) | Library-Seite (library/.md) // | Rubrik (_index.md) | Seite (sonst). function classify(rel) { const base = path.basename(rel); const parts = rel.split('/'); if (base === '_index.md') { const section = parts.length >= 2 ? parts[parts.length - 2] : 'home'; return { kind: 'rubrik', section }; } if (parts[0] === 'archiv' && parts.length === 3) { return { kind: 'beitrag', section: parts[1] }; } if (parts[0] === 'library') { return { kind: 'biblio', section: 'library' }; } return { kind: 'seite', section: null }; } // authors-Frontmatter zu Array normalisieren (String oder Array erlaubt). export function normAuthors(a) { if (Array.isArray(a)) return a.map(String).filter(Boolean); if (a) return [String(a)]; return []; } // Hat diese E-Mail Zugriff (steht sie in der authors-Liste)? export function hasAccess(authors, email) { const e = (email || '').toLowerCase(); return normAuthors(authors).some((a) => a.toLowerCase() === e); } // Hugo-URL aus dem relativen Pfad. export function urlFor(rel) { let p = rel.replace(/\.md$/, ''); if (p === '_index') return '/'; p = p.replace(/\/_index$/, ''); return '/' + p + '/'; } export async function listEntries() { const files = await walk(CONTENT); const items = []; for (const full of files) { const rel = path.relative(CONTENT, full).split(path.sep).join('/'); // Autor-Seiten werden über „Profil" verwaltet, nicht im Inhalts-Editor. if (rel === 'authors' || rel.startsWith('authors/')) continue; let data = {}; try { data = matter(await readFile(full, 'utf8')).data || {}; } catch {} items.push({ path: rel, title: data.title || rel, ...classify(rel), color: data.color || null, layout: data.layout || null, draft: !!data.draft, date: data.date ? String(data.date).slice(0, 10) : null, authors: normAuthors(data.authors), url: urlFor(rel), }); } // Beiträge zuerst, dann Library, Seiten, Rubriken; je nach Datum/Titel. const order = { beitrag: 0, biblio: 1, seite: 2, rubrik: 3 }; items.sort((a, b) => (order[a.kind] - order[b.kind]) || (b.date || '').localeCompare(a.date || '') || a.title.localeCompare(b.title)); return items; } export async function readEntry(rel) { rel = safeRel(rel); const { data, content } = matter(await readFile(path.join(CONTENT, rel), 'utf8')); return { path: rel, url: urlFor(rel), frontmatter: data || {}, body: content || '' }; } export async function entryExists(rel) { try { await stat(path.join(CONTENT, safeRel(rel))); return true; } catch { return false; } } export async function writeEntry(rel, frontmatter = {}, body = '') { rel = safeRel(rel); const full = path.join(CONTENT, rel); await mkdir(path.dirname(full), { recursive: true }); // Leere Werte rauswerfen, damit das Frontmatter sauber bleibt. const fm = {}; for (const [k, v] of Object.entries(frontmatter)) { if (v === '' || v === null || v === undefined) continue; if (Array.isArray(v) && v.length === 0) continue; fm[k] = v; } await writeFile(full, matter.stringify(body || '', fm), 'utf8'); return rel; }