feat(post): post actions toolbar (share / copy / print / reading time)
Adds a small toolbar next to the comment-count badge with four actions: - Share — uses `navigator.share()` on supporting devices, falls back to copy-link otherwise - Copy link — clipboard API with `<input>` + `execCommand` fallback, flashes a checkmark for 1.5s on success - Print — `window.print()`, hidden via `print:hidden` so it doesn't show up in the printed output itself - Reading time — server-computed from headline+body HTML at ~200 wpm, hidden when 0 Reading time + canonical URL come from `+page.server.ts` so SSR output is identical and shareable URLs render correctly. All labels go through the translations layer (5 new `post_action_*` keys). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -236,6 +236,19 @@ export const load: PageServerLoad = async ({ params, locals, url, setHeaders })
|
||||
jsonLd.push(eventLd);
|
||||
}
|
||||
|
||||
// Reading-time estimate. ~200 words per minute for German non-fiction.
|
||||
// Strips HTML tags, then counts whitespace-separated tokens. Includes only
|
||||
// the prose body (headline + content); rows aren't part of the prose flow.
|
||||
const readingTimeMinutes = (() => {
|
||||
const plain = (contentHeadlineHtml + " " + contentBodyHtml)
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (!plain) return 0;
|
||||
const wordCount = plain.split(" ").length;
|
||||
return Math.max(1, Math.ceil(wordCount / 200));
|
||||
})();
|
||||
|
||||
return {
|
||||
post,
|
||||
preview: !!preview,
|
||||
@@ -254,6 +267,8 @@ export const load: PageServerLoad = async ({ params, locals, url, setHeaders })
|
||||
mapEmbedUrl,
|
||||
mapLinkUrl,
|
||||
commentPageId: showCommentSection ? slug : null,
|
||||
canonicalUrl: canonical,
|
||||
readingTimeMinutes,
|
||||
translations,
|
||||
seoTitle: post.seoTitle ?? postHeadline,
|
||||
seoDescription: postDescription,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import Accordion from "$lib/components/Accordion.svelte";
|
||||
import CommentCountBadge from "$lib/components/CommentCountBadge.svelte";
|
||||
import Comments from "$lib/components/Comments.svelte";
|
||||
import PostActions from "$lib/components/PostActions.svelte";
|
||||
import RustyImage from "$lib/components/RustyImage.svelte";
|
||||
import { t, T } from "$lib/translations";
|
||||
import type { PageData } from "./$types";
|
||||
@@ -76,13 +77,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-zinc-600">
|
||||
{#if data.commentPageId}
|
||||
<CommentCountBadge
|
||||
pageId={data.commentPageId}
|
||||
targetId="comments-section"
|
||||
/>
|
||||
{/if}
|
||||
<PostActions
|
||||
url={data.canonicalUrl}
|
||||
title={data.seoTitle}
|
||||
readingTimeMinutes={data.readingTimeMinutes}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<article
|
||||
|
||||
Reference in New Issue
Block a user