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:
2026-05-31 11:51:49 +02:00
parent e7d820b83c
commit e2d986356c
12 changed files with 526 additions and 377 deletions
+29
View File
@@ -0,0 +1,29 @@
import { Hono } from 'hono';
import { listEntries, readEntry, writeEntry, entryExists } from '../files.js';
// Dateibasiert: liest/schreibt die echten .md unter content/.
const content = new Hono();
// Liste aller Einträge (Beiträge, Seiten, Rubriken).
content.get('/', async (c) => {
try { return c.json(await listEntries()); }
catch (e) { return c.json({ error: String(e.message || e) }, 500); }
});
// Einen Eintrag lesen: /api/content/entry?path=library/software/stack.md
content.get('/entry', async (c) => {
try { return c.json(await readEntry(c.req.query('path'))); }
catch (e) { return c.json({ error: String(e.message || e) }, 400); }
});
// Anlegen oder überschreiben.
content.put('/entry', async (c) => {
const { path: rel, frontmatter, body } = await c.req.json();
try {
const created = !(await entryExists(rel));
const saved = await writeEntry(rel, frontmatter, body);
return c.json({ ok: true, path: saved, created });
} catch (e) { return c.json({ error: String(e.message || e) }, 400); }
});
export default content;
-50
View File
@@ -1,50 +0,0 @@
import { Hono } from 'hono';
import { supabase } from '../supabase.js';
// Minimales CRUD, damit der Publish/Preview-Flow ohne UI testbar ist.
// (Auth-Middleware kommt im nächsten Meilenstein.)
const posts = new Hono();
posts.get('/', async (c) => {
const { data, error } = await supabase
.from('posts')
.select('*')
.order('date', { ascending: false });
if (error) return c.json({ error: error.message }, 500);
return c.json(data);
});
posts.get('/:id', async (c) => {
const { data, error } = await supabase
.from('posts')
.select('*')
.eq('id', c.req.param('id'))
.single();
if (error) return c.json({ error: error.message }, 404);
return c.json(data);
});
posts.post('/', async (c) => {
const body = await c.req.json();
const { data, error } = await supabase
.from('posts')
.insert({ ...body, status: 'draft' })
.select()
.single();
if (error) return c.json({ error: error.message }, 400);
return c.json(data, 201);
});
posts.put('/:id', async (c) => {
const body = await c.req.json();
const { data, error } = await supabase
.from('posts')
.update({ ...body, updated_at: new Date().toISOString() })
.eq('id', c.req.param('id'))
.select()
.single();
if (error) return c.json({ error: error.message }, 400);
return c.json(data);
});
export default posts;
+7 -14
View File
@@ -1,24 +1,17 @@
import { Hono } from 'hono';
import { supabase } from '../supabase.js';
import { writePostFile } from '../content.js';
import { urlFor, safeRel } from '../files.js';
import { hugoBuild } from '../hugo.js';
// Echte Hugo-Vorschau: Post als draft:true in content/ schreiben und mit
// --buildDrafts nach preview/ bauen. Der Live-Build (public/) lässt den
// Draft weiterhin aus.
// Echte Hugo-Vorschau: ganze Site mit --buildDrafts nach preview/ bauen und die
// URL des Eintrags zurückgeben (so erscheinen auch draft:true-Einträge).
const preview = new Hono();
preview.post('/:id', async (c) => {
const id = c.req.param('id');
const { data: post, error } = await supabase
.from('posts').select('*').eq('id', id).single();
if (error || !post) return c.json({ error: 'Post nicht gefunden' }, 404);
preview.post('/', async (c) => {
const { path: rel } = await c.req.json();
try {
await writePostFile(post, { draft: true });
const safe = safeRel(rel);
const build = await hugoBuild({ dest: 'preview', drafts: true });
const url = `/_preview/library/${post.section}/${post.slug}/`;
return c.json({ ok: true, url, hugo: build.stdout });
return c.json({ ok: true, url: `/_preview${urlFor(safe)}`, hugo: build.stdout });
} catch (e) {
return c.json({ error: String(e.message || e) }, 500);
}
+7 -28
View File
@@ -1,38 +1,17 @@
import { Hono } from 'hono';
import { supabase } from '../supabase.js';
import { writePostFile } from '../content.js';
import { urlFor, safeRel } from '../files.js';
import { hugoBuild, gitCommit } from '../hugo.js';
// Publizieren: Post als live (draft:false) nach content/ schreiben, public/
// neu bauen, Status setzen und optional nach Gitea committen.
// Publizieren: public/ neu bauen (ohne Drafts) → live. Optional git-commit.
const publish = new Hono();
publish.post('/:id', async (c) => {
const id = c.req.param('id');
const { data: post, error } = await supabase
.from('posts').select('*').eq('id', id).single();
if (error || !post) return c.json({ error: 'Post nicht gefunden' }, 404);
publish.post('/', async (c) => {
const { path: rel } = await c.req.json();
try {
const file = await writePostFile(post, { draft: false });
const safe = safeRel(rel);
const build = await hugoBuild({ dest: 'public', drafts: false });
const { error: upErr } = await supabase
.from('posts')
.update({ status: 'published', published_at: new Date().toISOString() })
.eq('id', id);
if (upErr) return c.json({ error: upErr.message }, 500);
const git = await gitCommit(`cms: publish ${post.section}/${post.slug}`)
.catch((e) => ({ error: String(e.message || e) }));
return c.json({
ok: true,
path: file.replace(process.env.SITE_DIR || '/site', '').replace(/^\//, ''),
url: `/library/${post.section}/${post.slug}/`,
git,
hugo: build.stdout,
});
const git = await gitCommit(`cms: publish ${safe}`).catch((e) => ({ error: String(e.message || e) }));
return c.json({ ok: true, url: urlFor(safe), git, hugo: build.stdout });
} catch (e) {
return c.json({ error: String(e.message || e) }, 500);
}