Commit Graph

74 Commits

Author SHA1 Message Date
Peter Meier 615d9cdbd1 chore(tooling): add vitest + svelte-check, drop yarn.lock, CI gates, typed env imports
Toolchain hygiene:
  - svelte-check is now a pinned devDep (was npx-fetched per CI run)
  - vitest + coverage as devDeps; vitest.config.ts is separate from
    vite.config.ts so vite build does not pull vitest
  - $env stub for vitest (alias resolves $env/dynamic/{public,private}
    to a process.env proxy) — keeps server modules testable without
    booting SvelteKit
  - npm scripts: check / check:watch / typecheck / test / test:watch
  - drop yarn.lock + packageManager field; CI uses npm ci, single source
    of truth is package-lock.json
  - CI runs generate:icons + svelte-check + tsc --noEmit before deploy

Switch from raw process.env.X to $env/dynamic/{public,private} in
file-cache, image-cache, transform-cache and rusty-image.server. The
typed imports get tree-shaken correctly per server/client and fail
loudly at build time on accidental client-side use of private vars.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:09:02 +02:00
Peter Meier d0dede92fe perf(iconify): tree-shake MDI to used icons (-99.5% JSON, -71% client bundle)
The previous setup loaded the full @iconify-json/mdi collection (7000+
icons, 2.95 MB) just to render the ~30 icons we actually use, producing a
2.99 MB client chunk.

generate-icons.mjs scans src/ for static `icon="mdi:..."` references,
extracts a minimal IconifyJSON containing only those icons (plus an
EXTRA_ICONS list for dynamic CMS-driven names) from
@iconify-json/mdi/icons.json, and writes it to a generated subset file.
iconify-offline.ts now registers that subset instead of the full pack.

  build/client: 5.6 MB -> 1.6 MB (-71%)
  build/server: 5.3 MB -> 2.4 MB (-55%)
  largest client chunk: 2.99 MB -> 127 KB (-96%)
  iconify subset: 2.95 MB -> 15.5 KB (-99.5%)

Hooked into predev/prebuild so the subset stays fresh; missing icons
fall back to the iconify API at runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:08:47 +02:00
Peter Meier 56084ac542 feat(maintenance): static fallback when the CMS is unreachable
Deploy / verify (push) Successful in 43s
Deploy / deploy (push) Successful in 1m4s
Adds a self-contained maintenance page that the SvelteKit handle hook
serves before the first CMS call when `isCmsAvailable()` returns
false. The probe hits `/api-docs/openapi.json` with a short timeout
and caches the result (10s healthy, 3s unhealthy) so we don't pay a
round-trip per request. `/maintenance` and `?cms_down=1` simulate
the page without taking the CMS down — useful for previewing the
copy.

The maintenance HTML is fully inline (no CMS calls, no layout, no
external CSS) so it survives even when every dependency is down.
Returned with 503 + Retry-After:60 + noindex headers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 14:47:15 +02:00
Peter Meier 1e9ef44b52 feat(header): highlight active nav link with bold + underline
Deploy / verify (push) Successful in 45s
Deploy / deploy (push) Successful in 1m9s
Compares the link href against `$page.url.pathname` (with trailing-slash
normalization + subpath match), applies `font-bold underline` and
`aria-current="page"` on a match. Wired into both the desktop horizontal
nav and the mobile slide-down menu.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 11:29:05 +02:00
Peter Meier 5cf141fa4a chore(blog-overview): apply prettier formatting
No behavior change — IDE auto-format pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 11:29:00 +02:00
Peter Meier 616fb66c00 fix(event-badges): suppress parent-link underline on hover
When EventBadges sits inside a `<a hover:underline>` (upcoming-events
list, post cards) the underline propagated through the badge spans on
hover. Force `text-decoration: none` on the date and location pills so
only the headline text underlines.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 10:07:56 +02:00
Peter Meier 4db2fd8562 fix(blog-overview): use EventBadges in upcoming-events list
The plain `<time>` was visually weak next to the headline. Reuse the
`EventBadges` component from the post card so the date renders as a
proper wald-tinted pill — same shape as event posts. Drops the local
`formatEventDate` since EventBadges does the localization itself
(weekday + time included, matching post cards).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 10:06:41 +02:00
Peter Meier dcce4fdea1 fix(blog-overview): upcoming-events block escapes prose ul-bullet rule
Deploy / verify (push) Successful in 46s
Deploy / deploy (push) Successful in 1m7s
`main ul:not(.not-prose) li` injects a disc bullet on every list under
`main`, which the upcoming-events list inherited. Mark the ul `not-prose`
+ `list-none` so the rule doesn't match. Also bump the calendar-clock
icon from `h-4 w-4` to `size-5` so it visually matches the heading
ascender.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 10:03:47 +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 35f7aa8f90 feat(blocks): YoutubeVideoGalleryBlock (carousel + grid)
Deploy / verify (push) Successful in 2m19s
Deploy / deploy (push) Successful in 1m7s
New block for `youtube_video_gallery` CMS type. Carousel renders
2 iframes side-by-side on md+ (1 on mobile) with prev/next/dots; grid
shows YT thumbnails opening modal embeds. Description supports
markdown via marked; long URLs break with overflow-wrap:anywhere.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 08:31:38 +02:00
Peter Meier 3d343822e9 feat: print-friendly layout + comment count on every post overview
Deploy / verify (push) Successful in 42s
Deploy / deploy (push) Successful in 1m4s
- Hides Header / HeaderOverlay / Footer / TopBanner / Breadcrumbs and
  the comment accordion when printing. Action toolbar already had
  `print:hidden`.
