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.
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 optionaldefault_price_eur, and akind(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_imagefield 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_copylands in SSR HTML (SEO + LLMs see it),guarded_copyis hidden from the initial DOM until a button click — documented as "not in initial HTML," not real access control.Pricing Tierscollection kept in Sveltia for now (caught the regression where it briefly disappeared from the sidebar — restored). It stays until the existingStandardentry is re-entered by hand as a Card under the new model.Build verified clean after each collection addition.
