Adds AnnouncementBanner component fed by a new top_banner CMS entry
(site-announcement). Extended top_banner schema and TS types with
optional link + linkLabel fields. Rendered inline-markdown text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renders a single CMS entry as a block in isolation — no header, footer, or
preview banner. Lets us share a deep link to a single widget (e.g. the live
strommix gauge) for embedding or quick previews without exposing the rest of
the page.
- New BlockRenderer.svelte: switch over `_type` shared with ContentRows.
- Generic `getEntryBySlug(collection, slug, options)` in cms.ts so any
collection (live_strommix, deadline_banner, image, ...) is fetchable.
- Route group `(embed)/+layout@.svelte` resets the layout chain so no
Header/Footer/TopBanner inherit. `x-robots-tag: noindex`.
- Optional `?preview=<token>` for drafts. Cache: short s-maxage for live,
no-store for preview.
Co-Authored-By: Claude Opus 4.7 <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>
PostCard now shows a small comment-count pill in the top-left of the
image when count > 0. BlogOverview bulk-fetches counts for the visible
posts via the new `/api/comments/counts?ids=…` endpoint and passes
them down. One request per page render, regardless of card count.
- New helper `getCommentCounts()` in `cms.ts`
- `BlogOverview.svelte` derives slugs from posts, fires bulk fetch
in `$effect`, hands the result map to `PostCard`
- `PostCard.svelte` accepts `commentCount?: number | null`, renders
pill only when value is a positive number
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.
- Modified cached function to bypass caching in development mode.
- Updated fetchOpenApi function to return cached data only if not in development mode, ensuring accurate API fetching during development.
- Updated getCalendarItems function to include the _resolve parameter set to "all", ensuring all related data is fetched.
- Refined getHref function in DeadlineBannerBlock.svelte to simplify link handling by directly checking for the url property.
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>
trailingSlash='always' caused [...slug] rest-param to capture with
trailing slash. normalizePageSlug/normalizePostSlug only stripped
leading slash, so fallback match failed and pages 404'd.
Strip both leading and trailing slashes; normalize once at entry of
getPageBySlug/getPostBySlug.