From febc027e6b67e1bc37e1a9cc75a9c1dc3b04dfe0 Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Tue, 28 Apr 2026 11:17:10 +0200 Subject: [PATCH] feat: replace Cusdis with own comment plugin Drops the Cusdis embed (postCommentSlot in pageConfig) in favour of the new RustyCMS comment plugin. New Comments.svelte renders the thread, form, replies and inline edit/delete via /api/comments. Count-badge near the post date links to the accordion and uses /api/comments/:id/count. Accordion gets a `lazy` mode so the comment iframe / fetch happens only on first open (avoids the previous "empty on first render" issue with hidden details). Co-Authored-By: Claude Opus 4.7 --- src/lib/components/Accordion.svelte | 34 +- src/lib/components/CommentCountBadge.svelte | 62 +++ src/lib/components/Comments.svelte | 394 ++++++++++++++++++++ src/routes/post/[slug]/+page.server.ts | 14 +- src/routes/post/[slug]/+page.svelte | 33 +- 5 files changed, 498 insertions(+), 39 deletions(-) create mode 100644 src/lib/components/CommentCountBadge.svelte create mode 100644 src/lib/components/Comments.svelte diff --git a/src/lib/components/Accordion.svelte b/src/lib/components/Accordion.svelte index dbe15c1..582f78a 100644 --- a/src/lib/components/Accordion.svelte +++ b/src/lib/components/Accordion.svelte @@ -3,27 +3,43 @@ import Icon from "@iconify/svelte"; import "$lib/iconify-offline"; + interface Props { + label: string; + open?: boolean; + lazy?: boolean; + id?: string; + class?: string; + onToggleEvent?: (e: Event) => void; + children: Snippet; + } + let { label, open = false, + lazy = false, id = undefined, class: cls = "", + onToggleEvent, children, - }: { - label: string; - open?: boolean; - id?: string; - class?: string; - children: Snippet; - } = $props(); + }: Props = $props(); + + let everOpened = $state(false); + const mounted = $derived(!lazy || open || everOpened); + + function onToggle(e: Event) { + if ((e.target as HTMLDetailsElement).open) everOpened = true; + onToggleEvent?.(e); + } -
+
{label}
- {@render children()} + {#if mounted} + {@render children()} + {/if}
diff --git a/src/lib/components/CommentCountBadge.svelte b/src/lib/components/CommentCountBadge.svelte new file mode 100644 index 0000000..1ff0862 --- /dev/null +++ b/src/lib/components/CommentCountBadge.svelte @@ -0,0 +1,62 @@ + + + + + {count ?? "…"} + diff --git a/src/lib/components/Comments.svelte b/src/lib/components/Comments.svelte new file mode 100644 index 0000000..8c5872a --- /dev/null +++ b/src/lib/components/Comments.svelte @@ -0,0 +1,394 @@ + + +
+ {#if loading && comments.length === 0} +

Lade Kommentare …

+ {:else if loadError} +

Fehler: {loadError}

+ {:else} + {#if topLevel.length === 0} +

Noch keine Kommentare. Sei der erste.

+ {/if} + +
    + {#each topLevel as c (c.id)} +
  • +
    + {c.author} + + {#if c.updated_at} + (bearbeitet) + {/if} + {#if c.status === "pending"} + (wartet auf Freigabe) + {/if} +
    + + {#if editingId === c.id} + + {#if editError}

    {editError}

    {/if} +
    + + +
    + {:else} +

    {c.body}

    + {#if c.can_edit} +
    + + +
    + {/if} + {/if} + +
    + +
    + + {#if replyTo === c.id} +
    onReply(e, c.id)} + class="mt-2 ml-4 space-y-2" + > + + + {#if replyError}

    {replyError}

    {/if} + +
    + {/if} + + {@const replies = repliesOf(c.id)} + {#if replies.length > 0} +
      + {#each replies as r (r.id)} +
    • +
      + {r.author} + + {#if r.updated_at} + (bearbeitet) + {/if} + {#if r.status === "pending"} + (wartet auf Freigabe) + {/if} +
      + {#if editingId === r.id} + + {#if editError}

      {editError}

      {/if} +
      + + +
      + {:else} +

      {r.body}

      + {#if r.can_edit} +
      + + +
      + {/if} + {/if} +
    • + {/each} +
    + {/if} +
  • + {/each} +
+ +
+

Kommentar schreiben

+ + + + {#if submitError}

{submitError}

{/if} + {#if requireApproval} +

Kommentare werden vor Veröffentlichung geprüft.

+ {/if} + +
+ {/if} +
diff --git a/src/routes/post/[slug]/+page.server.ts b/src/routes/post/[slug]/+page.server.ts index e2baa12..fff18ca 100644 --- a/src/routes/post/[slug]/+page.server.ts +++ b/src/routes/post/[slug]/+page.server.ts @@ -1,5 +1,5 @@ import type { PageServerLoad } from './$types'; -import { getPostBySlug, getPageConfigBySlug, getPageConfigs } from '$lib/cms'; +import { getPostBySlug } from '$lib/cms'; import { resolveContentImages, ensureTransformedImage, @@ -135,16 +135,6 @@ export const load: PageServerLoad = async ({ params, locals, url, setHeaders }) ? `https://www.openstreetmap.org/search?query=${encodeURIComponent(eventLocation.text)}` : null; - const pageConfig = - (await getPageConfigBySlug('page-config-default', { locale: 'de' })) ?? - (await getPageConfigs({ locale: 'de', per_page: 1 }))[0] ?? - null; - const commentSlotRaw = - (pageConfig as { postCommentSlot?: string } | null)?.postCommentSlot ?? ''; - const commentSlot = commentSlotRaw - .replace(/\{\{PAGE_ID\}\}/g, slug) - .replace(/\{\{PAGE_URL\}\}/g, url.href) - .replace(/\{\{PAGE_TITLE\}\}/g, post.headline ?? post.linkName ?? slug); const showCommentSection = (post as { showCommentSection?: boolean }).showCommentSection !== false; const jsonLd: Record[] = []; @@ -263,7 +253,7 @@ export const load: PageServerLoad = async ({ params, locals, url, setHeaders }) eventLocationText: eventLocation?.text ?? null, mapEmbedUrl, mapLinkUrl, - commentSlot: showCommentSection ? commentSlot : '', + commentPageId: showCommentSection ? slug : null, translations, seoTitle: post.seoTitle ?? postHeadline, seoDescription: postDescription, diff --git a/src/routes/post/[slug]/+page.svelte b/src/routes/post/[slug]/+page.svelte index b13fbe1..49fac09 100644 --- a/src/routes/post/[slug]/+page.svelte +++ b/src/routes/post/[slug]/+page.svelte @@ -5,21 +5,13 @@ import EventBadges from '$lib/components/EventBadges.svelte'; import EventMap from '$lib/components/EventMap.svelte'; import Accordion from '$lib/components/Accordion.svelte'; + import CommentCountBadge from '$lib/components/CommentCountBadge.svelte'; + import Comments from '$lib/components/Comments.svelte'; import RustyImage from '$lib/components/RustyImage.svelte'; import { t, T } from '$lib/translations'; import type { PageData } from './$types'; let { data }: { data: PageData } = $props(); - - function executeScripts(node: HTMLElement) { - const scripts = node.querySelectorAll("script"); - scripts.forEach((old) => { - const s = document.createElement("script"); - for (const { name, value } of old.attributes) s.setAttribute(name, value); - s.text = old.textContent ?? ""; - old.replaceWith(s); - }); - } @@ -34,7 +26,7 @@
{#if data.rawPostImageUrl} -
+
{:else if data.postImageUrl} -
+
{/if}
- {#if data.postDate} + {#if data.postDate || data.commentPageId}
- + {#if data.postDate} + + {/if} + {#if data.commentPageId} + + {/if}
{/if}
@@ -148,10 +145,10 @@ /> {/if} -{#if data.commentSlot} - -
- {@html data.commentSlot} +{#if data.commentPageId} + +
+
{/if}