feat: Social-Bild-Generator auch für Posts (Endpoint, Modal, PostActions-Button)
Deploy / verify (push) Successful in 1m4s
Deploy / deploy (push) Successful in 1m23s

Neuer Endpoint /api/social/post/[slug] mit gleichem Pattern wie für
Termine: Logo, Tag-Pill aus Title/Tags-Keywords (Presse, Demo,
Rückblick, Interview, Studie, Rechtlich, Meinung etc.), Datum,
Headline mit dynamischer Fontsize, Subheadline/Excerpt-Snippet, QR
zum Beitrag, Footer. 3 Formate × 3 Themes wie Kalender.

SocialImageModal in eigene wiederverwendbare Komponente extrahiert
und sowohl im PostActions als auch im CalendarBlock eingebunden.
PostActions akzeptiert socialSlug-Prop und blendet bei vorhandenem
Slug einen Modal-Trigger-Button neben den anderen Aktions-Icons ein.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-06-07 13:14:14 +02:00
parent 287a44ff6b
commit 155d9fb4b0
6 changed files with 555 additions and 141 deletions
+140
View File
@@ -0,0 +1,140 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
let {
open,
title,
slug,
endpointBase,
downloadPrefix = "social",
onclose,
}: {
open: boolean;
title: string;
slug: string;
/** "/api/social/calendar" oder "/api/social/post" */
endpointBase: string;
/** Prefix für Download-Dateinamen, z.B. "termin" oder "beitrag" */
downloadPrefix?: string;
onclose: () => void;
} = $props();
let theme = $state<"wald" | "transparent-onlight" | "transparent-ondark">("wald");
const themeQ = $derived(theme === "wald" ? "" : `&theme=${theme}`);
const ogUrl = $derived(`${endpointBase}/${slug}?format=og${themeQ}`);
const sqUrl = $derived(`${endpointBase}/${slug}?format=square${themeQ}`);
const storyUrl = $derived(`${endpointBase}/${slug}?format=story${themeQ}`);
const previewBg = $derived(
theme === "wald"
? "bg-stein-50"
: theme === "transparent-onlight"
? "bg-[repeating-linear-gradient(45deg,#f5f5f4,#f5f5f4_10px,#e7e5e4_10px,#e7e5e4_20px)]"
: "bg-[repeating-linear-gradient(45deg,#292524,#292524_10px,#1c1917_10px,#1c1917_20px)]",
);
</script>
{#if open}
<div
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 p-4"
role="dialog"
aria-modal="true"
aria-label="Social-Bild-Vorschau"
onclick={onclose}
onkeydown={(e) => { if (e.key === 'Escape') onclose(); }}
tabindex="-1"
>
<div
class="relative max-h-[92vh] w-full max-w-5xl overflow-y-auto rounded-lg bg-white p-5 shadow-2xl"
onclick={(e) => e.stopPropagation()}
role="presentation"
>
<div class="mb-4 flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="text-xs uppercase tracking-wider text-stein-500">Social-Bild</p>
<p class="truncate text-sm font-semibold text-stein-800">{title}</p>
</div>
<button
type="button"
onclick={onclose}
class="shrink-0 text-stein-400 hover:text-stein-700"
aria-label="Schließen"
>
<Icon icon="mdi:close" class="size-5" />
</button>
</div>
<div class="mb-4 rounded-lg border border-stein-200 bg-stein-50 p-3">
<p class="mb-2 text-[11px] font-medium uppercase tracking-wider text-stein-500">Hintergrund</p>
<div class="flex flex-wrap gap-2">
<button
type="button"
onclick={() => (theme = "wald")}
class="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition
{theme === 'wald' ? 'border-wald-500 bg-wald-100 text-wald-800' : 'border-stein-300 bg-white text-stein-700 hover:bg-stein-50'}"
>
<span class="size-3 rounded-full bg-gradient-to-br from-wald-900 to-wald-600"></span>
Wald (Standard)
</button>
<button
type="button"
onclick={() => (theme = "transparent-onlight")}
class="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition
{theme === 'transparent-onlight' ? 'border-wald-500 bg-wald-100 text-wald-800' : 'border-stein-300 bg-white text-stein-700 hover:bg-stein-50'}"
title="Transparenter Hintergrund mit dunklem Text"
>
<span class="size-3 rounded-full border border-stein-300 bg-[repeating-linear-gradient(45deg,#fff,#fff_2px,#e5e5e5_2px,#e5e5e5_4px)]"></span>
Transparent · helle Hintergründe
</button>
<button
type="button"
onclick={() => (theme = "transparent-ondark")}
class="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition
{theme === 'transparent-ondark' ? 'border-wald-500 bg-wald-100 text-wald-800' : 'border-stein-300 bg-white text-stein-700 hover:bg-stein-50'}"
title="Transparenter Hintergrund mit hellem Text"
>
<span class="size-3 rounded-full border border-stein-300 bg-[repeating-linear-gradient(45deg,#1c1917,#1c1917_2px,#44403c_2px,#44403c_4px)]"></span>
Transparent · dunkle Hintergründe
</button>
</div>
{#if theme !== "wald"}
<p class="mt-2 text-[10px] text-stein-500">
Vorschau-Hintergrund zeigt nur, wie der Text aussieht — heruntergeladene PNG hat echten Alpha-Kanal.
</p>
{/if}
</div>
<div class="grid gap-4 sm:grid-cols-3">
<div class="flex flex-col gap-2">
<p class="text-xs font-medium text-stein-600">Quer · 1200 × 630</p>
<img src={ogUrl} alt="Social-Bild Quer" class="w-full rounded-sm border border-stein-200 {previewBg}" loading="eager" />
<a href={ogUrl} download={`${downloadPrefix}-${slug}-og-${theme}.png`} class="btn-outline btn-sm justify-center no-underline!">
<Icon icon="mdi:download" class="size-3.5" />
Quer herunterladen
</a>
</div>
<div class="flex flex-col gap-2">
<p class="text-xs font-medium text-stein-600">Quadrat · 1080 × 1080</p>
<img src={sqUrl} alt="Social-Bild Quadrat" class="mx-auto w-full rounded-sm border border-stein-200 {previewBg}" loading="eager" />
<a href={sqUrl} download={`${downloadPrefix}-${slug}-square-${theme}.png`} class="btn-outline btn-sm justify-center no-underline!">
<Icon icon="mdi:download" class="size-3.5" />
Quadrat herunterladen
</a>
</div>
<div class="flex flex-col gap-2">
<p class="text-xs font-medium text-stein-600">Story · 1080 × 1920</p>
<img src={storyUrl} alt="Social-Bild Story" class="mx-auto w-auto max-h-[280px] rounded-sm border border-stein-200 {previewBg}" loading="eager" />
<a href={storyUrl} download={`${downloadPrefix}-${slug}-story-${theme}.png`} class="btn-outline btn-sm justify-center no-underline!">
<Icon icon="mdi:download" class="size-3.5" />
Story herunterladen
</a>
</div>
</div>
<p class="mt-4 text-[11px] text-stein-400">
Quer: Facebook/Twitter/Link-Preview. Quadrat: Instagram-Feed. Story: Instagram/WhatsApp Story.
</p>
</div>
</div>
{/if}