cms: dateibasiert + Editor im Decap/Sveltia-Look
- CMS liest/schreibt jetzt die echten content/**/*.md (gray-matter) statt DB: alle bestehenden Beiträge, Seiten und Rubriken erscheinen und sind editierbar. Supabase nur noch für Login. - Admin neu: Collections-Sidebar (Beiträge/Seiten/Rubriken), an OPENBUREAU-Theme angeglichen (Newsreader-Serif, Creme, Terracotta, dunkle Topbar). - Alle Frontmatter-Felder inkl. Farb-Dropdown mit Farbpunkten (Palette aus custom.css), Layout, Tags, summary, cover_image, external, toc, draft. - Markdown-Toolbar: Fett/Kursiv/Unterstrichen/H2/H3/Link/Bild-Upload/Liste/Zitat/Code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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 (library/<section>/<slug>.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] === 'library' && parts.length === 3) {
|
||||
return { kind: 'beitrag', section: parts[1] };
|
||||
}
|
||||
return { kind: 'seite', section: null };
|
||||
}
|
||||
|
||||
// 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('/');
|
||||
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,
|
||||
url: urlFor(rel),
|
||||
});
|
||||
}
|
||||
// Beiträge zuerst, dann Seiten, dann Rubriken; je nach Datum/Titel.
|
||||
const order = { beitrag: 0, seite: 1, rubrik: 2 };
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user