Commit Graph

13 Commits

Author SHA1 Message Date
Peter Meier ef163fb336 feat(layout): add global announcement banner above nav
Deploy / verify (push) Successful in 1m12s
Deploy / deploy (push) Successful in 1m6s
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>
2026-05-08 21:56:42 +02:00
Peter Meier 63a9721841 feat(embed): standalone widget route /widget/<collection>/<slug>
Deploy / verify (push) Successful in 51s
Deploy / deploy (push) Successful in 53s
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>
2026-05-05 09:37:21 +02:00
Peter Meier a2ca0db9ca feat(security+obs): SSRF allowlist, HTML sanitization, timing-safe tokens, fetch timeouts, structured logger
Deploy / verify (push) Successful in 56s
Deploy / deploy (push) Successful in 1m17s
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>
2026-05-04 16:09:25 +02:00
Peter Meier 00d42ebb42 feat(preview): consume rustycms live-preview drafts
Deploy / verify (push) Successful in 45s
Deploy / deploy (push) Successful in 1m1s
`?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>
2026-05-01 09:24:05 +02:00
Peter Meier 3bc7273e71 perf(cms): batch layout bootstrap, sparse fieldsets, depth caps
Deploy / verify (push) Successful in 40s
Deploy / deploy (push) Successful in 1m16s
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>
2026-04-30 22:00:10 +02:00
Peter Meier 8f791ebc54 feat(blog): comment count badge on post overview
Deploy / verify (push) Successful in 38s
Deploy / deploy (push) Successful in 59s
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>
2026-04-28 13:26:09 +02:00
Peter Meier 50bb0ef398 feat(preview): implement preview mode for content management
Deploy / verify (push) Successful in 41s
Deploy / deploy (push) Successful in 1m3s
- 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.
2026-04-23 11:11:27 +02:00
Peter Meier 4ec2aa10ec fix(cms): adjust caching behavior for development environment
Deploy / verify (push) Successful in 38s
Deploy / deploy (push) Failing after 57s
- 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.
2026-04-19 14:09:26 +02:00
Peter Meier 5f3a60003b fix(cms): add _resolve parameter to getCalendarItems for comprehensive data retrieval
Deploy / verify (push) Successful in 38s
Deploy / deploy (push) Successful in 56s
- 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.
2026-04-19 12:49:51 +02:00
Peter Meier 62d25202a6 feat(deadline-banner): auto mode pulls all calendar_items globally
Deploy / verify (push) Successful in 40s
Deploy / deploy (push) Successful in 59s
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>
2026-04-19 11:02:50 +02:00
Peter Meier 8dcfeb64c7 fix: strip trailing slash in normalize helpers
Deploy / verify (push) Successful in 36s
Deploy / deploy (push) Successful in 52s
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.
2026-04-17 22:50:21 +02:00
Peter Meier 5469b4c197 debug: log getPageBySlug resolution path (temp)
Deploy / verify (push) Successful in 37s
Deploy / deploy (push) Successful in 52s
2026-04-17 22:47:26 +02:00
Peter Meier 2fef91a548 Initial SvelteKit frontend port of windwiderstand.de
Deploy / verify (push) Failing after 32s
Deploy / deploy (push) Has been skipped
Full parity with Astro site: content rows, post/tag routes, pagination,
event badges + OSM map, comments, Live-Search via /api/search-index,
CMS image proxy, RSS, sitemap.

Deploy: Dockerfile + docker-compose.prod.yml + Gitea Actions workflow
to build + push to git.pm86.de registry and ssh-deploy to Contabo.
2026-04-17 22:01:30 +02:00