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>
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>
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>
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>
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>
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>
`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>
`?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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
`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>
- 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>
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>
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>
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>
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).
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.