Autor-Seiten: /authors/<slug>/ (rundes Bild, Name, Bio, zentriert) + Byline-Link
- layouts/authors/single.html rendert die Autor-Seite zentriert - Byline in single.html + index.html verlinkt den Autornamen zu /authors/<urlize name>/ (nur wenn die Seite existiert) - CMS-Profil-Speichern schreibt content/authors/<slug>.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 <noreply@anthropic.com>
This commit is contained in:
@@ -397,6 +397,34 @@ a:hover {
|
|||||||
[data-color="amagumo"] { --section-color: var(--palette-amagumo); }
|
[data-color="amagumo"] { --section-color: var(--palette-amagumo); }
|
||||||
[data-color="yuki"] { --section-color: var(--palette-yuki); }
|
[data-color="yuki"] { --section-color: var(--palette-yuki); }
|
||||||
|
|
||||||
|
/* Autor-Seite (/authors/<slug>/) — 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
|
Journal entries — three Republik-style layouts (set in front matter
|
||||||
via `layout: image|icon|text`). Every entry is a full-bleed coloured
|
via `layout: image|icon|text`). Every entry is a full-bleed coloured
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export async function listEntries() {
|
|||||||
const items = [];
|
const items = [];
|
||||||
for (const full of files) {
|
for (const full of files) {
|
||||||
const rel = path.relative(CONTENT, full).split(path.sep).join('/');
|
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 = {};
|
let data = {};
|
||||||
try { data = matter(await readFile(full, 'utf8')).data || {}; } catch {}
|
try { data = matter(await readFile(full, 'utf8')).data || {}; } catch {}
|
||||||
items.push({
|
items.push({
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import { Hono } from 'hono';
|
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 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).
|
// Profile als Hugo-Data-Datei (data/authors.json) + öffentliche Autor-Seite
|
||||||
// So kann das Theme die Autor:innen via site.Data.authors rendern.
|
// (content/authors/<slug>.md), gerendert von layouts/authors/single.html.
|
||||||
const SITE_DIR = process.env.SITE_DIR || '/site';
|
const SITE_DIR = process.env.SITE_DIR || '/site';
|
||||||
const FILE = path.join(SITE_DIR, 'data', 'authors.json');
|
const FILE = path.join(SITE_DIR, 'data', 'authors.json');
|
||||||
|
const AUTHORS_DIR = path.join(SITE_DIR, 'content', 'authors');
|
||||||
|
|
||||||
async function readAll() {
|
async function readAll() {
|
||||||
try { return JSON.parse(await readFile(FILE, 'utf8')); } catch { return {}; }
|
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();
|
const profile = new Hono();
|
||||||
|
|
||||||
@@ -22,11 +30,27 @@ profile.get('/', async (c) => {
|
|||||||
profile.put('/', async (c) => {
|
profile.put('/', async (c) => {
|
||||||
const email = c.get('user')?.email || 'default';
|
const email = c.get('user')?.email || 'default';
|
||||||
const { name, bio, avatar } = await c.req.json();
|
const { name, bio, avatar } = await c.req.json();
|
||||||
|
const slug = slugify(name);
|
||||||
|
|
||||||
const all = await readAll();
|
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 mkdir(path.dirname(FILE), { recursive: true });
|
||||||
await writeFile(FILE, JSON.stringify(all, null, 2) + '\n', 'utf8');
|
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;
|
export default profile;
|
||||||
|
|||||||
@@ -16,9 +16,13 @@
|
|||||||
<p class="single-summary">{{ . }}</p>
|
<p class="single-summary">{{ . }}</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ $author := .Params.author | default site.Params.author.name }}
|
{{ $author := .Params.author | default site.Params.author.name }}
|
||||||
|
{{ $authorPage := cond (ne $author "") (site.GetPage (printf "/authors/%s" (urlize $author))) false }}
|
||||||
{{ if or $author .Date }}
|
{{ if or $author .Date }}
|
||||||
<p class="single-byline">
|
<p class="single-byline">
|
||||||
{{- with $author -}}<span class="byline-author">{{ . }}</span>{{- end -}}
|
{{- with $author -}}
|
||||||
|
{{- if $authorPage -}}<a class="byline-author" href="{{ $authorPage.RelPermalink }}">{{ . }}</a>
|
||||||
|
{{- else -}}<span class="byline-author">{{ . }}</span>{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
{{- if and $author .Date -}}, {{ end -}}
|
{{- if and $author .Date -}}, {{ end -}}
|
||||||
{{- if .Date -}}<time class="byline-date" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "02.01.2006" }}</time>{{- end -}}
|
{{- if .Date -}}<time class="byline-date" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "02.01.2006" }}</time>{{- end -}}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
<article class="author-page">
|
||||||
|
{{ with .Params.avatar }}
|
||||||
|
<img class="author-photo" src="{{ . }}" alt="{{ $.Title }}" loading="eager" />
|
||||||
|
{{ end }}
|
||||||
|
<h1 class="author-name">{{ .Title }}</h1>
|
||||||
|
{{ with .Content }}<div class="author-bio">{{ . }}</div>{{ end }}
|
||||||
|
</article>
|
||||||
|
{{ end }}
|
||||||
+5
-1
@@ -15,6 +15,7 @@
|
|||||||
{{ $section := "" }}
|
{{ $section := "" }}
|
||||||
{{ with .Parent }}{{ $section = path.Base .RelPermalink }}{{ end }}
|
{{ with .Parent }}{{ $section = path.Base .RelPermalink }}{{ end }}
|
||||||
{{ $author := .Params.author | default site.Params.author.name }}
|
{{ $author := .Params.author | default site.Params.author.name }}
|
||||||
|
{{ $authorPage := cond (ne $author "") (site.GetPage (printf "/authors/%s" (urlize $author))) false }}
|
||||||
{{ $cover := .Params.cover_image }}
|
{{ $cover := .Params.cover_image }}
|
||||||
{{/* Layout: explicit `layout:` in front matter, else derive:
|
{{/* Layout: explicit `layout:` in front matter, else derive:
|
||||||
- cover_image present → image
|
- cover_image present → image
|
||||||
@@ -42,7 +43,10 @@
|
|||||||
<p class="journal-summary">{{ . }}</p>
|
<p class="journal-summary">{{ . }}</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<p class="journal-byline">
|
<p class="journal-byline">
|
||||||
{{- with $author -}}<span class="journal-author">{{ . }}</span>{{- end -}}
|
{{- with $author -}}
|
||||||
|
{{- if $authorPage -}}<a class="journal-author" href="{{ $authorPage.RelPermalink }}">{{ . }}</a>
|
||||||
|
{{- else -}}<span class="journal-author">{{ . }}</span>{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
{{- if and $author .Date -}}, {{ end -}}
|
{{- if and $author .Date -}}, {{ end -}}
|
||||||
<time class="journal-date" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "02.01.2006" }}</time>
|
<time class="journal-date" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "02.01.2006" }}</time>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user