Wire a newsletter signup form into the CMS internal-component pattern
(form-newsletter). Posts to /api/newsletter which subscribes to listmonk
(double opt-in) and mirrors a best-effort backup record to the RustyCMS
forms plugin. Reuses the contact-form anti-spam stack: Cloudflare
Turnstile, honeypots, client-age check, origin validation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Client renders explicit Turnstile widget (flexible, light theme) when
PUBLIC_TURNSTILE_SITE_KEY is set. Token sent with form payload, server
verifies via siteverify endpoint before forwarding to CMS. Skipped
entirely when no site key configured (dev fallback). New i18n keys:
contact_captcha_pending, contact_server_error_captcha. Deploy workflow
forwards PUBLIC_TURNSTILE_SITE_KEY + TURNSTILE_SECRET.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jedes Windvorranggebiet kann jetzt 1–n text_fragment-Einträge als
Stellungnahme-Kriterien referenzieren. Das Panel lädt diese lazy beim
Öffnen und zeigt sie als aufklappbare Accordion-Sektion.
Außerdem: alle hardcodierten Strings im Karten-Block (Panel, Map,
Block) durch t()-Aufrufe ersetzt; 7 neue Translation-Keys ergänzt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keine hardcodierten Texte mehr — alle Strings über CMS translation_bundle
(app.json5). Keys in translations.ts registriert.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /api/kalender.ics SSR route: ICS feed of all calendar items (webcal-subscribable, Cache 1h)
- buildICSFeed() / downloadICSFeed() for multi-event ICS export
- googleCalUrl() deep-link pre-fills Google Calendar create form
- Web Share API per event (navigator.share on mobile, clipboard fallback on desktop)
— "Kopiert\!" toast per event for 2s after clipboard write
- Actions bar (subscribe + bulk export) only rendered after onMount
— Apple platforms: webcal:// link opens Calendar.app natively
— All others: direct ICS download via <a download>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Calendar widget stays stacked (calendar above events list)
- Grid widens to max-w-2xl with fixed h-10 cells (no aspect-square height growth)
- Gap between cells: 0.5 → 1.5
- Button system extracted to shared btn-* utility classes in app.css
- Replaced Google Calendar link + Google Maps with OSM
- Removed google icon from iconify subset
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New "hero" variant between compact and full: pct + status +
sparkline + trend, no sub-cards or accordion
- 24h Wind+Solar sparkline below hero pct, accent-colored
- Trend arrow vs previous hour with delta percentage points
- Polling pauses on tab hidden, refetches on visibility return
if last fetch >5min old
- Skeleton loader replaces "Lade…" text (matches final layout)
- Retry button on error state (silent vs explicit fetch)
- Stale-flag now prominent red badge in header bar
- Stand-time wrapped in <time datetime title> for full-date hover
- loadMw=0 falls back to last hourly slot instead of red error
- Negative-price banner replaces standalone price card (dedup)
- Production breakdown grid: clean auto/1fr label-value alignment
- Print CSS: details auto-open, chevrons hidden
- All German strings moved to translation keys
- STROMMIX_DEFAULT_THRESHOLDS const as single source for buckets
Includes data-block="LiveStrommix" attribute for the new
identifier scheme + new RenewableSparkline.svelte component.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add full/compact variants, prominent share, source badge, big
negative-price banner with conditional disclaimer, cards-style
context row, fade-in animation, and remove em-dashes from
visible strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two issues from the audit:
1. Hardcoded `lg:col-span-8 lg:col-start-3` collided with the layout
coming from `getBlockLayoutClasses(block.layout)`. Tailwind picked
`lg:col-start-3` from the hardcoded class (no opposing
`lg:col-start-1` in the layout output) so the widget started at
column 3 with span 12 → overflowed past the 12-col grid on
desktop, looking like it was "hanging right". Drop the hardcoded
constraints; the layout config from the CMS controls width
(mobile/tablet/desktop = 12 by default = full width).
2. Roughly 25 German strings were inline. Extracted them to
translation keys (T.strommix_*, registered in translations.ts) and
read via `useTranslate()` like ImageGalleryBlock /
PostOverviewBlock / YoutubeVideoBlock. Includes field labels for
the breakdown disclosure, source attribution variants, status
bucket markers, error / loading / fallback messages, and the
conventional formula text shown under the "Konventionell jetzt"
number.
Default DE strings live in cms_content/windwiderstand/translation_bundle/
app.json5 (separate CMS push).
CommentCountBadge was a separate component rendered next to PostActions
— two adjacent strips of buttons that should have been one toolbar.
Merges the count fetch + click-to-scroll behaviour into PostActions
behind `commentPageId` / `commentTargetId` props. The standalone
CommentCountBadge.svelte is now redundant; deleted.
Layout under the post hero is now a single row:
[💬 3] [↗ Teilen] [📋 Link] [🖨️] [⏱ 4 min]
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Pulls every German string out of Comments.svelte, PostCard.svelte and
CommentCountBadge.svelte and routes it through `useTranslate()` +
the `T.*` key registry. New keys (`comments_*`, `post_overview` count
aria, `comments_jump_to`) are registered in `lib/translations.ts`.
Default DE values land in
`cms_content/windwiderstand/de/translation_bundle/app.json5` (committed
separately) so the CMS-loaded bundle keeps the same look without each
component falling back to its raw key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Render block.title as <h3> above the grid when set; the carousel
variant is unchanged.
- Download CTA in the modal shrunk to a 9×9 icon-only button with
text-white/80 hover-tint instead of the dark green link colour the
app's `main a` rule was forcing in. Adds `no-underline` so the
global underline doesn't sneak back in.
- All previously hardcoded German strings (Download, Schließen,
modal aria, open aria) now go through translations.ts → CMS
translation_bundle. Cms_content app.json5 ships the matching keys.
Open follow-up: per-image title/alt/caption is empty on the existing
windwiderstand mediathek gallery → editor needs to fill those if
hover labels are wanted.