Enhance component functionality and styling across the project
Deploy to Firebase Hosting / deploy (push) Failing after 1m46s
Deploy to Firebase Hosting / deploy (push) Failing after 1m46s
- Updated .gitignore to include raw image files for better asset management. - Added optional icon and color properties to Tag and TagItem interfaces for improved tag customization. - Enhanced BlogOverview and PostCard components to support new tag features, including icon and color display. - Refined Card component to accept target and rel attributes for better link handling. - Improved layout responsiveness in ContentRows and PostCard components. - Updated OrganisationsBlock to include a new CTA link feature for adding organizations. - Enhanced QuoteBlock styling for better visual emphasis on quotes. - Refactored image handling in various components to ensure consistent display and performance. - Introduced utility functions for resolving body content and filtering hidden posts in blog utilities.
This commit is contained in:
@@ -28,4 +28,5 @@ pnpm-debug.log*
|
|||||||
.firebase/
|
.firebase/
|
||||||
.history/
|
.history/
|
||||||
public/images/transformed/
|
public/images/transformed/
|
||||||
|
public/images/raw/
|
||||||
public/pagefind
|
public/pagefind
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
interface TagEntry {
|
interface TagEntry {
|
||||||
_slug?: string;
|
_slug?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -62,6 +64,8 @@
|
|||||||
href={tagHref(slug)}
|
href={tagHref(slug)}
|
||||||
variant={activeTag === slug ? "inactive" : "green"}
|
variant={activeTag === slug ? "inactive" : "green"}
|
||||||
active={activeTag === slug}
|
active={activeTag === slug}
|
||||||
|
icon={tag.icon?.trim() || null}
|
||||||
|
customColor={activeTag === slug ? null : tag.color?.trim() || null}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +79,7 @@
|
|||||||
activeTag={activeTag}
|
activeTag={activeTag}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="py-2 grid grid-cols-1 gap-4">
|
<div class="py-2 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{#each posts as post}
|
{#each posts as post}
|
||||||
<PostCard
|
<PostCard
|
||||||
post={post}
|
post={post}
|
||||||
|
|||||||
@@ -4,15 +4,25 @@
|
|||||||
let {
|
let {
|
||||||
children,
|
children,
|
||||||
href,
|
href,
|
||||||
|
target,
|
||||||
|
rel,
|
||||||
class: className = "",
|
class: className = "",
|
||||||
}: { children?: Snippet; href?: string; class?: string } = $props();
|
}: {
|
||||||
|
children?: Snippet;
|
||||||
|
href?: string;
|
||||||
|
target?: string;
|
||||||
|
rel?: string;
|
||||||
|
class?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const base = "relative flex flex-col gap-2 rounded-lg border border-stein-200 bg-white p-4 shadow-sm";
|
const base = "relative flex flex-col gap-2 rounded-lg border border-stein-200 bg-white p-4 shadow-sm min-w-0 overflow-hidden";
|
||||||
const interactive = href ? "hover:border-stein-300 hover:shadow-md transition-all no-underline text-inherit" : "";
|
const interactive = $derived(
|
||||||
|
href ? "hover:border-stein-300 hover:shadow-md transition-all no-underline text-inherit" : "",
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if href}
|
{#if href}
|
||||||
<a {href} class="{base} {interactive} {className}">
|
<a {href} {target} {rel} class="{base} {interactive} {className}">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
>
|
>
|
||||||
{#each row.content as item}
|
{#each row.content as item}
|
||||||
{#if isResolvedBlock(item)}
|
{#if isResolvedBlock(item)}
|
||||||
|
{@const rowBlockType = blockType(item)}
|
||||||
{#snippet blockContent()}
|
{#snippet blockContent()}
|
||||||
{#if blockType(item) === "markdown"}
|
{#if blockType(item) === "markdown"}
|
||||||
<MarkdownBlock block={item as MarkdownBlockData} />
|
<MarkdownBlock block={item as MarkdownBlockData} />
|
||||||
@@ -132,8 +133,12 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
{#if isBlockLayoutBreakout((item as { layout?: BlockLayout }).layout)}
|
{#if isBlockLayoutBreakout((item as { layout?: BlockLayout }).layout)}
|
||||||
<div class="col-span-12">
|
<div class="col-span-12">
|
||||||
<div class="component-breakout--{blockType(item)} {getSpaceBottomClass((item as { layout?: BlockLayout }).layout)} w-screen relative left-1/2 -translate-x-1/2 max-w-none [background:var(--color-container-breakout)]">
|
<div
|
||||||
<div class="container-custom py-4">
|
class="component-breakout--{rowBlockType} {getSpaceBottomClass((item as { layout?: BlockLayout }).layout)} w-screen relative left-1/2 -translate-x-1/2 max-w-none {rowBlockType === 'quote'
|
||||||
|
? 'bg-wald-50'
|
||||||
|
: '[background:var(--color-container-breakout)]'}"
|
||||||
|
>
|
||||||
|
<div class="container-custom py-8">
|
||||||
{@render blockContent()}
|
{@render blockContent()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,23 +15,29 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const resolvedImg = $derived(post._resolvedImageUrl);
|
const resolvedImg = $derived(post._resolvedImageUrl);
|
||||||
const bgStyle = $derived(resolvedImg ? `background-image: url(${resolvedImg})` : "");
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
class="transition-all flex flex-col text-slate-950 no-underline overflow-hidden border border-gray-200 bg-white relative min-h-[200px]"
|
class="transition-all flex flex-col text-slate-950 no-underline overflow-hidden border border-gray-200 bg-white"
|
||||||
>
|
>
|
||||||
{#if resolvedImg}
|
<div class="aspect-3/2 w-full overflow-hidden bg-gray-100">
|
||||||
<div
|
{#if resolvedImg}
|
||||||
class="absolute inset-0 bg-cover bg-center opacity-10"
|
<img
|
||||||
style={bgStyle}
|
src={resolvedImg}
|
||||||
aria-hidden="true"
|
alt={post.headline ?? ""}
|
||||||
></div>
|
class="w-full h-full object-cover"
|
||||||
{/if}
|
loading="lazy"
|
||||||
<div
|
/>
|
||||||
class="relative flex-1 flex flex-col justify-start p-4"
|
{:else}
|
||||||
>
|
<div class="w-full h-full flex items-center justify-center text-gray-300">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="size-12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col justify-start p-4">
|
||||||
<PostMeta
|
<PostMeta
|
||||||
date={post.created ?? post.date ?? null}
|
date={post.created ?? post.date ?? null}
|
||||||
tags={post.postTag ?? []}
|
tags={post.postTag ?? []}
|
||||||
@@ -42,12 +48,12 @@
|
|||||||
{post.headline ?? post.linkName ?? post.slug ?? post._slug}
|
{post.headline ?? post.linkName ?? post.slug ?? post._slug}
|
||||||
</h5>
|
</h5>
|
||||||
{#if post.subheadline}
|
{#if post.subheadline}
|
||||||
<div class="text-xs line-clamp-2 mb-1 ">
|
<div class="text-xs line-clamp-2 mb-1">
|
||||||
{post.subheadline}
|
{post.subheadline}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if post.excerpt}
|
{#if post.excerpt}
|
||||||
<p class="text-xs font-light line-clamp-3 ">
|
<p class="text-xs font-light line-clamp-3">
|
||||||
{post.excerpt}
|
{post.excerpt}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+94
-15
@@ -1,4 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import "../lib/iconify-offline";
|
||||||
|
import Icon from "@iconify/svelte";
|
||||||
|
|
||||||
type Variant = "white" | "green" | "blue" | "inactive" | "date";
|
type Variant = "white" | "green" | "blue" | "inactive" | "date";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -7,36 +10,112 @@
|
|||||||
href = null,
|
href = null,
|
||||||
active = false,
|
active = false,
|
||||||
onclick = null,
|
onclick = null,
|
||||||
|
icon = null,
|
||||||
|
customColor = null,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
href?: string | null;
|
href?: string | null;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
onclick?: (() => void) | null;
|
onclick?: (() => void) | null;
|
||||||
|
/** Iconify id (z. B. mdi:tag) */
|
||||||
|
icon?: string | null;
|
||||||
|
/** CSS-Farbe für Pill-Hintergrund (z. B. #2d5a27) */
|
||||||
|
customColor?: string | null;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
/** Grobe Helligkeit für lesbare Textfarbe auf customColor-Hintergrund. */
|
||||||
|
function isLightBackground(cssColor: string): boolean {
|
||||||
|
const s = cssColor.trim();
|
||||||
|
const hex6 = /^#([0-9a-f]{6})$/i.exec(s);
|
||||||
|
const hex3 = /^#([0-9a-f]{3})$/i.exec(s);
|
||||||
|
let r = 200;
|
||||||
|
let g = 200;
|
||||||
|
let b = 200;
|
||||||
|
if (hex6) {
|
||||||
|
const n = parseInt(hex6[1], 16);
|
||||||
|
r = (n >> 16) & 255;
|
||||||
|
g = (n >> 8) & 255;
|
||||||
|
b = n & 255;
|
||||||
|
} else if (hex3) {
|
||||||
|
const h = hex3[1];
|
||||||
|
r = parseInt(h[0] + h[0], 16);
|
||||||
|
g = parseInt(h[1] + h[1], 16);
|
||||||
|
b = parseInt(h[2] + h[2], 16);
|
||||||
|
} else if (s.startsWith("rgb")) {
|
||||||
|
const m = s.match(/\d+/g);
|
||||||
|
if (m && m.length >= 3) {
|
||||||
|
r = Number(m[0]);
|
||||||
|
g = Number(m[1]);
|
||||||
|
b = Number(m[2]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||||
|
return lum > 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
const baseClass =
|
const baseClass =
|
||||||
"inline-flex shrink-0 whitespace-nowrap py-1 px-2 text-[.6rem] rounded-full transition-all ring-1 ring-white/50 no-underline!";
|
"inline-flex shrink-0 items-center gap-1 whitespace-nowrap border-0 py-1 px-2.5 text-[.6rem] rounded-full transition-[opacity,filter] no-underline shadow-none outline-none ring-0";
|
||||||
const variantClass = $derived.by(() => {
|
const variantClass = $derived.by(() => {
|
||||||
if (variant === "white") return "bg-gradient-to-b from-white to-gray-100 text-black! ring-black/5!";
|
if (customColor?.trim()) {
|
||||||
if (variant === "green") return "bg-gradient-to-b from-green-300 to-green-400 text-black! ";
|
return "";
|
||||||
if (variant === "blue") return "bg-gradient-to-b from-teal-300 to-teal-400 text-black! ";
|
}
|
||||||
if (variant === "inactive") return "bg-gradient-to-t from-gray-200 to-gray-300 text-gray-500! shadow-none ring-black/15!";
|
if (variant === "white")
|
||||||
if (variant === "date") return "bg-gradient-to-b from-green-50 to-green-100 text-green-700! ring-green-200!";
|
return "bg-stein-100 text-stein-800";
|
||||||
return "bg-gray-200 text-gray-500";
|
if (variant === "green") return "bg-wald-100 text-wald-800";
|
||||||
|
if (variant === "blue") return "bg-sky-100/90 text-sky-900";
|
||||||
|
if (variant === "inactive") return "bg-stein-200 text-stein-500";
|
||||||
|
if (variant === "date") return "bg-wald-50 text-wald-800";
|
||||||
|
return "bg-stein-200 text-stein-500";
|
||||||
});
|
});
|
||||||
const shadowClass = $derived.by(() => {
|
const pillStyle = $derived.by(() => {
|
||||||
if (!href && !onclick) return "shadow-none";
|
const c = customColor?.trim();
|
||||||
if (variant === "date") return "shadow-[0_0_6px_rgba(0,0,0,0.3)]";
|
if (!c) return "";
|
||||||
return "shadow-[0_2px_6px_rgba(0,0,0,0.2)]";
|
const light = isLightBackground(c);
|
||||||
|
const fg = light ? "#111827" : "#fafafa";
|
||||||
|
return `background-color: ${c}; color: ${fg};`;
|
||||||
});
|
});
|
||||||
const classes = $derived.by(() => `${baseClass} ${variantClass} ${shadowClass} ${active ? "pointer-events-none" : ""}`);
|
const interactiveClass = $derived.by(() =>
|
||||||
|
href || onclick ? "hover:brightness-[0.97] active:brightness-[0.94]" : "",
|
||||||
|
);
|
||||||
|
const classes = $derived.by(
|
||||||
|
() =>
|
||||||
|
`${baseClass} ${variantClass} ${interactiveClass} ${active ? "pointer-events-none opacity-80" : ""}`.trim(),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if href}
|
{#if href}
|
||||||
<a href={href} class={classes} aria-current={active ? "true" : undefined}>{label}</a>
|
<a
|
||||||
|
href={href}
|
||||||
|
class={classes}
|
||||||
|
style={pillStyle || undefined}
|
||||||
|
aria-current={active ? "true" : undefined}
|
||||||
|
>
|
||||||
|
{#if icon}
|
||||||
|
<Icon {icon} class="size-3 shrink-0 opacity-90" aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
{:else if onclick}
|
{:else if onclick}
|
||||||
<button type="button" class={classes} aria-current={active ? "true" : undefined} onclick={onclick}>{label}</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class={classes}
|
||||||
|
style={pillStyle || undefined}
|
||||||
|
aria-current={active ? "true" : undefined}
|
||||||
|
onclick={onclick}
|
||||||
|
>
|
||||||
|
{#if icon}
|
||||||
|
<Icon {icon} class="size-3 shrink-0 opacity-90" aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<span class={classes}>{label}</span>
|
<span class={classes} style={pillStyle || undefined}>
|
||||||
|
{#if icon}
|
||||||
|
<Icon {icon} class="size-3 shrink-0 opacity-90" aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Tag from "./Tag.svelte";
|
import Tag from "./Tag.svelte";
|
||||||
|
|
||||||
type TagItem = string | { name?: string; _slug?: string };
|
type TagItem =
|
||||||
|
| string
|
||||||
|
| { name?: string; _slug?: string; icon?: string; color?: string };
|
||||||
type Variant = "white" | "green" | "blue";
|
type Variant = "white" | "green" | "blue";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -16,12 +18,30 @@
|
|||||||
|
|
||||||
function displayName(t: TagItem): string {
|
function displayName(t: TagItem): string {
|
||||||
if (typeof t === "string") return t;
|
if (typeof t === "string") return t;
|
||||||
return (t as { name?: string; _slug?: string })?.name ?? (t as { _slug?: string })?._slug ?? "";
|
return (
|
||||||
|
(t as { name?: string; _slug?: string })?.name ??
|
||||||
|
(t as { _slug?: string })?._slug ??
|
||||||
|
""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function slug(t: TagItem): string {
|
function slug(t: TagItem): string {
|
||||||
if (typeof t === "string") return t;
|
if (typeof t === "string") return t;
|
||||||
return (t as { _slug?: string })?._slug ?? (t as { name?: string })?.name ?? "";
|
return (
|
||||||
|
(t as { _slug?: string })?._slug ?? (t as { name?: string })?.name ?? ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagIcon(t: TagItem): string | null {
|
||||||
|
if (typeof t === "string") return null;
|
||||||
|
const i = (t as { icon?: string }).icon?.trim();
|
||||||
|
return i || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagColor(t: TagItem): string | null {
|
||||||
|
if (typeof t === "string") return null;
|
||||||
|
const c = (t as { color?: string }).color?.trim();
|
||||||
|
return c || null;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -32,6 +52,8 @@
|
|||||||
label={name}
|
label={name}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
href={tagBase ? `${tagBase}/${slug(t)}` : null}
|
href={tagBase ? `${tagBase}/${slug(t)}` : null}
|
||||||
|
icon={tagIcon(t)}
|
||||||
|
customColor={tagColor(t)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -13,25 +13,41 @@
|
|||||||
: "https://opnform.pm86.de"
|
: "https://opnform.pm86.de"
|
||||||
).replace(/\/$/, ""),
|
).replace(/\/$/, ""),
|
||||||
);
|
);
|
||||||
const iframeSrc = $derived(
|
const iframeSrc = $derived(formId ? `${baseUrl}/forms/${formId}` : "");
|
||||||
formId ? `${baseUrl}/forms/${formId}` : "",
|
const iframeTitle = $derived(block.iframeTitle?.trim() || "Kontaktformular");
|
||||||
);
|
|
||||||
const iframeTitle = $derived(
|
|
||||||
block.iframeTitle?.trim() || "Kontaktformular",
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
let iframeEl = $state<HTMLIFrameElement | null>(null);
|
||||||
* Wie im OpnForm-Embed: initEmbed erst wenn iframe.min.js fertig geladen ist
|
|
||||||
* (entspricht <script … onload="initEmbed('…', { autoResize: true })">).
|
|
||||||
* Zusätzlich min-height, weil das Widget oft ~150px inline setzt, bevor autoResize greift.
|
|
||||||
*/
|
|
||||||
const iframeStyle =
|
|
||||||
"border: none; width: 100%; min-height: min(88vh, 48rem) !important;";
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!formId) return;
|
if (!formId || !iframeEl) return;
|
||||||
|
|
||||||
|
const el = iframeEl;
|
||||||
|
// Höhe ausschließlich per DOM verwalten – nie über Sveltes style= Prop.
|
||||||
|
el.style.height = "600px";
|
||||||
|
el.style.transition = "height 0.25s ease";
|
||||||
|
// Verhindert native Browser-Scrollbar im Iframe, egal ob Höhe stimmt oder nicht.
|
||||||
|
el.setAttribute("scrolling", "no");
|
||||||
|
|
||||||
const scriptUrl = `${baseUrl}/widgets/iframe.min.js`;
|
const scriptUrl = `${baseUrl}/widgets/iframe.min.js`;
|
||||||
|
|
||||||
|
function handleMessage(event: MessageEvent) {
|
||||||
|
if (event.source !== el.contentWindow) return;
|
||||||
|
const d = event.data;
|
||||||
|
let h: number | undefined;
|
||||||
|
if (typeof d === "number") {
|
||||||
|
h = d;
|
||||||
|
} else if (d && typeof d === "object") {
|
||||||
|
h = d.height ?? d.frameHeight ?? d.iFrameHeight;
|
||||||
|
} else if (typeof d === "string" && d.startsWith("[iFrameSizer]")) {
|
||||||
|
const parts = d.split(":");
|
||||||
|
h = parts.length >= 2 ? parseFloat(parts[1]) : undefined;
|
||||||
|
}
|
||||||
|
if (typeof h === "number" && h > 50) {
|
||||||
|
el.style.height = `${Math.ceil(h)}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("message", handleMessage);
|
||||||
|
|
||||||
function runInit() {
|
function runInit() {
|
||||||
const w = window as Window & {
|
const w = window as Window & {
|
||||||
initEmbed?: (id: string, opts: { autoResize?: boolean }) => void;
|
initEmbed?: (id: string, opts: { autoResize?: boolean }) => void;
|
||||||
@@ -50,27 +66,31 @@
|
|||||||
} else {
|
} else {
|
||||||
existing.addEventListener("load", runInit, { once: true });
|
existing.addEventListener("load", runInit, { once: true });
|
||||||
}
|
}
|
||||||
return;
|
} else {
|
||||||
|
const s = document.createElement("script");
|
||||||
|
s.type = "text/javascript";
|
||||||
|
s.async = true;
|
||||||
|
s.src = scriptUrl;
|
||||||
|
s.dataset.opnformEmbedJs = baseUrl;
|
||||||
|
s.onload = () => runInit();
|
||||||
|
document.body.appendChild(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
const s = document.createElement("script");
|
return () => {
|
||||||
s.type = "text/javascript";
|
window.removeEventListener("message", handleMessage);
|
||||||
s.async = true;
|
};
|
||||||
s.src = scriptUrl;
|
|
||||||
s.dataset.opnformEmbedJs = baseUrl;
|
|
||||||
s.onload = () => runInit();
|
|
||||||
document.body.appendChild(s);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="{layoutClasses} content-opnform w-full min-w-0 overflow-visible -mx-6"
|
class="{layoutClasses} content-opnform w-[calc(100%+2rem)] max-w-none -mx-4 min-w-0 overflow-visible border border-stein-200 lg:rounded-lg px-0 py-3 md:w-[calc(100%+3rem)] md:-mx-6 md:px-4 md:py-4"
|
||||||
data-block-type="opnform"
|
data-block-type="opnform"
|
||||||
data-block-slug={block._slug}
|
data-block-slug={block._slug}
|
||||||
>
|
>
|
||||||
{#if iframeSrc}
|
{#if iframeSrc}
|
||||||
<iframe
|
<iframe
|
||||||
style={iframeStyle}
|
bind:this={iframeEl}
|
||||||
|
style="border: none; width: 100%;"
|
||||||
id={formId}
|
id={formId}
|
||||||
title={iframeTitle}
|
title={iframeTitle}
|
||||||
src={iframeSrc}
|
src={iframeSrc}
|
||||||
|
|||||||
@@ -34,6 +34,30 @@
|
|||||||
? (marked.parse(block.addOrganisationMarkdown) as string)
|
? (marked.parse(block.addOrganisationMarkdown) as string)
|
||||||
: ""
|
: ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const addOrganisationCtaLink = $derived.by(() => {
|
||||||
|
const raw = block.addOrganisationLink;
|
||||||
|
if (!raw || typeof raw !== "object") return undefined;
|
||||||
|
const url = typeof raw.url === "string" ? raw.url.trim() : "";
|
||||||
|
if (!url) return undefined;
|
||||||
|
const linkName =
|
||||||
|
typeof raw.linkName === "string" && raw.linkName.trim() !== ""
|
||||||
|
? raw.linkName.trim()
|
||||||
|
: url;
|
||||||
|
return { url, linkName, newTab: Boolean(raw.newTab) };
|
||||||
|
});
|
||||||
|
|
||||||
|
const showAddOrganisationCard = $derived(
|
||||||
|
Boolean(
|
||||||
|
block.addOrganisationTitle?.trim() ||
|
||||||
|
block.addOrganisationMarkdown?.trim() ||
|
||||||
|
addOrganisationCtaLink
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const addOrganisationCardClass = $derived(
|
||||||
|
`${compact ? "border-dashed border-wald-300 bg-wald-50! p-3! gap-1.5!" : "border-dashed border-wald-300 bg-wald-50!"}${addOrganisationCtaLink ? " cursor-pointer" : ""}`,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={layoutClasses} data-block-type="organisations" data-block-slug={block._slug}>
|
<div class={layoutClasses} data-block-type="organisations" data-block-slug={block._slug}>
|
||||||
@@ -103,12 +127,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0 flex-1 flex flex-col gap-0.5">
|
<div class="min-w-0 flex-1 flex flex-col gap-0.5">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<h5 class="text-sm font-semibold text-stein-900 leading-snug">{o.name ?? ""}</h5>
|
||||||
<h5 class="text-sm font-semibold text-stein-900 leading-snug">{o.name ?? ""}</h5>
|
|
||||||
{#if o.badge && typeof o.badge === "object" && o.badge.label}
|
|
||||||
<Badge color={o.badge.color}>{o.badge.label}</Badge>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if o.description}
|
{#if o.description}
|
||||||
<p class="text-xs text-stein-600 line-clamp-2">{o.description}</p>
|
<p class="text-xs text-stein-600 line-clamp-2">{o.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -165,7 +184,7 @@
|
|||||||
<Badge color={o.badge.color}>{o.badge.label}</Badge>
|
<Badge color={o.badge.color}>{o.badge.label}</Badge>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<h5 class="text-base font-semibold text-stein-900 leading-snug">{o.name ?? ""}</h5>
|
<h5 class="min-w-0 text-sm font-semibold leading-snug text-stein-900 truncate">{o.name ?? ""}</h5>
|
||||||
{#if o.description}
|
{#if o.description}
|
||||||
<p class="text-sm text-stein-600">{o.description}</p>
|
<p class="text-sm text-stein-600">{o.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -174,11 +193,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if block.addOrganisationTitle || block.addOrganisationMarkdown}
|
{#if showAddOrganisationCard}
|
||||||
<Card
|
<Card
|
||||||
class={compact
|
href={addOrganisationCtaLink?.url}
|
||||||
? "border-dashed border-wald-300 bg-wald-50 p-3! gap-1.5!"
|
target={addOrganisationCtaLink?.newTab ? "_blank" : undefined}
|
||||||
: "border-dashed border-wald-300 bg-wald-50"}
|
rel={addOrganisationCtaLink?.newTab ? "noopener noreferrer" : undefined}
|
||||||
|
class={addOrganisationCardClass}
|
||||||
>
|
>
|
||||||
{#if block.addOrganisationTitle}
|
{#if block.addOrganisationTitle}
|
||||||
<h5
|
<h5
|
||||||
@@ -194,6 +214,15 @@
|
|||||||
{@html ctaHtml}
|
{@html ctaHtml}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if addOrganisationCtaLink}
|
||||||
|
<p
|
||||||
|
class={compact
|
||||||
|
? "mb-0 mt-1 text-xs font-medium text-wald-800"
|
||||||
|
: "mb-0 mt-2 text-sm font-medium text-wald-800"}
|
||||||
|
>
|
||||||
|
{addOrganisationCtaLink.linkName}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</Card>
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={layoutClasses} data-block-type="quote" data-block-slug={block._slug}>
|
<div class={layoutClasses} data-block-type="quote" data-block-slug={block._slug}>
|
||||||
<blockquote class="border-l-4 border-green-700 pl-4 {variantClass}">
|
<blockquote class="border-l-4 border-wald-700 pl-4 {variantClass}">
|
||||||
<p class="text-lg italic text-zinc-700">"{block.quote ?? ""}"</p>
|
<p class="text-xl font-semibold text-wald-700">{block.quote ?? ""}</p>
|
||||||
{#if block.author}
|
{#if block.author}
|
||||||
<cite class="mt-1 block not-italic text-sm text-zinc-500">— {block.author}</cite>
|
<cite class="mt-1 block not-italic text-sm text-wald-300">{block.author}</cite>
|
||||||
{/if}
|
{/if}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ export interface OrganisationsBlockData {
|
|||||||
presentation?: "default" | "compact";
|
presentation?: "default" | "compact";
|
||||||
addOrganisationTitle?: string;
|
addOrganisationTitle?: string;
|
||||||
addOrganisationMarkdown?: string;
|
addOrganisationMarkdown?: string;
|
||||||
|
/** Referenz auf link-Collection (aufgelöst: url, linkName, newTab) */
|
||||||
|
addOrganisationLink?: string | { url?: string; linkName?: string; newTab?: boolean };
|
||||||
organisations?: (string | OrganisationEntry)[];
|
organisations?: (string | OrganisationEntry)[];
|
||||||
layout?: BlockLayout;
|
layout?: BlockLayout;
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-28
@@ -11,35 +11,74 @@ import type { RowContentLayout } from "./block-types";
|
|||||||
import type { CalendarItemData } from "./block-types";
|
import type { CalendarItemData } from "./block-types";
|
||||||
/** rusty-image nur dynamisch importieren, damit client-seitige Importe (z. B. PostOverviewBlock) nicht node:crypto in den Browser ziehen. */
|
/** rusty-image nur dynamisch importieren, damit client-seitige Importe (z. B. PostOverviewBlock) nicht node:crypto in den Browser ziehen. */
|
||||||
|
|
||||||
/** Slug → Anzeigename (name) aus der Tag-API. Für Auflösung von post.postTag in Lesbarkeit. */
|
/** Aus der Tag-API: Anzeigename plus optionales Icon und Farbe (CMS-Felder icon / color). */
|
||||||
export async function getTagsMap(): Promise<Map<string, string>> {
|
export type TagMeta = {
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Slug → TagMeta aus der Tag-API. Für Auflösung von post.postTag. */
|
||||||
|
export async function getTagsMap(): Promise<Map<string, TagMeta>> {
|
||||||
const tags = await getTags();
|
const tags = await getTags();
|
||||||
const m = new Map<string, string>();
|
const m = new Map<string, TagMeta>();
|
||||||
for (const t of tags) {
|
for (const t of tags) {
|
||||||
const slug =
|
const slug =
|
||||||
(t as { _slug?: string })._slug ?? (t as { slug?: string }).slug ?? "";
|
(t as { _slug?: string })._slug ?? (t as { slug?: string }).slug ?? "";
|
||||||
if (slug) m.set(slug, (t as { name?: string }).name ?? slug);
|
if (!slug) continue;
|
||||||
|
const name = (t as { name?: string }).name?.trim() ?? slug;
|
||||||
|
const icon = (t as { icon?: string }).icon?.trim();
|
||||||
|
const color = (t as { color?: string }).color?.trim();
|
||||||
|
m.set(slug, {
|
||||||
|
name,
|
||||||
|
...(icon ? { icon } : {}),
|
||||||
|
...(color ? { color } : {}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Setzt post.postTag auf { _slug, name }[]; bei reinen Slug-Strings wird name aus slugToName geholt. */
|
export type ResolvedPostTag = {
|
||||||
|
_slug: string;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Setzt post.postTag auf { _slug, name, icon?, color? }[]; Metadaten aus slugToMeta bzw. bereits aufgelösten Refs. */
|
||||||
export function resolvePostTagsInPost(
|
export function resolvePostTagsInPost(
|
||||||
post: PostEntry,
|
post: PostEntry,
|
||||||
slugToName: Map<string, string>,
|
slugToMeta: Map<string, TagMeta>,
|
||||||
): void {
|
): void {
|
||||||
const raw = post.postTag ?? [];
|
const raw = post.postTag ?? [];
|
||||||
if (!Array.isArray(raw)) return;
|
if (!Array.isArray(raw)) return;
|
||||||
(post as { postTag?: { _slug: string; name: string }[] }).postTag = raw.map(
|
(post as { postTag?: ResolvedPostTag[] }).postTag = raw.map((t) => {
|
||||||
(t) => {
|
if (typeof t === "string") {
|
||||||
if (typeof t === "string") {
|
const meta = slugToMeta.get(t);
|
||||||
return { _slug: t, name: slugToName.get(t) ?? t };
|
return {
|
||||||
}
|
_slug: t,
|
||||||
const o = t as { _slug?: string; name?: string };
|
name: meta?.name ?? t,
|
||||||
const slug = o._slug ?? o?.name ?? "";
|
...(meta?.icon ? { icon: meta.icon } : {}),
|
||||||
return { _slug: slug, name: o?.name ?? slugToName.get(slug) ?? slug };
|
...(meta?.color ? { color: meta.color } : {}),
|
||||||
},
|
};
|
||||||
);
|
}
|
||||||
|
const o = t as {
|
||||||
|
_slug?: string;
|
||||||
|
name?: string;
|
||||||
|
icon?: string;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
const slug = o._slug ?? o?.name ?? "";
|
||||||
|
const meta = slug ? slugToMeta.get(slug) : undefined;
|
||||||
|
const icon = (o.icon ?? meta?.icon)?.trim();
|
||||||
|
const color = (o.color ?? meta?.color)?.trim();
|
||||||
|
return {
|
||||||
|
_slug: slug,
|
||||||
|
name: o?.name ?? meta?.name ?? slug,
|
||||||
|
...(icon ? { icon } : {}),
|
||||||
|
...(color ? { color } : {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const POSTS_PER_PAGE = 10;
|
const POSTS_PER_PAGE = 10;
|
||||||
@@ -48,6 +87,13 @@ export function getPostsPerPage(): number {
|
|||||||
return POSTS_PER_PAGE;
|
return POSTS_PER_PAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Filtert Posts mit hideFromListing: true aus Übersichten heraus. */
|
||||||
|
export function filterHiddenPosts(posts: PostEntry[]): PostEntry[] {
|
||||||
|
return posts.filter(
|
||||||
|
(p) => !(p as PostEntry & { hideFromListing?: boolean }).hideFromListing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Sortiert Posts nach Datum (neueste zuerst). */
|
/** Sortiert Posts nach Datum (neueste zuerst). */
|
||||||
export function sortPostsByDate(posts: PostEntry[]): PostEntry[] {
|
export function sortPostsByDate(posts: PostEntry[]): PostEntry[] {
|
||||||
return [...posts].sort((a, b) => {
|
return [...posts].sort((a, b) => {
|
||||||
@@ -159,12 +205,12 @@ function isPostOverviewBlock(item: unknown): item is {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt Posts für alle post_overview-Blöcke im Layout und setzt postsResolved.
|
* Lädt Posts für alle post_overview-Blöcke im Layout und setzt postsResolved.
|
||||||
* Nutzt die globale Tag-API (getTagsMap()) für Slug → Anzeigename.
|
* Nutzt die globale Tag-API (getTagsMap()) für Slug → TagMeta.
|
||||||
* Gibt die tagsMap zurück (z. B. für resolvePostTagsInPost auf [slug].astro).
|
* Gibt die tagsMap zurück (z. B. für resolvePostTagsInPost auf [slug].astro).
|
||||||
*/
|
*/
|
||||||
export async function resolvePostOverviewBlocks(
|
export async function resolvePostOverviewBlocks(
|
||||||
layout: RowContentLayout | null | undefined,
|
layout: RowContentLayout | null | undefined,
|
||||||
): Promise<Map<string, string>> {
|
): Promise<Map<string, TagMeta>> {
|
||||||
const tagsMap = await getTagsMap();
|
const tagsMap = await getTagsMap();
|
||||||
|
|
||||||
if (!layout) return tagsMap;
|
if (!layout) return tagsMap;
|
||||||
@@ -186,7 +232,7 @@ export async function resolvePostOverviewBlocks(
|
|||||||
_order: "desc",
|
_order: "desc",
|
||||||
resolve: "all",
|
resolve: "all",
|
||||||
});
|
});
|
||||||
let list = sortPostsByDate(allPosts);
|
let list = filterHiddenPosts(sortPostsByDate(allPosts));
|
||||||
const rawFilter = item.filterByTag ?? [];
|
const rawFilter = item.filterByTag ?? [];
|
||||||
const tagSlugs = Array.isArray(rawFilter)
|
const tagSlugs = Array.isArray(rawFilter)
|
||||||
? rawFilter
|
? rawFilter
|
||||||
@@ -242,8 +288,8 @@ export async function resolvePostOverviewBlocks(
|
|||||||
try {
|
try {
|
||||||
const { ensureTransformedImage } = await import("./rusty-image");
|
const { ensureTransformedImage } = await import("./rusty-image");
|
||||||
const url = await ensureTransformedImage(raw, {
|
const url = await ensureTransformedImage(raw, {
|
||||||
width: 150,
|
width: 400,
|
||||||
height: 200,
|
height: 267,
|
||||||
fit: "cover",
|
fit: "cover",
|
||||||
format: "webp",
|
format: "webp",
|
||||||
});
|
});
|
||||||
@@ -340,19 +386,19 @@ function isResolvedFragment(f: unknown): boolean {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalisiert tags eines Fragments zu Anzeigenamen (string[]).
|
* Normalisiert tags eines Fragments zu Anzeigenamen (string[]).
|
||||||
* Nutzt slugToName für Slug → Name; Objekte mit name/_slug werden unterstützt.
|
* Nutzt slugToMeta für Slug → Anzeigename; Objekte mit name/_slug werden unterstützt.
|
||||||
*/
|
*/
|
||||||
function resolveFragmentTags(
|
function resolveFragmentTags(
|
||||||
tagsRaw: unknown[] | undefined,
|
tagsRaw: unknown[] | undefined,
|
||||||
slugToName: Map<string, string>,
|
slugToMeta: Map<string, TagMeta>,
|
||||||
): string[] {
|
): string[] {
|
||||||
if (!Array.isArray(tagsRaw)) return [];
|
if (!Array.isArray(tagsRaw)) return [];
|
||||||
return tagsRaw
|
return tagsRaw
|
||||||
.map((t) => {
|
.map((t) => {
|
||||||
if (typeof t === "string") return slugToName.get(t) ?? t;
|
if (typeof t === "string") return slugToMeta.get(t)?.name ?? t;
|
||||||
const o = t as { _slug?: string; name?: string };
|
const o = t as { _slug?: string; name?: string };
|
||||||
const slug = o._slug ?? "";
|
const slug = o._slug ?? "";
|
||||||
return (o.name ?? slugToName.get(slug) ?? slug).trim();
|
return (o.name ?? slugToMeta.get(slug)?.name ?? slug).trim();
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
@@ -361,14 +407,14 @@ function resolveFragmentTags(
|
|||||||
* Lädt für alle searchable_text-Blöcke im Layout die textFragments nach
|
* Lädt für alle searchable_text-Blöcke im Layout die textFragments nach
|
||||||
* (wenn sie als Slugs/Refs kommen), setzt die aufgelösten Einträge und
|
* (wenn sie als Slugs/Refs kommen), setzt die aufgelösten Einträge und
|
||||||
* löst Tag-Slugs/Refs in Anzeigenamen auf.
|
* löst Tag-Slugs/Refs in Anzeigenamen auf.
|
||||||
* @param slugToName Optionale Map Slug → Anzeigename (z. B. von resolvePostOverviewBlocks); sonst getTagsMap().
|
* @param slugToMeta Optionale Map Slug → TagMeta (z. B. von resolvePostOverviewBlocks); sonst getTagsMap().
|
||||||
*/
|
*/
|
||||||
export async function resolveSearchableTextBlocks(
|
export async function resolveSearchableTextBlocks(
|
||||||
layout: RowContentLayout | null | undefined,
|
layout: RowContentLayout | null | undefined,
|
||||||
slugToName?: Map<string, string>,
|
slugToMeta?: Map<string, TagMeta>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!layout) return;
|
if (!layout) return;
|
||||||
const tagsMap = slugToName ?? (await getTagsMap());
|
const tagsMap = slugToMeta ?? (await getTagsMap());
|
||||||
const rows = [
|
const rows = [
|
||||||
layout.row1Content ?? [],
|
layout.row1Content ?? [],
|
||||||
layout.row2Content ?? [],
|
layout.row2Content ?? [],
|
||||||
|
|||||||
@@ -4300,7 +4300,9 @@ export interface components {
|
|||||||
organisations: {
|
organisations: {
|
||||||
/** @description Entry identifier (filename) */
|
/** @description Entry identifier (filename) */
|
||||||
readonly _slug?: string;
|
readonly _slug?: string;
|
||||||
/** @description Optional markdown content shown above the organisation list */
|
/** @description Optional CTA link in the dashed card (label from link.linkName) */
|
||||||
|
addOrganisationLink?: string;
|
||||||
|
/** @description Optional markdown body for the dashed CTA card (no link here; use addOrganisationLink) */
|
||||||
addOrganisationMarkdown?: string;
|
addOrganisationMarkdown?: string;
|
||||||
/** @description Optional section title shown above the organisation list */
|
/** @description Optional section title shown above the organisation list */
|
||||||
addOrganisationTitle?: string;
|
addOrganisationTitle?: string;
|
||||||
@@ -4322,7 +4324,9 @@ export interface components {
|
|||||||
organisations_input: {
|
organisations_input: {
|
||||||
/** @description URL slug (used as filename) */
|
/** @description URL slug (used as filename) */
|
||||||
_slug: string;
|
_slug: string;
|
||||||
/** @description Optional markdown content shown above the organisation list */
|
/** @description Optional CTA link in the dashed card (label from link.linkName) */
|
||||||
|
addOrganisationLink?: string;
|
||||||
|
/** @description Optional markdown body for the dashed CTA card (no link here; use addOrganisationLink) */
|
||||||
addOrganisationMarkdown?: string;
|
addOrganisationMarkdown?: string;
|
||||||
/** @description Optional section title shown above the organisation list */
|
/** @description Optional section title shown above the organisation list */
|
||||||
addOrganisationTitle?: string;
|
addOrganisationTitle?: string;
|
||||||
@@ -4547,6 +4551,11 @@ export interface components {
|
|||||||
excerpt?: string;
|
excerpt?: string;
|
||||||
/** @description Post headline */
|
/** @description Post headline */
|
||||||
headline: string;
|
headline: string;
|
||||||
|
/**
|
||||||
|
* @description Hide this post from all listing pages (still accessible via direct URL)
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hideFromListing: boolean;
|
||||||
/** @description Optional icon identifier */
|
/** @description Optional icon identifier */
|
||||||
icon?: string;
|
icon?: string;
|
||||||
/**
|
/**
|
||||||
@@ -4639,6 +4648,11 @@ export interface components {
|
|||||||
excerpt?: string;
|
excerpt?: string;
|
||||||
/** @description Post headline */
|
/** @description Post headline */
|
||||||
headline: string;
|
headline: string;
|
||||||
|
/**
|
||||||
|
* @description Hide this post from all listing pages (still accessible via direct URL)
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hideFromListing: boolean;
|
||||||
/** @description Optional icon identifier */
|
/** @description Optional icon identifier */
|
||||||
icon?: string;
|
icon?: string;
|
||||||
/**
|
/**
|
||||||
@@ -5193,12 +5207,20 @@ export interface components {
|
|||||||
tag: {
|
tag: {
|
||||||
/** @description Entry identifier (filename) */
|
/** @description Entry identifier (filename) */
|
||||||
readonly _slug?: string;
|
readonly _slug?: string;
|
||||||
|
/** @description Optional pill color (CSS, e.g. #2d5a27 or rgb()). Admin: color picker + text field. */
|
||||||
|
color?: string;
|
||||||
|
/** @description Optional Iconify icon id (e.g. mdi:map-marker, same as link.icon). */
|
||||||
|
icon?: string;
|
||||||
/** @description Tag name (e.g. politik, termin). Must be unique. */
|
/** @description Tag name (e.g. politik, termin). Must be unique. */
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
tag_input: {
|
tag_input: {
|
||||||
/** @description URL slug (used as filename) */
|
/** @description URL slug (used as filename) */
|
||||||
_slug: string;
|
_slug: string;
|
||||||
|
/** @description Optional pill color (CSS, e.g. #2d5a27 or rgb()). Admin: color picker + text field. */
|
||||||
|
color?: string;
|
||||||
|
/** @description Optional Iconify icon id (e.g. mdi:map-marker, same as link.icon). */
|
||||||
|
icon?: string;
|
||||||
/** @description Tag name (e.g. politik, termin). Must be unique. */
|
/** @description Tag name (e.g. politik, termin). Must be unique. */
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
@@ -11976,6 +11998,8 @@ export interface operations {
|
|||||||
addOrganisationTitle?: string;
|
addOrganisationTitle?: string;
|
||||||
/** @description Filter by addOrganisationMarkdown */
|
/** @description Filter by addOrganisationMarkdown */
|
||||||
addOrganisationMarkdown?: string;
|
addOrganisationMarkdown?: string;
|
||||||
|
/** @description Filter by addOrganisationLink */
|
||||||
|
addOrganisationLink?: string;
|
||||||
/** @description Filter by allowedBadges */
|
/** @description Filter by allowedBadges */
|
||||||
allowedBadges?: string;
|
allowedBadges?: string;
|
||||||
/** @description Filter by organisations */
|
/** @description Filter by organisations */
|
||||||
@@ -12764,6 +12788,8 @@ export interface operations {
|
|||||||
date?: string;
|
date?: string;
|
||||||
/** @description Filter by icon */
|
/** @description Filter by icon */
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
/** @description Filter by hideFromListing */
|
||||||
|
hideFromListing?: boolean;
|
||||||
/** @description Filter by important */
|
/** @description Filter by important */
|
||||||
important?: boolean;
|
important?: boolean;
|
||||||
/** @description Filter by linkName */
|
/** @description Filter by linkName */
|
||||||
@@ -14226,6 +14252,10 @@ export interface operations {
|
|||||||
_locale?: string;
|
_locale?: string;
|
||||||
/** @description Filter by name */
|
/** @description Filter by name */
|
||||||
name?: string;
|
name?: string;
|
||||||
|
/** @description Filter by icon */
|
||||||
|
icon?: string;
|
||||||
|
/** @description Filter by color */
|
||||||
|
color?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|||||||
@@ -354,6 +354,22 @@ export async function downloadCmsAssetLinks(html: string): Promise<string> {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet das Top-Level `content`-Feld (Markdown-Body) eines Post/Page-Eintrags:
|
||||||
|
* Markdown → HTML → Bilder transformieren → Asset-Links lokalisieren.
|
||||||
|
* Gibt fertiges HTML zurück (oder "" wenn kein content vorhanden).
|
||||||
|
*/
|
||||||
|
export async function resolveBodyContent(
|
||||||
|
entry: { content?: unknown },
|
||||||
|
): Promise<string> {
|
||||||
|
const raw = typeof entry.content === "string" ? entry.content.trim() : "";
|
||||||
|
if (!raw) return "";
|
||||||
|
const { marked } = await import("marked");
|
||||||
|
marked.setOptions({ gfm: true });
|
||||||
|
const html = marked.parse(raw) as string;
|
||||||
|
return downloadCmsAssetLinks(await processMarkdownHtmlImages(html));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Geht alle Content-Rows durch: Image-Blöcke → resolvedImageSrc;
|
* Geht alle Content-Rows durch: Image-Blöcke → resolvedImageSrc;
|
||||||
* Markdown-Blöcke → resolvedContent (HTML mit transformierten Bildern + Link-Wrapper).
|
* Markdown-Blöcke → resolvedContent (HTML mit transformierten Bildern + Link-Wrapper).
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ import ContentRows from "../../components/ContentRows.svelte";
|
|||||||
import Tag from "../../components/Tag.svelte";
|
import Tag from "../../components/Tag.svelte";
|
||||||
import Tags from "../../components/Tags.svelte";
|
import Tags from "../../components/Tags.svelte";
|
||||||
import { getPostSlugs, getPostBySlug } from "../../lib/cms";
|
import { getPostSlugs, getPostBySlug } from "../../lib/cms";
|
||||||
import { marked } from "marked";
|
|
||||||
|
|
||||||
marked.setOptions({ gfm: true });
|
|
||||||
import type { PostEntry } from "../../lib/cms";
|
import type { PostEntry } from "../../lib/cms";
|
||||||
import {
|
import {
|
||||||
resolveContentImages,
|
resolveContentImages,
|
||||||
|
resolveBodyContent,
|
||||||
ensureTransformedImage,
|
ensureTransformedImage,
|
||||||
processMarkdownHtmlImages,
|
|
||||||
} from "../../lib/rusty-image";
|
} from "../../lib/rusty-image";
|
||||||
import {
|
import {
|
||||||
resolvePostTagsInPost,
|
resolvePostTagsInPost,
|
||||||
@@ -66,14 +63,7 @@ const postImageUrl = rawPostImageUrl
|
|||||||
|
|
||||||
const postDate = formatPostDate(post.created ?? post.date);
|
const postDate = formatPostDate(post.created ?? post.date);
|
||||||
|
|
||||||
const postContent = (post as { content?: string }).content;
|
const contentHtml = await resolveBodyContent(post as { content?: unknown });
|
||||||
let contentHtml =
|
|
||||||
typeof postContent === "string" && postContent.trim()
|
|
||||||
? (marked.parse(postContent) as string)
|
|
||||||
: "";
|
|
||||||
if (contentHtml) {
|
|
||||||
contentHtml = await processMarkdownHtmlImages(contentHtml);
|
|
||||||
}
|
|
||||||
const hasRowContent =
|
const hasRowContent =
|
||||||
((post as { row1Content?: unknown[] }).row1Content?.length ?? 0) > 0 ||
|
((post as { row1Content?: unknown[] }).row1Content?.length ?? 0) > 0 ||
|
||||||
((post as { row2Content?: unknown[] }).row2Content?.length ?? 0) > 0 ||
|
((post as { row2Content?: unknown[] }).row2Content?.length ?? 0) > 0 ||
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import {
|
|||||||
} from '../../lib/cms';
|
} from '../../lib/cms';
|
||||||
import {
|
import {
|
||||||
sortPostsByDate,
|
sortPostsByDate,
|
||||||
|
filterHiddenPosts,
|
||||||
filterPostsByTag,
|
filterPostsByTag,
|
||||||
getTotalPages,
|
getTotalPages,
|
||||||
paginate,
|
paginate,
|
||||||
getPostsPerPage,
|
getPostsPerPage,
|
||||||
getTagsMap,
|
getTagsMap,
|
||||||
resolvePostTagsInPost,
|
resolvePostTagsInPost,
|
||||||
|
getPostImageUrl,
|
||||||
} from '../../lib/blog-utils';
|
} from '../../lib/blog-utils';
|
||||||
import { t, T } from '../../lib/translations';
|
import { t, T } from '../../lib/translations';
|
||||||
|
|
||||||
@@ -23,10 +25,21 @@ let tags: Awaited<ReturnType<typeof getTags>> = [];
|
|||||||
let cmsError: string | null = null;
|
let cmsError: string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
[posts, tags] = await Promise.all([getPosts({ resolve: ['postImage'] }), getTags()]);
|
||||||
posts = sortPostsByDate(posts);
|
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||||
const tagsMap = await getTagsMap();
|
const tagsMap = await getTagsMap();
|
||||||
for (const p of posts) resolvePostTagsInPost(p, tagsMap);
|
for (const p of posts) resolvePostTagsInPost(p, tagsMap);
|
||||||
|
// Resolve post card images
|
||||||
|
const { ensureTransformedImage } = await import('../../lib/rusty-image');
|
||||||
|
for (const post of posts) {
|
||||||
|
const raw = getPostImageUrl(post.postImage);
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
const url = await ensureTransformedImage(raw, { width: 400, height: 267, fit: 'cover', format: 'webp' });
|
||||||
|
(post as typeof post & { _resolvedImageUrl?: string })._resolvedImageUrl = url;
|
||||||
|
} catch { /* image optional */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
cmsError = e instanceof Error ? e.message : String(e);
|
cmsError = e instanceof Error ? e.message : String(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import BlogOverview from '../../../components/BlogOverview.svelte';
|
|||||||
import { getPosts, getTags } from '../../../lib/cms';
|
import { getPosts, getTags } from '../../../lib/cms';
|
||||||
import {
|
import {
|
||||||
sortPostsByDate,
|
sortPostsByDate,
|
||||||
|
filterHiddenPosts,
|
||||||
filterPostsByTag,
|
filterPostsByTag,
|
||||||
getTotalPages,
|
getTotalPages,
|
||||||
paginate,
|
paginate,
|
||||||
getPostsPerPage,
|
getPostsPerPage,
|
||||||
|
getPostImageUrl,
|
||||||
} from '../../../lib/blog-utils';
|
} from '../../../lib/blog-utils';
|
||||||
import { t, T } from '../../../lib/translations';
|
import { t, T } from '../../../lib/translations';
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ export async function getStaticPaths() {
|
|||||||
let posts: Awaited<ReturnType<typeof getPosts>> = [];
|
let posts: Awaited<ReturnType<typeof getPosts>> = [];
|
||||||
try {
|
try {
|
||||||
posts = await getPosts();
|
posts = await getPosts();
|
||||||
posts = sortPostsByDate(posts);
|
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -37,8 +39,18 @@ let tags: Awaited<ReturnType<typeof getTags>> = [];
|
|||||||
let cmsError: string | null = null;
|
let cmsError: string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
[posts, tags] = await Promise.all([getPosts({ resolve: ['postImage'] }), getTags()]);
|
||||||
posts = sortPostsByDate(posts);
|
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||||
|
const { ensureTransformedImage } = await import('../../../lib/rusty-image');
|
||||||
|
for (const post of posts) {
|
||||||
|
const raw = getPostImageUrl(post.postImage);
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
const url = await ensureTransformedImage(raw, { width: 400, height: 267, fit: 'cover', format: 'webp' });
|
||||||
|
(post as typeof post & { _resolvedImageUrl?: string })._resolvedImageUrl = url;
|
||||||
|
} catch { /* image optional */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
cmsError = e instanceof Error ? e.message : String(e);
|
cmsError = e instanceof Error ? e.message : String(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import BlogOverview from '../../../components/BlogOverview.svelte';
|
|||||||
import { getPosts, getTags } from '../../../lib/cms';
|
import { getPosts, getTags } from '../../../lib/cms';
|
||||||
import {
|
import {
|
||||||
sortPostsByDate,
|
sortPostsByDate,
|
||||||
|
filterHiddenPosts,
|
||||||
filterPostsByTag,
|
filterPostsByTag,
|
||||||
getTotalPages,
|
getTotalPages,
|
||||||
paginate,
|
paginate,
|
||||||
getPostsPerPage,
|
getPostsPerPage,
|
||||||
getTagsMap,
|
getTagsMap,
|
||||||
resolvePostTagsInPost,
|
resolvePostTagsInPost,
|
||||||
|
getPostImageUrl,
|
||||||
} from '../../../lib/blog-utils';
|
} from '../../../lib/blog-utils';
|
||||||
import { t, T } from '../../../lib/translations';
|
import { t, T } from '../../../lib/translations';
|
||||||
|
|
||||||
@@ -22,7 +24,7 @@ export async function getStaticPaths() {
|
|||||||
let tags: Awaited<ReturnType<typeof getTags>> = [];
|
let tags: Awaited<ReturnType<typeof getTags>> = [];
|
||||||
try {
|
try {
|
||||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
||||||
posts = sortPostsByDate(posts);
|
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -46,10 +48,20 @@ let tags: Awaited<ReturnType<typeof getTags>> = [];
|
|||||||
let cmsError: string | null = null;
|
let cmsError: string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
[posts, tags] = await Promise.all([getPosts({ resolve: ['postImage'] }), getTags()]);
|
||||||
posts = sortPostsByDate(posts);
|
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||||
const tagsMap = await getTagsMap();
|
const tagsMap = await getTagsMap();
|
||||||
for (const p of posts) resolvePostTagsInPost(p, tagsMap);
|
for (const p of posts) resolvePostTagsInPost(p, tagsMap);
|
||||||
|
const { ensureTransformedImage } = await import('../../../lib/rusty-image');
|
||||||
|
for (const post of posts) {
|
||||||
|
const raw = getPostImageUrl(post.postImage);
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
const url = await ensureTransformedImage(raw, { width: 400, height: 267, fit: 'cover', format: 'webp' });
|
||||||
|
(post as typeof post & { _resolvedImageUrl?: string })._resolvedImageUrl = url;
|
||||||
|
} catch { /* image optional */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
cmsError = e instanceof Error ? e.message : String(e);
|
cmsError = e instanceof Error ? e.message : String(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import BlogOverview from '../../../../../components/BlogOverview.svelte';
|
|||||||
import { getPosts, getTags } from '../../../../../lib/cms';
|
import { getPosts, getTags } from '../../../../../lib/cms';
|
||||||
import {
|
import {
|
||||||
sortPostsByDate,
|
sortPostsByDate,
|
||||||
|
filterHiddenPosts,
|
||||||
filterPostsByTag,
|
filterPostsByTag,
|
||||||
getTotalPages,
|
getTotalPages,
|
||||||
paginate,
|
paginate,
|
||||||
getPostsPerPage,
|
getPostsPerPage,
|
||||||
getTagsMap,
|
getTagsMap,
|
||||||
resolvePostTagsInPost,
|
resolvePostTagsInPost,
|
||||||
|
getPostImageUrl,
|
||||||
} from '../../../../../lib/blog-utils';
|
} from '../../../../../lib/blog-utils';
|
||||||
import { t, T } from '../../../../../lib/translations';
|
import { t, T } from '../../../../../lib/translations';
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@ export async function getStaticPaths() {
|
|||||||
let tags: Awaited<ReturnType<typeof getTags>> = [];
|
let tags: Awaited<ReturnType<typeof getTags>> = [];
|
||||||
try {
|
try {
|
||||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
||||||
posts = sortPostsByDate(posts);
|
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -58,10 +60,20 @@ let tags: Awaited<ReturnType<typeof getTags>> = [];
|
|||||||
let cmsError: string | null = null;
|
let cmsError: string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
[posts, tags] = await Promise.all([getPosts({ resolve: ['postImage'] }), getTags()]);
|
||||||
posts = sortPostsByDate(posts);
|
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||||
const tagsMap = await getTagsMap();
|
const tagsMap = await getTagsMap();
|
||||||
for (const p of posts) resolvePostTagsInPost(p, tagsMap);
|
for (const p of posts) resolvePostTagsInPost(p, tagsMap);
|
||||||
|
const { ensureTransformedImage } = await import('../../../../../lib/rusty-image');
|
||||||
|
for (const post of posts) {
|
||||||
|
const raw = getPostImageUrl(post.postImage);
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
const url = await ensureTransformedImage(raw, { width: 400, height: 267, fit: 'cover', format: 'webp' });
|
||||||
|
(post as typeof post & { _resolvedImageUrl?: string })._resolvedImageUrl = url;
|
||||||
|
} catch { /* image optional */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
cmsError = e instanceof Error ? e.message : String(e);
|
cmsError = e instanceof Error ? e.message : String(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ main a:not(.no-underline):hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Lange URLs umbrechen, damit sie nicht über den Bildschirmrand ragen */
|
/* Lange URLs umbrechen, damit sie nicht über den Bildschirmrand ragen */
|
||||||
main a,
|
main a:not(.no-underline),
|
||||||
.markdown a {
|
.markdown a {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
|||||||
Reference in New Issue
Block a user