From 6a3b1b5b5e92a40233c961f4b035419a742b7efd Mon Sep 17 00:00:00 2001 From: karim Date: Sun, 31 May 2026 13:07:26 +0200 Subject: [PATCH] Autor-Seiten: /authors// (rundes Bild, Name, Bio, zentriert) + Byline-Link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - layouts/authors/single.html rendert die Autor-Seite zentriert - Byline in single.html + index.html verlinkt den Autornamen zu /authors// (nur wenn die Seite existiert) - CMS-Profil-Speichern schreibt content/authors/.md (aus Name/Bio/Avatar) + data/authors.json und baut public neu → Seite & Links sofort live - Autor-Seiten aus dem Inhalts-Editor ausgeblendet (über „Profil" verwaltet) - custom.css: .author-page / -photo / -name / -bio + Byline-Link-Stil Co-Authored-By: Claude Opus 4.8 --- assets/css/custom.css | 28 ++++++++++++++++++++++++++++ cms/api/src/files.js | 2 ++ cms/api/src/routes/profile.js | 34 +++++++++++++++++++++++++++++----- layouts/_default/single.html | 6 +++++- layouts/authors/single.html | 9 +++++++++ layouts/index.html | 6 +++++- 6 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 layouts/authors/single.html diff --git a/assets/css/custom.css b/assets/css/custom.css index 7c50966..5835b10 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -397,6 +397,34 @@ a:hover { [data-color="amagumo"] { --section-color: var(--palette-amagumo); } [data-color="yuki"] { --section-color: var(--palette-yuki); } +/* Autor-Seite (/authors//) — zentriert: rundes Bild, Name, Kurztext */ +.author-page { + max-width: 54ch; + margin: 0 auto; + padding: var(--spacing-xl) var(--spacing-md); + text-align: center; +} +.author-photo { + width: 168px; + height: 168px; + border-radius: 50%; + object-fit: cover; + display: block; + margin: 0 auto var(--spacing-md); +} +.author-name { + font-family: var(--font-family-serif); + margin: 0 0 var(--spacing-sm); +} +.author-bio { + color: var(--color-text-muted); + line-height: 1.7; +} +.author-bio p { margin: 0 0 1em; } +.byline-author { color: inherit; text-decoration: none; border-bottom: 1px solid currentColor; } +.byline-author:hover { color: var(--accent); } +.journal-author { color: inherit; } + /* ------------------------------------------------------------------------ Journal entries — three Republik-style layouts (set in front matter via `layout: image|icon|text`). Every entry is a full-bleed coloured diff --git a/cms/api/src/files.js b/cms/api/src/files.js index 4635fff..9bcc176 100644 --- a/cms/api/src/files.js +++ b/cms/api/src/files.js @@ -66,6 +66,8 @@ export async function listEntries() { 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({ diff --git a/cms/api/src/routes/profile.js b/cms/api/src/routes/profile.js index 0053d44..114528b 100644 --- a/cms/api/src/routes/profile.js +++ b/cms/api/src/routes/profile.js @@ -1,15 +1,23 @@ import { Hono } from 'hono'; -import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { readFile, writeFile, mkdir, stat } from 'node:fs/promises'; import path from 'node:path'; +import matter from 'gray-matter'; +import { hugoBuild } from '../hugo.js'; -// Profile als Hugo-Data-Datei: data/authors.json (Map E-Mail → Profil). -// So kann das Theme die Autor:innen via site.Data.authors rendern. +// Profile als Hugo-Data-Datei (data/authors.json) + öffentliche Autor-Seite +// (content/authors/.md), gerendert von layouts/authors/single.html. const SITE_DIR = process.env.SITE_DIR || '/site'; const FILE = path.join(SITE_DIR, 'data', 'authors.json'); +const AUTHORS_DIR = path.join(SITE_DIR, 'content', 'authors'); async function readAll() { try { return JSON.parse(await readFile(FILE, 'utf8')); } catch { return {}; } } +// Muss zu Hugos `urlize` passen (Byline-Link). +function slugify(s) { + return String(s || '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); +} +async function exists(p) { try { await stat(p); return true; } catch { return false; } } const profile = new Hono(); @@ -22,11 +30,27 @@ profile.get('/', async (c) => { profile.put('/', async (c) => { const email = c.get('user')?.email || 'default'; const { name, bio, avatar } = await c.req.json(); + const slug = slugify(name); + const all = await readAll(); - all[email] = { name: name || '', bio: bio || '', avatar: avatar || '' }; + all[email] = { name: name || '', bio: bio || '', avatar: avatar || '', slug }; await mkdir(path.dirname(FILE), { recursive: true }); await writeFile(FILE, JSON.stringify(all, null, 2) + '\n', 'utf8'); - return c.json({ ok: true }); + + // Öffentliche Autor-Seite schreiben (nur mit Name). + if (slug) { + await mkdir(AUTHORS_DIR, { recursive: true }); + const idx = path.join(AUTHORS_DIR, '_index.md'); + if (!(await exists(idx))) { + await writeFile(idx, matter.stringify('', { title: 'Autor:innen' }), 'utf8'); + } + const page = matter.stringify(bio || '', { title: name, avatar: avatar || '' }); + await writeFile(path.join(AUTHORS_DIR, `${slug}.md`), page, 'utf8'); + // Live bauen, damit die Seite + Byline-Links sofort funktionieren. + await hugoBuild({ dest: 'public', drafts: false }).catch(() => {}); + } + + return c.json({ ok: true, slug }); }); export default profile; diff --git a/layouts/_default/single.html b/layouts/_default/single.html index 48e8ca2..73ccaea 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -16,9 +16,13 @@

{{ . }}

{{ end }} {{ $author := .Params.author | default site.Params.author.name }} + {{ $authorPage := cond (ne $author "") (site.GetPage (printf "/authors/%s" (urlize $author))) false }} {{ if or $author .Date }} diff --git a/layouts/authors/single.html b/layouts/authors/single.html new file mode 100644 index 0000000..57414f1 --- /dev/null +++ b/layouts/authors/single.html @@ -0,0 +1,9 @@ +{{ define "main" }} +
+ {{ with .Params.avatar }} + {{ $.Title }} + {{ end }} +

{{ .Title }}

+ {{ with .Content }}
{{ . }}
{{ end }} +
+{{ end }} diff --git a/layouts/index.html b/layouts/index.html index 8a946e4..921ce27 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -15,6 +15,7 @@ {{ $section := "" }} {{ with .Parent }}{{ $section = path.Base .RelPermalink }}{{ end }} {{ $author := .Params.author | default site.Params.author.name }} + {{ $authorPage := cond (ne $author "") (site.GetPage (printf "/authors/%s" (urlize $author))) false }} {{ $cover := .Params.cover_image }} {{/* Layout: explicit `layout:` in front matter, else derive: - cover_image present → image @@ -42,7 +43,10 @@

{{ . }}

{{ end }}