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/
|
||||
.history/
|
||||
public/images/transformed/
|
||||
public/images/raw/
|
||||
public/pagefind
|
||||
@@ -10,6 +10,8 @@
|
||||
interface TagEntry {
|
||||
_slug?: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -62,6 +64,8 @@
|
||||
href={tagHref(slug)}
|
||||
variant={activeTag === slug ? "inactive" : "green"}
|
||||
active={activeTag === slug}
|
||||
icon={tag.icon?.trim() || null}
|
||||
customColor={activeTag === slug ? null : tag.color?.trim() || null}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -75,7 +79,7 @@
|
||||
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}
|
||||
<PostCard
|
||||
post={post}
|
||||
|
||||
@@ -4,15 +4,25 @@
|
||||
let {
|
||||
children,
|
||||
href,
|
||||
target,
|
||||
rel,
|
||||
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 interactive = href ? "hover:border-stein-300 hover:shadow-md transition-all no-underline text-inherit" : "";
|
||||
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 = $derived(
|
||||
href ? "hover:border-stein-300 hover:shadow-md transition-all no-underline text-inherit" : "",
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a {href} class="{base} {interactive} {className}">
|
||||
<a {href} {target} {rel} class="{base} {interactive} {className}">
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
>
|
||||
{#each row.content as item}
|
||||
{#if isResolvedBlock(item)}
|
||||
{@const rowBlockType = blockType(item)}
|
||||
{#snippet blockContent()}
|
||||
{#if blockType(item) === "markdown"}
|
||||
<MarkdownBlock block={item as MarkdownBlockData} />
|
||||
@@ -132,8 +133,12 @@
|
||||
{/snippet}
|
||||
{#if isBlockLayoutBreakout((item as { layout?: BlockLayout }).layout)}
|
||||
<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 class="container-custom py-4">
|
||||
<div
|
||||
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()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,23 +15,29 @@
|
||||
} = $props();
|
||||
|
||||
const resolvedImg = $derived(post._resolvedImageUrl);
|
||||
const bgStyle = $derived(resolvedImg ? `background-image: url(${resolvedImg})` : "");
|
||||
</script>
|
||||
|
||||
<a
|
||||
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"
|
||||
>
|
||||
<div class="aspect-3/2 w-full overflow-hidden bg-gray-100">
|
||||
{#if resolvedImg}
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center opacity-10"
|
||||
style={bgStyle}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<img
|
||||
src={resolvedImg}
|
||||
alt={post.headline ?? ""}
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
{: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
|
||||
class="relative flex-1 flex flex-col justify-start p-4"
|
||||
>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-start p-4">
|
||||
<PostMeta
|
||||
date={post.created ?? post.date ?? null}
|
||||
tags={post.postTag ?? []}
|
||||
@@ -42,12 +48,12 @@
|
||||
{post.headline ?? post.linkName ?? post.slug ?? post._slug}
|
||||
</h5>
|
||||
{#if post.subheadline}
|
||||
<div class="text-xs line-clamp-2 mb-1 ">
|
||||
<div class="text-xs line-clamp-2 mb-1">
|
||||
{post.subheadline}
|
||||
</div>
|
||||
{/if}
|
||||
{#if post.excerpt}
|
||||
<p class="text-xs font-light line-clamp-3 ">
|
||||
<p class="text-xs font-light line-clamp-3">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
+94
-15
@@ -1,4 +1,7 @@
|
||||
<script lang="ts">
|
||||
import "../lib/iconify-offline";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
type Variant = "white" | "green" | "blue" | "inactive" | "date";
|
||||
|
||||
let {
|
||||
@@ -7,36 +10,112 @@
|
||||
href = null,
|
||||
active = false,
|
||||
onclick = null,
|
||||
icon = null,
|
||||
customColor = null,
|
||||
}: {
|
||||
label: string;
|
||||
variant?: Variant;
|
||||
href?: string | null;
|
||||
active?: boolean;
|
||||
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();
|
||||
|
||||
/** 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 =
|
||||
"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(() => {
|
||||
if (variant === "white") return "bg-gradient-to-b from-white to-gray-100 text-black! ring-black/5!";
|
||||
if (variant === "green") return "bg-gradient-to-b from-green-300 to-green-400 text-black! ";
|
||||
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 === "date") return "bg-gradient-to-b from-green-50 to-green-100 text-green-700! ring-green-200!";
|
||||
return "bg-gray-200 text-gray-500";
|
||||
if (customColor?.trim()) {
|
||||
return "";
|
||||
}
|
||||
if (variant === "white")
|
||||
return "bg-stein-100 text-stein-800";
|
||||
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(() => {
|
||||
if (!href && !onclick) return "shadow-none";
|
||||
if (variant === "date") return "shadow-[0_0_6px_rgba(0,0,0,0.3)]";
|
||||
return "shadow-[0_2px_6px_rgba(0,0,0,0.2)]";
|
||||
const pillStyle = $derived.by(() => {
|
||||
const c = customColor?.trim();
|
||||
if (!c) return "";
|
||||
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>
|
||||
|
||||
{#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}
|
||||
<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}
|
||||
<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}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
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";
|
||||
|
||||
let {
|
||||
@@ -16,12 +18,30 @@
|
||||
|
||||
function displayName(t: TagItem): string {
|
||||
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 {
|
||||
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>
|
||||
|
||||
@@ -32,6 +52,8 @@
|
||||
label={name}
|
||||
variant={variant}
|
||||
href={tagBase ? `${tagBase}/${slug(t)}` : null}
|
||||
icon={tagIcon(t)}
|
||||
customColor={tagColor(t)}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -13,25 +13,41 @@
|
||||
: "https://opnform.pm86.de"
|
||||
).replace(/\/$/, ""),
|
||||
);
|
||||
const iframeSrc = $derived(
|
||||
formId ? `${baseUrl}/forms/${formId}` : "",
|
||||
);
|
||||
const iframeTitle = $derived(
|
||||
block.iframeTitle?.trim() || "Kontaktformular",
|
||||
);
|
||||
const iframeSrc = $derived(formId ? `${baseUrl}/forms/${formId}` : "");
|
||||
const iframeTitle = $derived(block.iframeTitle?.trim() || "Kontaktformular");
|
||||
|
||||
/**
|
||||
* 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;";
|
||||
let iframeEl = $state<HTMLIFrameElement | null>(null);
|
||||
|
||||
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`;
|
||||
|
||||
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() {
|
||||
const w = window as Window & {
|
||||
initEmbed?: (id: string, opts: { autoResize?: boolean }) => void;
|
||||
@@ -50,9 +66,7 @@
|
||||
} else {
|
||||
existing.addEventListener("load", runInit, { once: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
const s = document.createElement("script");
|
||||
s.type = "text/javascript";
|
||||
s.async = true;
|
||||
@@ -60,17 +74,23 @@
|
||||
s.dataset.opnformEmbedJs = baseUrl;
|
||||
s.onload = () => runInit();
|
||||
document.body.appendChild(s);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<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-slug={block._slug}
|
||||
>
|
||||
{#if iframeSrc}
|
||||
<iframe
|
||||
style={iframeStyle}
|
||||
bind:this={iframeEl}
|
||||
style="border: none; width: 100%;"
|
||||
id={formId}
|
||||
title={iframeTitle}
|
||||
src={iframeSrc}
|
||||
|
||||
@@ -34,6 +34,30 @@
|
||||
? (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>
|
||||
|
||||
<div class={layoutClasses} data-block-type="organisations" data-block-slug={block._slug}>
|
||||
@@ -103,12 +127,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<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>
|
||||
{#if o.badge && typeof o.badge === "object" && o.badge.label}
|
||||
<Badge color={o.badge.color}>{o.badge.label}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{#if o.description}
|
||||
<p class="text-xs text-stein-600 line-clamp-2">{o.description}</p>
|
||||
{/if}
|
||||
@@ -165,7 +184,7 @@
|
||||
<Badge color={o.badge.color}>{o.badge.label}</Badge>
|
||||
</div>
|
||||
{/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}
|
||||
<p class="text-sm text-stein-600">{o.description}</p>
|
||||
{/if}
|
||||
@@ -174,11 +193,12 @@
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if block.addOrganisationTitle || block.addOrganisationMarkdown}
|
||||
{#if showAddOrganisationCard}
|
||||
<Card
|
||||
class={compact
|
||||
? "border-dashed border-wald-300 bg-wald-50 p-3! gap-1.5!"
|
||||
: "border-dashed border-wald-300 bg-wald-50"}
|
||||
href={addOrganisationCtaLink?.url}
|
||||
target={addOrganisationCtaLink?.newTab ? "_blank" : undefined}
|
||||
rel={addOrganisationCtaLink?.newTab ? "noopener noreferrer" : undefined}
|
||||
class={addOrganisationCardClass}
|
||||
>
|
||||
{#if block.addOrganisationTitle}
|
||||
<h5
|
||||
@@ -194,6 +214,15 @@
|
||||
{@html ctaHtml}
|
||||
</div>
|
||||
{/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>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
</script>
|
||||
|
||||
<div class={layoutClasses} data-block-type="quote" data-block-slug={block._slug}>
|
||||
<blockquote class="border-l-4 border-green-700 pl-4 {variantClass}">
|
||||
<p class="text-lg italic text-zinc-700">"{block.quote ?? ""}"</p>
|
||||
<blockquote class="border-l-4 border-wald-700 pl-4 {variantClass}">
|
||||
<p class="text-xl font-semibold text-wald-700">{block.quote ?? ""}</p>
|
||||
{#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}
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
@@ -229,6 +229,8 @@ export interface OrganisationsBlockData {
|
||||
presentation?: "default" | "compact";
|
||||
addOrganisationTitle?: string;
|
||||
addOrganisationMarkdown?: string;
|
||||
/** Referenz auf link-Collection (aufgelöst: url, linkName, newTab) */
|
||||
addOrganisationLink?: string | { url?: string; linkName?: string; newTab?: boolean };
|
||||
organisations?: (string | OrganisationEntry)[];
|
||||
layout?: BlockLayout;
|
||||
}
|
||||
|
||||
+71
-25
@@ -11,35 +11,74 @@ import type { RowContentLayout } 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. */
|
||||
|
||||
/** Slug → Anzeigename (name) aus der Tag-API. Für Auflösung von post.postTag in Lesbarkeit. */
|
||||
export async function getTagsMap(): Promise<Map<string, string>> {
|
||||
/** Aus der Tag-API: Anzeigename plus optionales Icon und Farbe (CMS-Felder icon / color). */
|
||||
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 m = new Map<string, string>();
|
||||
const m = new Map<string, TagMeta>();
|
||||
for (const t of tags) {
|
||||
const 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;
|
||||
}
|
||||
|
||||
/** 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(
|
||||
post: PostEntry,
|
||||
slugToName: Map<string, string>,
|
||||
slugToMeta: Map<string, TagMeta>,
|
||||
): void {
|
||||
const raw = post.postTag ?? [];
|
||||
if (!Array.isArray(raw)) return;
|
||||
(post as { postTag?: { _slug: string; name: string }[] }).postTag = raw.map(
|
||||
(t) => {
|
||||
(post as { postTag?: ResolvedPostTag[] }).postTag = raw.map((t) => {
|
||||
if (typeof t === "string") {
|
||||
return { _slug: t, name: slugToName.get(t) ?? t };
|
||||
const meta = slugToMeta.get(t);
|
||||
return {
|
||||
_slug: t,
|
||||
name: meta?.name ?? t,
|
||||
...(meta?.icon ? { icon: meta.icon } : {}),
|
||||
...(meta?.color ? { color: meta.color } : {}),
|
||||
};
|
||||
}
|
||||
const o = t as { _slug?: string; name?: string };
|
||||
const o = t as {
|
||||
_slug?: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
};
|
||||
const slug = o._slug ?? o?.name ?? "";
|
||||
return { _slug: slug, name: o?.name ?? slugToName.get(slug) ?? slug };
|
||||
},
|
||||
);
|
||||
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;
|
||||
@@ -48,6 +87,13 @@ export function getPostsPerPage(): number {
|
||||
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). */
|
||||
export function sortPostsByDate(posts: PostEntry[]): PostEntry[] {
|
||||
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.
|
||||
* 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).
|
||||
*/
|
||||
export async function resolvePostOverviewBlocks(
|
||||
layout: RowContentLayout | null | undefined,
|
||||
): Promise<Map<string, string>> {
|
||||
): Promise<Map<string, TagMeta>> {
|
||||
const tagsMap = await getTagsMap();
|
||||
|
||||
if (!layout) return tagsMap;
|
||||
@@ -186,7 +232,7 @@ export async function resolvePostOverviewBlocks(
|
||||
_order: "desc",
|
||||
resolve: "all",
|
||||
});
|
||||
let list = sortPostsByDate(allPosts);
|
||||
let list = filterHiddenPosts(sortPostsByDate(allPosts));
|
||||
const rawFilter = item.filterByTag ?? [];
|
||||
const tagSlugs = Array.isArray(rawFilter)
|
||||
? rawFilter
|
||||
@@ -242,8 +288,8 @@ export async function resolvePostOverviewBlocks(
|
||||
try {
|
||||
const { ensureTransformedImage } = await import("./rusty-image");
|
||||
const url = await ensureTransformedImage(raw, {
|
||||
width: 150,
|
||||
height: 200,
|
||||
width: 400,
|
||||
height: 267,
|
||||
fit: "cover",
|
||||
format: "webp",
|
||||
});
|
||||
@@ -340,19 +386,19 @@ function isResolvedFragment(f: unknown): boolean {
|
||||
|
||||
/**
|
||||
* 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(
|
||||
tagsRaw: unknown[] | undefined,
|
||||
slugToName: Map<string, string>,
|
||||
slugToMeta: Map<string, TagMeta>,
|
||||
): string[] {
|
||||
if (!Array.isArray(tagsRaw)) return [];
|
||||
return tagsRaw
|
||||
.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 slug = o._slug ?? "";
|
||||
return (o.name ?? slugToName.get(slug) ?? slug).trim();
|
||||
return (o.name ?? slugToMeta.get(slug)?.name ?? slug).trim();
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
@@ -361,14 +407,14 @@ function resolveFragmentTags(
|
||||
* 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
|
||||
* 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(
|
||||
layout: RowContentLayout | null | undefined,
|
||||
slugToName?: Map<string, string>,
|
||||
slugToMeta?: Map<string, TagMeta>,
|
||||
): Promise<void> {
|
||||
if (!layout) return;
|
||||
const tagsMap = slugToName ?? (await getTagsMap());
|
||||
const tagsMap = slugToMeta ?? (await getTagsMap());
|
||||
const rows = [
|
||||
layout.row1Content ?? [],
|
||||
layout.row2Content ?? [],
|
||||
|
||||
@@ -4300,7 +4300,9 @@ export interface components {
|
||||
organisations: {
|
||||
/** @description Entry identifier (filename) */
|
||||
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;
|
||||
/** @description Optional section title shown above the organisation list */
|
||||
addOrganisationTitle?: string;
|
||||
@@ -4322,7 +4324,9 @@ export interface components {
|
||||
organisations_input: {
|
||||
/** @description URL slug (used as filename) */
|
||||
_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;
|
||||
/** @description Optional section title shown above the organisation list */
|
||||
addOrganisationTitle?: string;
|
||||
@@ -4547,6 +4551,11 @@ export interface components {
|
||||
excerpt?: string;
|
||||
/** @description Post headline */
|
||||
headline: string;
|
||||
/**
|
||||
* @description Hide this post from all listing pages (still accessible via direct URL)
|
||||
* @default false
|
||||
*/
|
||||
hideFromListing: boolean;
|
||||
/** @description Optional icon identifier */
|
||||
icon?: string;
|
||||
/**
|
||||
@@ -4639,6 +4648,11 @@ export interface components {
|
||||
excerpt?: string;
|
||||
/** @description Post headline */
|
||||
headline: string;
|
||||
/**
|
||||
* @description Hide this post from all listing pages (still accessible via direct URL)
|
||||
* @default false
|
||||
*/
|
||||
hideFromListing: boolean;
|
||||
/** @description Optional icon identifier */
|
||||
icon?: string;
|
||||
/**
|
||||
@@ -5193,12 +5207,20 @@ export interface components {
|
||||
tag: {
|
||||
/** @description Entry identifier (filename) */
|
||||
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. */
|
||||
name: string;
|
||||
};
|
||||
tag_input: {
|
||||
/** @description URL slug (used as filename) */
|
||||
_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. */
|
||||
name: string;
|
||||
};
|
||||
@@ -11976,6 +11998,8 @@ export interface operations {
|
||||
addOrganisationTitle?: string;
|
||||
/** @description Filter by addOrganisationMarkdown */
|
||||
addOrganisationMarkdown?: string;
|
||||
/** @description Filter by addOrganisationLink */
|
||||
addOrganisationLink?: string;
|
||||
/** @description Filter by allowedBadges */
|
||||
allowedBadges?: string;
|
||||
/** @description Filter by organisations */
|
||||
@@ -12764,6 +12788,8 @@ export interface operations {
|
||||
date?: string;
|
||||
/** @description Filter by icon */
|
||||
icon?: string;
|
||||
/** @description Filter by hideFromListing */
|
||||
hideFromListing?: boolean;
|
||||
/** @description Filter by important */
|
||||
important?: boolean;
|
||||
/** @description Filter by linkName */
|
||||
@@ -14226,6 +14252,10 @@ export interface operations {
|
||||
_locale?: string;
|
||||
/** @description Filter by name */
|
||||
name?: string;
|
||||
/** @description Filter by icon */
|
||||
icon?: string;
|
||||
/** @description Filter by color */
|
||||
color?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -354,6 +354,22 @@ export async function downloadCmsAssetLinks(html: string): Promise<string> {
|
||||
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;
|
||||
* 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 Tags from "../../components/Tags.svelte";
|
||||
import { getPostSlugs, getPostBySlug } from "../../lib/cms";
|
||||
import { marked } from "marked";
|
||||
|
||||
marked.setOptions({ gfm: true });
|
||||
import type { PostEntry } from "../../lib/cms";
|
||||
import {
|
||||
resolveContentImages,
|
||||
resolveBodyContent,
|
||||
ensureTransformedImage,
|
||||
processMarkdownHtmlImages,
|
||||
} from "../../lib/rusty-image";
|
||||
import {
|
||||
resolvePostTagsInPost,
|
||||
@@ -66,14 +63,7 @@ const postImageUrl = rawPostImageUrl
|
||||
|
||||
const postDate = formatPostDate(post.created ?? post.date);
|
||||
|
||||
const postContent = (post as { content?: string }).content;
|
||||
let contentHtml =
|
||||
typeof postContent === "string" && postContent.trim()
|
||||
? (marked.parse(postContent) as string)
|
||||
: "";
|
||||
if (contentHtml) {
|
||||
contentHtml = await processMarkdownHtmlImages(contentHtml);
|
||||
}
|
||||
const contentHtml = await resolveBodyContent(post as { content?: unknown });
|
||||
const hasRowContent =
|
||||
((post as { row1Content?: unknown[] }).row1Content?.length ?? 0) > 0 ||
|
||||
((post as { row2Content?: unknown[] }).row2Content?.length ?? 0) > 0 ||
|
||||
|
||||
@@ -7,12 +7,14 @@ import {
|
||||
} from '../../lib/cms';
|
||||
import {
|
||||
sortPostsByDate,
|
||||
filterHiddenPosts,
|
||||
filterPostsByTag,
|
||||
getTotalPages,
|
||||
paginate,
|
||||
getPostsPerPage,
|
||||
getTagsMap,
|
||||
resolvePostTagsInPost,
|
||||
getPostImageUrl,
|
||||
} from '../../lib/blog-utils';
|
||||
import { t, T } from '../../lib/translations';
|
||||
|
||||
@@ -23,10 +25,21 @@ let tags: Awaited<ReturnType<typeof getTags>> = [];
|
||||
let cmsError: string | null = null;
|
||||
|
||||
try {
|
||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
||||
posts = sortPostsByDate(posts);
|
||||
[posts, tags] = await Promise.all([getPosts({ resolve: ['postImage'] }), getTags()]);
|
||||
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||
const tagsMap = await getTagsMap();
|
||||
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) {
|
||||
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 {
|
||||
sortPostsByDate,
|
||||
filterHiddenPosts,
|
||||
filterPostsByTag,
|
||||
getTotalPages,
|
||||
paginate,
|
||||
getPostsPerPage,
|
||||
getPostImageUrl,
|
||||
} from '../../../lib/blog-utils';
|
||||
import { t, T } from '../../../lib/translations';
|
||||
|
||||
@@ -18,7 +20,7 @@ export async function getStaticPaths() {
|
||||
let posts: Awaited<ReturnType<typeof getPosts>> = [];
|
||||
try {
|
||||
posts = await getPosts();
|
||||
posts = sortPostsByDate(posts);
|
||||
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -37,8 +39,18 @@ let tags: Awaited<ReturnType<typeof getTags>> = [];
|
||||
let cmsError: string | null = null;
|
||||
|
||||
try {
|
||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
||||
posts = sortPostsByDate(posts);
|
||||
[posts, tags] = await Promise.all([getPosts({ resolve: ['postImage'] }), getTags()]);
|
||||
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) {
|
||||
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 {
|
||||
sortPostsByDate,
|
||||
filterHiddenPosts,
|
||||
filterPostsByTag,
|
||||
getTotalPages,
|
||||
paginate,
|
||||
getPostsPerPage,
|
||||
getTagsMap,
|
||||
resolvePostTagsInPost,
|
||||
getPostImageUrl,
|
||||
} from '../../../lib/blog-utils';
|
||||
import { t, T } from '../../../lib/translations';
|
||||
|
||||
@@ -22,7 +24,7 @@ export async function getStaticPaths() {
|
||||
let tags: Awaited<ReturnType<typeof getTags>> = [];
|
||||
try {
|
||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
||||
posts = sortPostsByDate(posts);
|
||||
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -46,10 +48,20 @@ let tags: Awaited<ReturnType<typeof getTags>> = [];
|
||||
let cmsError: string | null = null;
|
||||
|
||||
try {
|
||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
||||
posts = sortPostsByDate(posts);
|
||||
[posts, tags] = await Promise.all([getPosts({ resolve: ['postImage'] }), getTags()]);
|
||||
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||
const tagsMap = await getTagsMap();
|
||||
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) {
|
||||
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 {
|
||||
sortPostsByDate,
|
||||
filterHiddenPosts,
|
||||
filterPostsByTag,
|
||||
getTotalPages,
|
||||
paginate,
|
||||
getPostsPerPage,
|
||||
getTagsMap,
|
||||
resolvePostTagsInPost,
|
||||
getPostImageUrl,
|
||||
} from '../../../../../lib/blog-utils';
|
||||
import { t, T } from '../../../../../lib/translations';
|
||||
|
||||
@@ -21,7 +23,7 @@ export async function getStaticPaths() {
|
||||
let tags: Awaited<ReturnType<typeof getTags>> = [];
|
||||
try {
|
||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
||||
posts = sortPostsByDate(posts);
|
||||
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -58,10 +60,20 @@ let tags: Awaited<ReturnType<typeof getTags>> = [];
|
||||
let cmsError: string | null = null;
|
||||
|
||||
try {
|
||||
[posts, tags] = await Promise.all([getPosts(), getTags()]);
|
||||
posts = sortPostsByDate(posts);
|
||||
[posts, tags] = await Promise.all([getPosts({ resolve: ['postImage'] }), getTags()]);
|
||||
posts = filterHiddenPosts(sortPostsByDate(posts));
|
||||
const tagsMap = await getTagsMap();
|
||||
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) {
|
||||
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 */
|
||||
main a,
|
||||
main a:not(.no-underline),
|
||||
.markdown a {
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
|
||||
Reference in New Issue
Block a user