CalendarCompact:
- denser layout (smaller padding/font/badge), header icon size-8
- icon stein-900 (black), subtle wald gradient background
- rounded-sm to match design tokens (was rounded-lg)
- list items link to linkTo + "#event-<slug>" anchor; <svelte:element>
falls back to div when no linkTo is set
- not-prose on root so global content margins/list/link rules don't
apply to the card subtree
blog-utils.resolveCalendarBlocks:
- when items array is empty, fall back to the full calendar_item pool
(both default and compact variant get this) — saves editors from
re-listing items on every page.
app.css: add `:where(:not(.not-prose *))` to .content p/h1-h4,
main a (link colour + underline), main ul/ol so subtrees marked
not-prose stop inheriting the global prose-like defaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Calendar block now supports variant: "default" | "compact".
- default = full calendar (unchanged)
- compact = next N events (default 3) + link to a target page
New CalendarCompactBlock.svelte: small card with month/day badge,
title, date/time/location line, "Alle kommenden Termine →" footer
link resolved from the schema's linkTo reference. Filters to future
events (12h tolerance for running ones).
BlockRenderer dispatches by block.variant inside the calendar case.
Schema (cms_content/_types/core/calendar.json5) adds variant + linkTo
+ limit fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reads page.topImage first (new direct image field on the page schema);
falls back to legacy topFullwidthBanner.image during transition. Same
ResponsiveImage build, social-image transform and warm-up as before.
Banner-object still passed through so existing rendering keeps working
when headline/subheadline/variant are wanted; pure-image migration
needs no further code changes.
Schema + 21 pages migrated CMS-side in the same change set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pages now expose `socialImage` from the topFullwidthBanner field —
same 1200×630 cover transform with focal-aware crop that posts already
use. Layout meta-tag pipeline picks it up automatically; pages without
a banner still fall back to the global default social image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TextInput/Textarea size="sm": text-xs, tighter padding (h-9, px-2.5
py-1.5) so placeholder/value match label scale.
- Checkbox: new size prop (sm = h-4 w-4 + text-xs leading-snug).
Switched container to flex/items-start so multi-line labels align
with checkbox top instead of vertically centering and pushing the
box halfway down the paragraph.
- ContactForm: consent checkbox uses size="sm"; privacy note + error
hint at text-[0.7rem] leading-snug to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Global rule `main a, .markdown a { word-break: break-all }` broke
German words mid-character whenever the anchor wrapped non-URL content
(e.g. PostCard wraps title/excerpt in <a href>). Visible in PostOverview
as "schützt" → "sc / hützt", "überall" → "über / all".
Replaced with overflow-wrap: anywhere — still wraps long URLs softly,
but only when nothing else fits.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- blog-utils.resolvePostOverviewBlocks: parallel getPostBySlug via
Promise.all + .catch(null) instead of sequential await loop.
- blog-utils.resolveCalendarBlocks: same fix for calendar items.
- [...slug] page load: SearchableText/Calendar/DeadlineBanner now run
parallel after tagsMap (was strict waterfall).
- sanitize-blocks: LRU cache keyed on raw html input — sanitize-html is
~ms/KB, the same content repeats across pages and cache hits.
- cms.getNormalizedTranslations: WeakMap-memoised the per-request
Object.entries normalize loop on top of the existing 5-min bundle TTL.
- cms.cached: opt-in dev cache via DEV_CACHE_CMS=1 (default still off).
- rusty-image.ensureTransformedImage: was async-wrapping a sync URL
builder; made sync, removed awaits at call sites — saves microtasks.
- BlockRenderer: lazy-load StrommixBlock (~220 KB) via dynamic import
in the template — pages without the live-strommix widget no longer
ship its bundle.
Also: enabled Caddy zstd+gzip on windwiderstand.de vhost (server-side
edit; verified `content-encoding: zstd` on response).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New $lib/redirects.server.ts: lazy-loads GET /api/redirects, exact-match
index with trailing-slash normalisation, 60s TTL with background refresh
(cold start blocks once, subsequent requests get stale cache instantly).
hooks.server.ts: before legacy-redirect fallback, runs lookupRedirect for
GET requests. Editor-defined rules win over hardcoded /blog→/posts etc.
External targets (http://…) drop query; internal targets keep ?search.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
svelte-check (strict) flagged ContactFormComponent for not passing
children to <Button>. The component already renders label as fallback;
typed children as optional Snippet so callers don't need to pass it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SvelteKit already validates Origin at /api/contact; forwarding the
client Origin upstream caused the prod CMS to reject local dev
submissions (allowlist is prod-only). CMS forms plugin accepts
no-Origin requests when REQUIRE_ORIGIN=false.
Co-Authored-By: Claude Sonnet 4.6 <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>
ContentRows now delegates to BlockRenderer instead of maintaining its
own parallel if-else chain. Single source of truth for all block types.
Adds internal_component support (ContactFormComponent via /api/contact).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
InternalComponentBlock dispatches on `component` field value.
ContactFormComponent submits via /api/contact (SvelteKit proxy →
CMS /api/forms/kontakt/submit) with honeypot + client_age_ms.
BlockRenderer and block-types.ts updated accordingly.
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>
- /api/vorranggebiete.geojson + .kml endpoints (filter via ?id=W-XX)
- Download-Buttons in WindAreaPanel pro selektiertes Gebiet
- Hover-Tooltip auf Polygon zeigt Fläche, geplante Anlagen, Höhe, Gemeinden
- Favicon + apple-touch-icon dynamisch aus data.logoUrl (CMS-Logo)
- alte windrad-PNGs in static/favicons/ entfernt
- WindkarteBlock list-fetch: _limit statt limit (RustyCMS-Param)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Carousel: touch swipe support (40px threshold)
- OrganisationsBlock: 1.5 cards on mobile to hint at more content
- DeadlineBanner: left-aligned text, shrink-0 close button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Hide status badge for rechtskraeftig in WindAreaPanel
- Default hiddenStatuses=["rechtskraeftig"] so polygons start hidden
- Fix: call restyleAll after geojsonLayer init so initial hidden state applies
- Default hiddenRings=[] so 2km+5km buffer rings show on selection
- Remove entwurf_2_voraussichtlich from legend
- Lower maxZoom 12→11 for more context on feature select
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Icon already communicates file type. Size now sits below the icon
in a fixed-width left column. Ext text removed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reduce padding, smaller icon, filename + ext/size on one line.
Description hidden on mobile, truncated on desktop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getCmsFileUrl() returned /cms-files?src=... for all URLs including
external ones. resolveCmsSource() rejects foreign hosts (SSRF guard),
so external file links 404'd. External http(s) URLs now returned directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix bug: post page was overriding og:image with 150x200 thumbnail
- og:type = "article" for posts (was always "website")
- article:published_time, article:modified_time, article:author, article:tag
- og:image:alt + twitter:image:alt (post headline)
- twitter:label1/data1 with reading time
- Remove thumbnail fallback from socialImage (falls back to site logo instead)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace <details> with Svelte-controlled accordion + slide transition (180ms)
- Close all items on search/tag filter change
- Live tag counts per filter showing matches for current search query
- Visual empty state with reset button when no results
- focus-visible ring on all interactive elements (was focus: — shows on click too)
- Tag.svelte: remove ring-0, add focus-visible:ring-2
- Card.svelte: add focus-visible ring to linked cards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Portal the tooltip into document.body so overflow:hidden ancestors
can't clip it. Fixed positioning + getBoundingClientRect() for placement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switches from absolute (clipped by ancestor overflow:hidden) to fixed
with viewport-relative coords from getBoundingClientRect().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Green gradient header, white card with shadow, left-border accent on open
accordion items, hover-visible copy button in summary row, lighter dividers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>