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>
- 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>
Security:
- cms-images / cms-files now share resolveCmsSource (cms-source.server.ts)
which enforces an allowlist: asset filename, absolute path on the CMS
host, or full URL exact-matching the configured CMS host+protocol.
Foreign hosts, protocol-relative URLs and path traversal return 400.
- markdown-safe.ts is the single configured marked entrypoint; it
disables raw HTML rendering globally (renderer.html() returns ''),
removing the inline script/iframe injection vector via markdown.
All 8 marked imports across components and loaders now go through it.
- sanitize-blocks.server.ts walks resolved page/post content and runs
HtmlBlock html fields through sanitize-html with a tight allowlist
(no script/style/on*, only youtube/vimeo/spotify iframes, mailto/tel
plus http(s) schemes, target=_blank gets rel=noopener).
- Webhook tokens (revalidate, cache/clear) compared via
crypto.timingSafeEqual through auth-token.server.ts.
- cms.ts wraps every fetch in cmsFetch(): 5 s default timeout via
AbortSignal.timeout, override per call or globally via
PUBLIC_CMS_FETCH_TIMEOUT_MS. Stops slow CMS responses from pinning
SSR workers indefinitely.
Observability:
- log.server.ts gives logWarn / logError / logInfo with scope + context;
swallowed catch {} blocks in hooks, layout.server, +page.server,
[...slug]/+page.server and sitemap now log instead of going silent.
Routing:
- Legacy /blog, /blog/page/:n, /blog/tag/:t and /p/:slug stub routes
deleted; their 301 redirects moved into hooks.server.ts so the
request never enters the SvelteKit page pipeline.
Tests (vitest, server-side):
- auth-token.server.test.ts (6 cases for constant-time compare)
- cms-source.server.test.ts (8 cases for SSRF allowlist)
- sanitize-blocks.server.test.ts (9 cases for sanitizer + walker)
All 23 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`?preview_draft=<sid>` on any page/post route forwards the signed
session id to the CMS, which renders the in-memory draft instead of
the persisted entry. Layout listens for `rustycms:reload` postMessages
from the admin iframe parent and re-runs `invalidateAll()` to pick up
fresh draft state. CSP `frame-ancestors` is set per-request when
`PUBLIC_PREVIEW_PARENT_ORIGIN` is configured so the admin iframe can
embed the response.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adopt RustyCMS' new resolve semantics (_depth, _fields,
POST /_batch) where they pay off:
- +layout.server.ts: 4 sequential GETs (navHeader, navSocial,
footer, page_config) collapsed into one batchFetch() call.
Legacy "default" page_config slug stays as fallback Single-
Batch only when the canonical slug is missing.
- New getPageStubs() / getPostStubs() use _fields= so the
Slug→Label/Slug→URL maps don't pull body/rows. Typically
10-50× smaller payload, eligible for the same TTL cache.
- loadPostsList() now requests posts with _depth=2 and a
sparse fieldset that keeps card-level fields plus
postImage.* and postTag.{name,icon,color,_slug}. Body
Markdown stays out of the listing entirely.
- Page + post detail routes set _depth=3 as a cycle/blast-radius
cap (page → block → image → img reaches in three levels).
- cms.ts: ContentGetOptions gains depth/fields, getPageBySlug /
getPostBySlug / getPosts thread them through (cache key
extended). New batchFetch() / batchData<T>() helpers.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Added support for a preview mode in the CMS, allowing draft content to be viewed without being indexed.
- Updated hooks and server logic to handle preview parameters, adjusting cache control and robots tags accordingly.
- Enhanced layout to display a preview notification when in preview mode, improving user experience for content editors.
Auto-mode deadline_banner now resolves to the next future event across
all calendar_items in the CMS instead of requiring a manually-curated
items[] pool. Server load hooks (+page.server.ts for /, /[...slug],
/post/[slug]) call resolveDeadlineBannerBlocks which fetches the
global calendar_item list once and attaches it as items[] when mode
is "auto" and items[] is not already resolved. Component logic
unchanged — it still picks the soonest future entry.
Also:
- cms.ts: getCalendarItems() bulk list fetcher
- PostOverviewBlock.svelte: design defaults to "cards" when value is
empty string (not just null/undefined) so existing entries with
design: "" render
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Introduced keywords property in layout and page server files to enhance SEO capabilities.
- Updated relevant components to include keywords in meta tags for better search engine visibility.
- Changed site URLs in docker-compose and deployment configurations to remove "www" prefix for consistency.
- Improved image handling by introducing warmPostCardImages function for pre-fetching images in posts and tags.
- Enhanced PostCard component with responsive image sizes for better performance.
- Updated layout to include robots meta tag and JSON-LD structured data for improved SEO.
- Added loading state for images in TopBanner component to enhance user experience.
- Updated PostEntry type to include _rawImageUrl and _imageFocal for improved image management.
- Refactored image handling in various components to utilize the new properties, ensuring better focal point support and responsive image rendering.
- Introduced warmResponsiveImage function for pre-fetching image variants to optimize loading performance.
- Replaced CmsImage component with RustyImage in multiple blocks for consistent image processing.
- Added support for extracting focal points and alt text from CMS image fields.
- Introduced responsive image handling in TopBanner component, allowing for AVIF/WebP sources and LQIP placeholders.
- Updated image transformation functions to accommodate focal point parameters for better cropping.
- Refactored image URL resolution logic across various components and routes to utilize new image field structure.