diff --git a/frontend/src/lib/components/GeschichtenCard.svelte b/frontend/src/lib/components/GeschichtenCard.svelte
new file mode 100644
index 00000000..6b8a0d4c
--- /dev/null
+++ b/frontend/src/lib/components/GeschichtenCard.svelte
@@ -0,0 +1,87 @@
+
+
+{#if geschichten.length > 0}
+
+ {authorName(g)}
+ {#if formatPublishedDate(g)}· {formatPublishedDate(g)}{/if}
+ {plainExcerpt(g.body, 80)}
+ {m.geschichten_card_heading()}
+
+ {#if canWrite}
+
+ {m.geschichten_card_write_action()}
+
+ {/if}
+
+ {#each visible as g (g.id)}
+
+
+ {#if hasOverflow}
+
+ {/if}
+
Hello world
')).toBe('Hello world'); + }); + + it('strips nested HTML', () => { + expect(stripHtml('A
B
short
', 80)).toBe('short'); + }); + + it('truncates at the boundary with an ellipsis', () => { + const html = '' + 'a'.repeat(100) + '
'; + const out = plainExcerpt(html, 20); + expect(out.length).toBeLessThanOrEqual(21); // 20 chars + ellipsis + expect(out.endsWith('…')).toBe(true); + }); + + it('breaks at a word boundary when possible', () => { + const out = plainExcerpt('The quick brown fox jumps over
', 18); + expect(out).toBe('The quick brown…'); + }); +}); diff --git a/frontend/src/lib/utils/stripHtml.ts b/frontend/src/lib/utils/stripHtml.ts new file mode 100644 index 00000000..00ce62ff --- /dev/null +++ b/frontend/src/lib/utils/stripHtml.ts @@ -0,0 +1,23 @@ +/** + * Strip HTML tags from a string and return the plain text. + * Uses DOMParser in the browser, falls back to a regex strip on the server + * (where DOMParser is not available without isomorphic-dompurify's JSDOM). + */ +export function stripHtml(html: string | null | undefined): string { + if (!html) return ''; + if (typeof DOMParser === 'function') { + const doc = new DOMParser().parseFromString(html, 'text/html'); + return (doc.body.textContent ?? '').trim(); + } + return html.replace(/<[^>]*>/g, '').trim(); +} + +/** + * Strip HTML and truncate to a maximum length, appending an ellipsis when + * the source exceeds it. Used for editorial story excerpts. + */ +export function plainExcerpt(html: string | null | undefined, max = 80): string { + const text = stripHtml(html); + if (text.length <= max) return text; + return text.slice(0, max).replace(/\s+\S*$/, '') + '…'; +}