From 3d343822e9dadb04e7ab49cd532d29cc49ad0b03 Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Tue, 28 Apr 2026 15:29:15 +0200 Subject: [PATCH] feat: print-friendly layout + comment count on every post overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hides Header / HeaderOverlay / Footer / TopBanner / Breadcrumbs and the comment accordion when printing. Action toolbar already had `print:hidden`. - Adds @media print rules in app.css: 1.5cm page margins, 11pt body, forced black-on-white, expand link URLs after anchor text inside articles, keep images and headings together at page breaks. - PostCard now renders the count pill whenever a value is known (incl. zero); previously hidden for `count === 0`, so posts whose comments are still pending always looked empty. - Extracts the bulk-fetch helper to `lib/comment-counts.ts` (`fetchCommentCounts` + `postSlugForComments`) and uses it from both `BlogOverview` and `PostOverviewBlock`. Homepage / any page with a post_overview block now shows the badges too — previously only the paginated /posts/* routes did. Co-Authored-By: Claude Opus 4.7 --- src/app.css | 39 +++++++++++++++ src/lib/comment-counts.ts | 49 +++++++++++++++++++ src/lib/components/BlogOverview.svelte | 46 ++++------------- src/lib/components/PostCard.svelte | 2 +- .../blocks/PostOverviewBlock.svelte | 26 +++++++++- src/routes/+layout.svelte | 48 ++++++++++-------- src/routes/post/[slug]/+page.svelte | 22 +++++---- 7 files changed, 162 insertions(+), 70 deletions(-) create mode 100644 src/lib/comment-counts.ts diff --git a/src/app.css b/src/app.css index 0b2e5e8..5452a41 100644 --- a/src/app.css +++ b/src/app.css @@ -506,3 +506,42 @@ footer a:hover, border-left: 1px solid var(--color-stein-200); } } + +/* ── Print rules ──────────────────────────────────────────────────────────── + Goal: print only the article. `print:hidden` on chrome (header/footer/ + banner/breadcrumbs/comments) handles structural removal. The rules below + set page margins, force black-on-white, expand links, and keep images + inside the page break. */ +@media print { + @page { + margin: 1.5cm; + } + html, + body { + background: white \!important; + color: black \!important; + font-size: 11pt; + } + /* Show the URL after each anchor inside the article so printed pages stay + useful when the screen-only links are gone. */ + article a[href]:not([href^="#"])::after { + content: " (" attr(href) ")"; + font-size: 0.85em; + color: #555; + word-break: break-all; + } + /* Don't break inside images, keep figures together with their captions. */ + img, + figure { + break-inside: avoid; + max-width: 100% \!important; + height: auto \!important; + } + /* Keep headings with the next paragraph. */ + h1, + h2, + h3, + h4 { + break-after: avoid; + } +} diff --git a/src/lib/comment-counts.ts b/src/lib/comment-counts.ts new file mode 100644 index 0000000..963edd8 --- /dev/null +++ b/src/lib/comment-counts.ts @@ -0,0 +1,49 @@ +/** + * Helper for fetching comment counts in bulk and keying them by post slug. + * Used by overview components (BlogOverview, PostOverviewBlock) so each + * render fires a single `/api/comments/counts?ids=…` request instead of + * one request per card. + */ + +import { env } from "$env/dynamic/public"; +import type { PostEntry } from "$lib/cms"; + +export type CommentCountsMap = Record; + +/** Strip the leading slash so it matches `params.slug` (= comment page_id). */ +export function postSlugForComments(p: PostEntry): string { + const raw = (p as { slug?: string; _slug?: string }).slug ?? p._slug ?? ""; + return raw.replace(/^\//, "").trim(); +} + +/** + * Bulk-fetch approved comment counts for many posts. Returns a map + * `{ slug: count }` (slug as in `postSlugForComments`). Missing or failed + * responses resolve to an empty object — callers can then `?? null` per slug. + */ +export async function fetchCommentCounts( + slugs: string[], + options?: { environment?: string; signal?: AbortSignal }, +): Promise { + const cleaned = slugs.filter(Boolean); + if (cleaned.length === 0) return {}; + const cmsBase = ( + env.PUBLIC_CMS_URL || + import.meta.env.PUBLIC_CMS_URL || + "" + ).replace(/\/$/, ""); + if (!cmsBase) return {}; + const url = new URL(`${cmsBase}/api/comments/counts`); + url.searchParams.set("ids", cleaned.join(",")); + if (options?.environment) { + url.searchParams.set("_environment", options.environment); + } + try { + const r = await fetch(url.toString(), { signal: options?.signal }); + if (!r.ok) return {}; + const d = (await r.json()) as { counts?: CommentCountsMap }; + return d?.counts ?? {}; + } catch { + return {}; + } +} diff --git a/src/lib/components/BlogOverview.svelte b/src/lib/components/BlogOverview.svelte index 42f6fef..579bdcc 100644 --- a/src/lib/components/BlogOverview.svelte +++ b/src/lib/components/BlogOverview.svelte @@ -8,7 +8,7 @@ import { postHref } from "$lib/blog-utils"; import { t as tStatic, T } from "$lib/translations"; import type { Translations } from "$lib/translations"; - import { env } from "$env/dynamic/public"; + import { fetchCommentCounts, postSlugForComments, type CommentCountsMap } from "$lib/comment-counts"; interface TagEntry { _slug?: string; @@ -115,43 +115,15 @@ const totalCount = $derived(totalPosts); - // Bulk-fetch comment counts for the visible posts. Uses a single - // `/api/comments/counts` request keyed on the post slug. Failures keep the - // map empty so cards just don't render the badge. - let commentCounts = $state>({}); - - function postSlug(p: PostEntry): string { - const raw = (p as { slug?: string; _slug?: string }).slug ?? p._slug ?? ""; - return raw.replace(/^\//, "").trim(); - } - + // Bulk-fetch comment counts for the visible posts (single request per render). + let commentCounts = $state({}); $effect(() => { - const slugs = posts.map(postSlug).filter(Boolean); - if (slugs.length === 0) { - commentCounts = {}; - return; - } - const cmsBase = ( - env.PUBLIC_CMS_URL || - import.meta.env.PUBLIC_CMS_URL || - "" - ).replace(/\/$/, ""); - if (!cmsBase) return; - const url = new URL(`${cmsBase}/api/comments/counts`); - url.searchParams.set("ids", slugs.join(",")); - let cancelled = false; - fetch(url.toString()) - .then((r) => (r.ok ? r.json() : { counts: {} })) - .then((d) => { - if (cancelled) return; - commentCounts = (d?.counts ?? {}) as Record; - }) - .catch(() => { - if (!cancelled) commentCounts = {}; + const ctrl = new AbortController(); + fetchCommentCounts(posts.map(postSlugForComments), { signal: ctrl.signal }) + .then((map) => { + commentCounts = map; }); - return () => { - cancelled = true; - }; + return () => ctrl.abort(); }); @@ -298,7 +270,7 @@ post={post} href={postHref(post)} tagBase={`${basePath}/tag`} - commentCount={commentCounts[postSlug(post)] ?? null} + commentCount={commentCounts[postSlugForComments(post)] ?? null} /> {/each} diff --git a/src/lib/components/PostCard.svelte b/src/lib/components/PostCard.svelte index 9a43477..f4a734e 100644 --- a/src/lib/components/PostCard.svelte +++ b/src/lib/components/PostCard.svelte @@ -97,7 +97,7 @@ {/if} - {#if commentCount != null && commentCount > 0} + {#if commentCount != null}
0 || textHtml.length > 0); + + // Bulk-fetch comment counts for the visible posts (single request per render). + let commentCounts = $state({}); + $effect(() => { + const ctrl = new AbortController(); + fetchCommentCounts(posts.map(postSlugForComments), { signal: ctrl.signal }) + .then((map) => { + commentCounts = map; + }); + return () => ctrl.abort(); + }); {#if hasContent} @@ -42,13 +54,23 @@ {#if design === "list" && posts.length > 0}
{#each posts as post} - + {/each}
{:else if design === "cards" && posts.length > 0}
{#each posts as post} - + {/each}
{/if} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index a7303eb..73508c6 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -269,35 +269,43 @@
{/if} -
- +
+
+ +
{#if topBannerData} - +
+ +
{/if} -
-
+
+
{#if breadcrumbItems.length > 0} - +
+ +
{/if} {@render children()}
-
+
+
+
diff --git a/src/routes/post/[slug]/+page.svelte b/src/routes/post/[slug]/+page.svelte index 40d11ed..bc06ee7 100644 --- a/src/routes/post/[slug]/+page.svelte +++ b/src/routes/post/[slug]/+page.svelte @@ -165,14 +165,16 @@ {/if} {#if data.commentPageId} - -
- -
-
+
+ +
+ +
+
+
{/if}