v2.3.0
Added
- UI / Storyblok-ui: a badge primitive and a matching CMS blok.
Badgerenders a single external badge image (a shields.io SVG, say) at its natural size with a configurable height and an optional link;BadgeGrouplays badges out in a wrapping row;Badgestakes anitemsarray and renders the whole row in one call. Thebadgesblok (holdingbadge_itemchildren) exposes vertical alignment, row alignment and badge height on top of the standard spacing tab, and is allowed insidesection. Documented in Storybook underDisplay/Badges. - UI: a brutalist text-selection marker — selected text now carries the site's own highlight treatment instead of the browser default.
Fixed
-
Storyblok-richtext: images inside a richtext field no longer break hydration. A richtext image can be nested in a paragraph, and the old renderer wrapped it in a block
<div>— invalid inside the resulting<p>, which the browser reparents, producing a React #418 hydration error. Images render as phrasing-content<img>now, and keep their natural aspect ratio instead of being forced into a 16:9 crop. -
Storyblok-utils:
getProcessedImageno longer leaks the crop mode into the focal filter — a crop like1200x630/smartproduced an invalidfocal(…:1200x630/smart)coordinate for any story with a focal point set, silently degrading Open Graph and RSS images. Regression test added. -
Portfolio: the branded ASCII 404 screen actually renders now. It lived at the
(portfolio)group boundary, where the[...slug]catch-all's own plainnot-foundboundary always shadowed it; the branded screen moved into the catch-all boundary and the shadowed file is gone. -
Storyblok-sync: component and group lookups fail loudly instead of swallowing errors — a transient Management API failure used to make
upsertBlockre-create (duplicate) every existing component, andsync-component-groupshad the same duplicate-creating swallow. Group UUIDs are fetched once per run instead of once per group name,sync-componentsexits non-zero when any block fails instead of reporting success, and malformed list responses are rejected instead of being treated as empty (which also created duplicates). -
UI:
formatYearguards invalid dates (no more"NaN") and uses UTC so date-only strings can't shift a year across timezones; the drifted duplicate inog-work-metanow delegates to the shared helper. External projects show the external-link icon in both the desktop nav and the mobile menu, matching the websites column, and the desktop header'swww.httpjpg.comtext is now an actual home link (previously the only/link in the header was mobile-only). -
Portfolio: the dev-mode fallback for unknown bloks works again —
@storyblok/reactv7 ignores a_fallbackregistry key, soSbMissingis now wired viaenableFallbackComponent/customFallbackComponent. -
Spotify / Now-playing:
useNowPlayingignores in-flight responses after unmount or an endpoint change, andNowPlayingcancels color extraction and its settle timeout on cleanup — no more stale state or setState-after-unmount warnings. -
Portfolio: the Discord API route returns a distinct 503 when the Storyblok config story cannot be loaded, instead of blaming the CMS content for a fetch failure.
-
Observability: server and edge Sentry init guard like the client — skipped without a DSN, when disabled, or when a client is already initialized.
Changed
- CMS: image widths use an even 10%-step scale (
10%…100%) instead of the previous hand-picked5/25/33/50/65/75/100. The old scale mixed quarters, thirds and one-off values, so the editor offered uneven jumps. Values outside the new scale are dropped rather than aliased: any story still holding5%,25%,33%,65%or75%loses that width and falls back to the layout default, so re-check those images after syncing. - Storyblok-ui:
SbImageandSbScrollClipImagerender their caption only when the richtext document has content, matchingSbVideo. - UI: the work-card description clamp uses Panda's
lineClamputility instead of a hand-rolled-webkit-boxstyle that needed anas anycast.
Removed
- Portfolio: the guessed fallback navigation (
Home/CV/Feed, with a/feed-xml_htmllink that 404ed and a/cvlink that only resolved if such a story happened to exist). The header menu now mirrors the Storyblok config exactly — when the config story is unpublished or the menu is empty, no menu links render and the header's built-in home link keeps the site navigable. - Dead code across the workspace, each confirmed unused by a repo-wide reference audit: the orphaned
@httpjpg/uistyles/globalmodule and subpath,getAllSlugs(storyblok-api),hasMediaConsentplus theOPEN_COOKIE_SETTINGS_EVENTre-export and./bannersubpath (consent), the./google/./umamisubpath barrels (analytics), the root and./sentrybarrels (observability), the unregisteredSbCaptionre-export andFooterConfigalias (storyblok-ui),OverlayPatternName, the phantomImagePreviewheightprop anduseNowPlaying'srefetch(ui/spotify), unused Storybook option arrays, fixtures and theaddon-linksaddon, unused Studio option constants, the dead Spotify OAuth callback route, the unusedconfig.uitheme block, and the legacytextblok registry alias. - Env:
DISCORD_USER_ID(the Storyblok config story is the source of truth) and the CI-onlySENTRY_AUTH_TOKEN/SENTRY_ORG/SENTRY_PROJECTdropped from the runtime env contract,turbo.json, and.env.example. - Music player: the no-op
customsource removed from the UI type, the CMS schema, and Storybook — it rendered nothing; runsync:componentsto update the space.
Performance
- Spotify: the API access token is cached per credential set until shortly before
expires_in(with in-flight request dedupe), cutting the ~10s now-playing poll from two Spotify requests to one; failures are never cached, and a 401 on a cached token drops the cache and retries once with a fresh token so a revoked token can't pin the widget to errors.
Tooling
- CI:
apps/studiojoins thebuild-appsmatrix — it was typechecked and linted but never built, sonext buildregressions shipped uncaught. - Docs:
apps/studiodocumented in the CLAUDE.md architecture overview.
Dependencies
- Runtime:
framer-motionreplaced by its successor packagemotion; Swiper migrated to v14;sharpto ^0.35. - Build & styling: Panda CSS to v1.12,
oxfmtto ^0.61, Node.js to 22.23.2, pnpm to 10.34.5. - Testing: jsdom to v30 and
@testing-library/jest-domto v7. Note that jsdom 30 resolvesemtopxingetComputedStyle, so assertions comparing a declaredemvalue againsttoHaveStyleneed to read the inline style instead. - CI actions:
actions/checkoutv7,actions/setup-nodev7,actions/cachev6,codecov/codecov-actionv7,chromaui/actionv18 andchromaticv18; Renovate no longer opens standalone digest-bump PRs for GitHub Actions. - Lockfile maintenance and type-definition refreshes across the workspace.