- Adds @media print rules in app.css: 1.5cm page margins, 11pt body,
  forced black-on-white, expand link URLs after anchor text inside
  articles, keep images and headings together at page breaks.
- PostCard now renders the count pill whenever a value is known (incl.
  zero); previously hidden for `count === 0`, so posts whose comments
  are still pending always looked empty.
- Extracts the bulk-fetch helper to `lib/comment-counts.ts`
  (`fetchCommentCounts` + `postSlugForComments`) and uses it from both
  `BlogOverview` and `PostOverviewBlock`. Homepage / any page with a
  post_overview block now shows the badges too — previously only the
  paginated /posts/* routes did.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 15:29:15 +02:00
Peter Meier 6f14e4f150 style(post): polish post header + actions toolbar
Deploy / verify (push) Successful in 43s
Deploy / deploy (push) Successful in 1m4s
- PostActions: every item is a rounded-full pill with subtle border;
  the three icon-only actions (share/copy/print) cluster in one
  segmented control with divider lines, count + reading-time stand
  alone left and right
- Share button only renders when `navigator.share` is available so
  desktop doesn't get a duplicate of "Copy link"
- All action buttons drop their inline labels and use `title=` +
  `aria-label` for tooltip-only access; copy still flips icon to a
  checkmark for 1.5s on success
- Empty comment state stacks vertically on mobile (icon above), goes
  horizontal + left-aligned on ≥sm
- Post header rebuilt: image fixed at 150x150 (no more mobile blow-up),
  right column is `flex flex-col gap-3` so date/tags/actions share one
  rhythm instead of ad-hoc margins
- Reading-time now also walks `row*Content` and pulls `content`/`body`/
  `text` strings — Termin-Posts with empty body but markdown rows now
  show a meaningful estimate

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 15:08:04 +02:00
Peter Meier 6b2b7245f4 refactor: fold comment count into PostActions toolbar
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>
2026-04-28 14:52:30 +02:00
Peter Meier 3318fe9e17 feat(post): post actions toolbar (share / copy / print / reading time)
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>
2026-04-28 14:42:16 +02:00
Peter Meier e4804ce673 i18n(comments): all UI strings through translations
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>
2026-04-28 14:34:48 +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 8f45094f64 style(comments): move count badge under image, drop underline
Deploy / verify (push) Successful in 43s
Deploy / deploy (push) Successful in 1m3s
Repositions the CommentCountBadge out of the date row into its own
container below the post image. Removes hover colour-change + the
prose underline (`no-underline\!`) so the badge sits flat with the
post chrome.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 13:21:05 +02:00
Peter Meier 40b2d406ae fix(rusty-image): derive arValue from arString for layout match
`arValue` (height computation) and `arString` (URL `ar=` param) used
to read from different sources, so the URL could request a different
aspect than the `<img>` element reserved — caused a visible flicker
when the transformed image loaded. Both now derive from `arString`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 13:20:59 +02:00
Peter Meier 9d1a7ab46d feat(comments): polish UI + send client_age_ms
- Avatars with hash-derived hue, initials per author
- Relative dates (Intl.RelativeTimeFormat), absolute as title
- localStorage remembers author name for next visit
- Auto-resize textarea, char counter, Cmd+Enter submit
- Skeleton loading state, icon empty state, retry button on error
- Pending comments get amber card + status pill (visible only to author)
- Reply count in toggle ("3 Antworten")
- Edit/delete buttons fade in on hover (group-hover)
- DRY: meta + body rendering as snippets, used by top-level + replies
- Sends `client_age_ms` for backend's anti-instant-bot check

No content filtering — only timing / structure / re-use checks live in
the backend now (sha256 dedupe, session cooldown, banned IPs, author
char sanity).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 13:18:23 +02:00
Peter Meier 817f658977 fix(comments): inline repliesOf() — {@const} placement
Deploy / verify (push) Successful in 47s
Deploy / deploy (push) Successful in 1m3s
Svelte rejected `{@const replies = repliesOf(c.id)}` because it sat
inside `<li>` rather than directly under `{#each}`. The Gitea deploy
typecheck (`svelte-check --threshold error`) failed on this, leaving
the previous Cusdis bundle live. Inlining the call costs one extra
filter per render — fine here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 11:49:27 +02:00
Peter Meier febc027e6b feat: replace Cusdis with own comment plugin
Deploy / verify (push) Failing after 45s
Deploy / deploy (push) Has been skipped
Drops the Cusdis embed (postCommentSlot in pageConfig) in favour of the
new RustyCMS comment plugin. New Comments.svelte renders the thread,
form, replies and inline edit/delete via /api/comments. Count-badge near
the post date links to the accordion and uses /api/comments/:id/count.

Accordion gets a `lazy` mode so the comment iframe / fetch happens only
on first open (avoids the previous "empty on first render" issue with
hidden details).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 11:17:10 +02:00
Peter Meier 9b4191151c docs(images): correct misleading AVIF default in RustyImage / TopBanner
Deploy / verify (push) Successful in 41s
Deploy / deploy (push) Successful in 1m5s
The runtime default has been `["webp", "jpeg"]` for a while, but the doc
comments still referenced AVIF. Update them and add the rationale (server-side
AVIF encoding is 5-10× slower than WebP for marginal bandwidth gain).

No runtime change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 18:56:39 +02:00
Peter Meier 354d3f09d3 feat(youtube): playlist support + URL parsing in youtube_video block
Deploy / verify (push) Successful in 42s
Deploy / deploy (push) Successful in 1m5s
YoutubeVideoBlockData gains an optional `playlistId`. The block now
accepts either a video, a playlist, or both, and either field can be
filled with a raw ID *or* a full youtube.com / youtu.be URL — a
helper extracts the ?v= and ?list= parameters automatically.

Embed routing:
- video only           → /embed/<id>?rel=0
- playlist only        → /embed/videoseries?list=<plid>&rel=0
- video + playlist     → /embed/<id>?list=<plid>&rel=0
- block.params merged  via URLSearchParams

Also tweaks ImageGalleryBlock (grid variant): green padding/contain
fit, darker caption gradient, smaller left-aligned label that expands
on hover. Hides post_overview block when no posts and no intro text
(prevents a bare heading on /mediathek).
2026-04-25 13:15:11 +02:00
Peter Meier 942b9feefa feat(files-block): icons for tsv/json/xml/py/js/ts/rs/sh
Deploy / verify (push) Successful in 39s
Deploy / deploy (push) Successful in 59s
iconForExt now matches the backend's expanded ALLOWED_EXTENSIONS:
- tsv routes to the excel-box icon (alongside csv).
- json → mdi:code-json, xml/html → mdi:xml.
- py → mdi:language-python; js/ts/jsx/tsx → language-javascript;
  rs → language-rust; sh/bash → console.

Visitors who download a CMS-hosted dataset, source dump or archive
now see a meaningful icon instead of the generic file-outline
fallback.
2026-04-25 11:21:40 +02:00
Peter Meier 9eab404240 fix(post-overview): hide block entirely when no posts + no intro text
Deploy / verify (push) Successful in 38s
Deploy / deploy (push) Successful in 59s
The post_overview block kept rendering its <h3> headline + the
"all posts" footer link even when both `postsResolved` was empty
and `text` was unset, leaving a heading with nothing under it on
pages like /mediathek that include the block defensively.

Wrap the whole render in `{#if hasContent}` so empty overviews
disappear; the heading reappears as soon as posts or intro text
are present.
2026-04-25 10:54:05 +02:00
Peter Meier 998fba8904 feat(gallery): grid title, subtle download icon, full i18n
Deploy / verify (push) Successful in 38s
Deploy / deploy (push) Successful in 59s
- 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.
2026-04-25 10:44:30 +02:00
Peter Meier 9946061105 fix(css): global main ul/ol bullet rules respect .not-prose opt-out
Deploy / verify (push) Successful in 38s
Deploy / deploy (push) Successful in 58s
The global typography rules in app.css (main ul li → list-style: disc)
ran independently of Tailwind Typography, so `not-prose` on the
gallery grid couldn't suppress the bullets. Adding :not(.not-prose)
to those selectors makes the same opt-out class work for both
prose-managed and globally-styled lists, so the gallery grid renders
without disc markers and any future grid-of-list use case can opt out
the same way.
2026-04-25 10:34:58 +02:00
Peter Meier 1154be3c25 fix(gallery): use not-prose on grid <ul> to escape Typography styles
Deploy / verify (push) Successful in 40s
Deploy / deploy (push) Successful in 1m1s
The Tailwind Typography (`prose prose-zinc`) wrapper applied higher in
the page hierarchy injects `.prose ul` rules — list-style: disc, an
indent, and ::marker color — at a higher specificity than single-class
utilities, so list-none/p-0/m-0 silently lost.

`not-prose` is the official escape hatch from the typography plugin
and zaps every prose rule on the element plus its descendants. One
class instead of three \!important overrides.
2026-04-25 10:34:06 +02:00
Peter Meier 6246d7c3a4 feat(gallery): grid variant with hover labels + download modal
Deploy / verify (push) Successful in 40s
Deploy / deploy (push) Successful in 1m1s
ImageGalleryBlock now branches on `block.variant`:

- "standard" (default) → existing carousel.
- "grid" → 2/3/4-column thumbnail grid (1:1 squares, w=400/q=75
  through /cms-images). Each tile scales 5% on hover and surfaces
  the image's title/alt/caption/description as a gradient overlay
  caption that fades in on hover and keyboard focus.

Click any tile → fullscreen lightbox (z-100 overlay + 90vh image).
The lightbox carries:
- A download button using the asset's filename (extracted from the
  src URL).
- Prev/Next arrows when there's more than one image (keyboard:
  ←/→), Esc to close, click on the backdrop closes too.
- Position counter (n/total) when applicable.

Type updates in block-types.ts: ImageGalleryBlockData gains
`variant?: "standard"|"grid"` and `title?`. ImageGalleryImage gains
`alt`/`caption`/`title` so the schema's `image` field shape is
honoured for the hover label.
2026-04-25 10:32:37 +02:00
Peter Meier ecff7b0bbf feat(images): cache-bust /cms-images URLs by post._updated
Deploy / verify (push) Successful in 40s
Deploy / deploy (push) Successful in 1m1s
Browser caches /cms-images responses immutably for a year. When the
editor changes a focal point or alt text, the underlying transform
output changes too, but the URL stays the same — so the browser keeps
serving the previous bytes. Manual hard-reloads and /api/cache/clear
help once but not for visitors who already have the file.

- RustyImageTransformParams gains an optional `version` that
  serializes as `_v` in the /cms-images URL.
- RustyImage.svelte takes a matching `version` prop and forwards it
  through `baseParams`, so every <picture> source carries the tag.
- The post thumbnail passes `post._updated` (truncated to seconds)
  as version. Each save bumps `_updated`, the URL changes, browsers
  re-fetch.

`_v` is intentionally not consumed by `/cms-images` — its sole job is
to break the browser cache key. The server-side image-cache and the
RustyCMS transform cache key both ignore it, so disk usage doesn't
balloon when the version tag changes.
2026-04-25 09:43:34 +02:00
Peter Meier bb9df1f9bd fix(post): align microformat block with PostEntry types for svelte-check
Deploy / verify (push) Successful in 37s
Deploy / deploy (push) Successful in 1m0s
Three CI typecheck failures from the previous microformat patch:

- post.created is the only date field in the schema (no _created on the
  generated PostEntry). Drop the _created fallback.
- postTag items can be a slug string OR a tag object. Type narrowing
  with `'name' in tag` widened to `never`; pull the name through an
  explicit `{ name?: string }` cast in @const so the iteration stays
  honest about the runtime shape.
- author isn't on PostEntry either; cast at the use-site for the
  hidden h-card author block.
2026-04-25 09:22:18 +02:00
Peter Meier 1ff4dc7a9a feat(api): POST /api/cache/clear + flush images on asset webhooks
Deploy / verify (push) Failing after 38s
Deploy / deploy (push) Has been skipped
Two changes:
- New POST /api/cache/clear (auth: REVALIDATE_TOKEN). Wipes the
  TTL content cache, the image disk + memory cache, and forwards to
  the RustyCMS /api/transform/cache/invalidate endpoint when an
  API key is configured. Single trigger to flush everything.
- /api/revalidate now also calls clearImageCache() on asset.* events
  so a re-upload (focal change, alt edit, replacement) doesn't keep
  serving the previous transformed image from disk.

clearImageCache() in image-cache.server.ts wipes the in-memory map
and removes every file under IMAGE_CACHE_DIR.
2026-04-25 09:20:48 +02:00
Peter Meier 7264ad6707 feat(post): emit VideoObject JSON-LD for embedded YouTube videos
Deploy / verify (push) Failing after 38s
Deploy / deploy (push) Has been skipped
Posts whose row*Content contains a `youtube_video` block now ship a
schema.org VideoObject per video alongside the existing BlogPosting /
Event JSON-LD. Lets Google attach a video rich-result to the post URL
(thumbnail, duration where available, embed/content URLs) instead of
treating the page as plain text whose body happens to be a single
iframe.

Thumbnail uses YouTube's predictable maxresdefault + hqdefault URLs;
upload date falls back to post.created when the embed has none.
2026-04-25 09:16:33 +02:00
Peter Meier 5b82d37be4 feat(post): add h-entry microformat + tag-name keywords fallback
Deploy / verify (push) Failing after 44s
Deploy / deploy (push) Has been skipped
Microformats:
- Wrap the post <article> in h-entry with p-name on the headline,
  p-summary on the subheadline (or sr-only fallback from excerpt),
  e-content on the rendered body, dt-published on the created date
  (hidden time element) and an invisible p-author h-card. Tag names
  are mirrored as hidden p-category entries so crawlers see them
  even though the visual Tags component already links them.
- itemscope/itemtype maps to schema.org BlogPosting (or Event when
  `isEvent`), aligning the invisible microdata with the existing
  JSON-LD.

Keywords:
- When post.seoKeywords is empty, fall back to the comma-joined tag
  names so <meta name="keywords"> still ships something meaningful.
2026-04-24 11:04:14 +02:00
Peter Meier 36288befe1 fix(post): render post.headline/subheadline when markdown body has no H1
Deploy / verify (push) Successful in 49s
Deploy / deploy (push) Successful in 1m6s
The article template only rendered the H1 it could pluck from the
markdown body (`contentHeadlineHtml`). Posts whose body opens with H2
(or no heading) silently lost both the schema-level `headline` and
`subheadline` — they were used in SEO/JSON-LD/breadcrumb only.

Fall back to the structured fields when no inline H1 is present.
2026-04-24 08:30:50 +02:00
Peter Meier 724b75b6bf feat(cms-images): forward dpr + format=auto to RustyCMS
Deploy / verify (push) Successful in 40s
Deploy / deploy (push) Successful in 1m2s
- Accept dpr query param (clamped 1..4) and include it in cache key so
  each density gets its own cached file.
- Accept format=auto and negotiate locally from the client's Accept
  header (avif > webp > jpeg) before building the cache key — concrete
  formats in the cache avoid cross-client leaks.
2026-04-23 13:36:38 +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 9216138a8c style(FilesBlock): enhance list styling for improved presentation
Deploy / verify (push) Successful in 40s
Deploy / deploy (push) Successful in 1m0s
- Updated the list styling in FilesBlock component to remove default list styles and improve layout consistency.
- Adjusted list item classes for better alignment and visual clarity, enhancing the overall user experience.
2026-04-22 10:51:39 +02:00
Peter Meier 5b5ec7c659 feat: add FilesBlock interface and component integration
Deploy / verify (push) Successful in 46s
Deploy / deploy (push) Successful in 1m8s
- Introduced FilesBlockItem and FilesBlockData interfaces to support file handling in the CMS.
- Integrated FilesBlock component into ContentRows.svelte for rendering file blocks.
- Updated block type handling to include support for files, enhancing content versatility.
2026-04-22 10:23:20 +02:00
Peter Meier bedfe597ab style: update typography and enhance OrganisationsBlock layout
Deploy / verify (push) Successful in 41s
Deploy / deploy (push) Successful in 1m0s
- Changed comment in app.css to reflect German terminology for quotes.
- Added Button component to OrganisationsBlock for improved call-to-action presentation.
- Adjusted button styling and layout in OrganisationsBlock for better responsiveness and visual consistency.
2026-04-21 13:39:57 +02:00
Peter Meier 47cca3abc6 chore: update package.json and yarn.lock for dependency management
Deploy / verify (push) Successful in 48s
Deploy / deploy (push) Successful in 1m15s
- Added `packageManager` field to package.json for Yarn versioning.
- Updated yarn.lock to include new dependencies for `@emnapi` and `@esbuild` packages, enhancing compatibility and performance.
- Adjusted styling in TopBanner.svelte for improved layout and responsiveness.
2026-04-21 13:16:55 +02:00
Peter Meier 6794d41d8d chore: update package-lock and yarn.lock to include class-variance-authority
Deploy / verify (push) Successful in 50s
Deploy / deploy (push) Successful in 1m13s
- Added `class-variance-authority` version 0.7.1 to both package-lock.json and yarn.lock for enhanced styling capabilities.
- Removed outdated entries from yarn.lock to streamline dependencies and improve project maintainability.
2026-04-20 23:16:07 +02:00
Peter Meier d327798ce6 feat: add class-variance-authority for card styling and enhance color palettes
Deploy / verify (push) Failing after 1m40s
Deploy / deploy (push) Has been skipped
- Introduced `class-variance-authority` to manage card component styles, allowing for flexible variant and layout options.
- Added a new color palette for "Fire" to the CSS for improved theming.
- Updated `Card` and `PostCard` components to utilize the new styling system, enhancing visual consistency and responsiveness.
- Refined layout and styling in various components for better user experience.
2026-04-20 22:09:43 +02:00
Peter Meier a18c0a8cf7 feat(post-page): separate headline and body content for improved rendering
Deploy / verify (push) Successful in 45s
Deploy / deploy (push) Successful in 59s
- Extracted headline and body content from the post's HTML, allowing for distinct rendering of the headline and body in the post page.
- Updated the Svelte component to display the extracted headline and body content separately, enhancing the layout and readability of posts.
2026-04-19 21:56:30 +02:00
Peter Meier 2d0a6a7747 style(PostCard): update headline styling for improved readability
Deploy / verify (push) Successful in 39s
Deploy / deploy (push) Failing after 1m10s
- Changed headline class from `line-clamp-2` to `truncate` and adjusted font size to `text-sm`, enhancing the visual presentation and readability of post headlines.
2026-04-19 14:29:45 +02:00
Peter Meier 2ee451f4ca feat(post-overview): add excludeTag functionality for filtering posts
Deploy / verify (push) Successful in 39s
Deploy / deploy (push) Failing after 1m7s
- Introduced `excludeTag` property in PostOverviewBlockData interface to allow exclusion of specific tag slugs.
- Implemented `excludePostsByTagSlugs` function to filter out posts containing specified tag slugs.
- Updated `resolvePostOverviewBlocks` function to utilize the new exclusion logic, enhancing post filtering capabilities.
2026-04-19 14:18:50 +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 2e0601e178 feat(post-page): implement script execution for dynamic content rendering
Deploy / verify (push) Successful in 42s
Deploy / deploy (push) Successful in 1m11s
- Added `executeScripts` function to handle script elements within the comment slot, ensuring proper execution of inline scripts.
- Updated the comment slot container to utilize the new script execution functionality, enhancing dynamic content handling.
2026-04-19 13:52:29 +02:00
Peter Meier 1d75610cd3 feat(organisations-block): enhance OrganisationsBlock with carousel presentation option and improved layout
Deploy / verify (push) Successful in 37s
Deploy / deploy (push) Successful in 58s
- Added "carousel" option to the presentation property in OrganisationsBlockData for horizontal slider functionality.
- Updated OrganisationsBlock.svelte to support carousel layout with smooth scrolling feature.
- Refined Badge component styling for better visual consistency.
- Improved overall structure and responsiveness of the organisations display.
2026-04-19 13:13:12 +02:00