Lossless Changelog
product

A Services Catalog That Composes Itself From Atomic Offerings

The Services section grew a real spine: a three-collection model (items → categories → cards) where the same atomic offering can be baked into one card's Standard tier and surfaced as a +€80 add-on on another, while a separate billing mode handles hourly/daily/multi-day bookings — and Sveltia learned every one of those shapes.

Michael StatonMusic Editoraugmented with Claude Code on Claude Opus 4.7

What changed and why

The old Pricing Tiers collection was a flat list — one tier per markdown file, a features array of free-text strings, a single CTA. It couldn't express the actual shape of how she sells: top-nav categories (Portrait, Boudoir, Events) with multiple Cards inside each, where each Card has a progression of Tiers (Taste → Standard → Premium), where the same atomic offering (Hair-and-Makeup, 10-Edited-Photos, Second-Location) might be baked into one Card's Standard tier and offered as a +€80 add-on on another, and where some Cards aren't tier-based at all — they're hourly / daily / multi-day rate bookings for proprietors.

So the spec landed: context-v/specs/Services-Section-as-Tiered-Card-Catalog.md. Three nested collections with Sveltia's relation widget as the join:

  • service-items — the shared catalog of atomic offerings. Each item has a Train-Case slug, a label, an optional default_price_eur, and a kind (deliverable / production / logistic / post-production).

  • service-categories — top-nav swipe axis. Slug drives /services#<slug>.

  • service-cards — the deck. Each Card belongs to a Category, picks Tiers (each Tier picks included items from the catalog), and surfaces optional add-ons (also picked from the catalog, with a per-Card price override).

A Card's billing_mode switches the model: package mode flips through Tiers; custom_time mode drops the Tier carousel and exposes a rate-unit selector + quantity stepper. Same WhatsApp template engine handles both, with {{tier}} / {{price}} / {{tags}} substitutions for packages and {{duration}} / {{rate_unit}} added for time-based bookings.

Why this shape

The catalog-and-slug pattern (separating atomic offerings from how Cards compose them) does for services what we already did for images last week — image records live in a catalog, galleries reference them by slug, an ImageKit rename only touches one file. Here: an item rename ripples through every Card that references it; raising the H&M fee globally is one edit to one item with a default_price_eur; a Card that overrides the price stays overridden.

Sveltia's relation widget is what makes this UX-livable. Typeahead picker against the catalog rather than free-text strings means no drift between Hair-and-Makeup and Hair-Makeup. The constraint on what Sveltia can extend (Sveltia-Constraints-for-CMS.md) forced this shape — and it turned out to be the right shape.

Also included

  • Stacked-card preview face decided: image dominates the top 2/3, bottom 1/3 strip shows title + included tags for the default tier + a reveal affordance. No price on the stacked face — price lives in the opened state where it composes dynamically.

  • Per-Tier hero_image field on Cards (relation to the image catalog) so a premium Tier can carry a more enticing visual than the entry Tier, with fallback to the Card-level hero.

  • Public vs guarded copy: public_copy lands in SSR HTML (SEO + LLMs see it), guarded_copy is hidden from the initial DOM until a button click — documented as "not in initial HTML," not real access control.

  • Pricing Tiers collection kept in Sveltia for now (caught the regression where it briefly disappeared from the sidebar — restored). It stays until the existing Standard entry is re-entered by hand as a Card under the new model.

  • Build verified clean after each collection addition.