Saelle Care
A short skincare routine, sold by skin type and step, on a store rebuilt entirely from code.
The brief
Saelle Care sells a four-step routine (cleanse, tone, treat, moisturize) plus refills. Shoppers choose by skin type and step and read the ingredients before they buy, so the store runs on typed data, not product copy.
How it’s built
Everything that defines the store lives in the repository.
- One tokens file sets the theme, the brand book and the social images, and a contrast check blocks any color pair below WCAG AA.
- One command loads the catalog, size variants, ingredients, collections, pages, journal, policies and menus. Running it twice changes nothing.
- Skin type, key ingredients and how to use come from fields the merchant edits in the admin.
01Features
Feature breakdown
01
Product pages driven by skin type and ingredients
Skincare shoppers decide by skin type, ingredients and how to use a product, and none of that should be buried in a description.Decision
Skin type chips linked to collections, key ingredients from metaobjects, how-to-use bound to a metafield, and a refill template that links back to its bottle.
Three custom blocks (product attributes, key ingredients and refill for) read typed data, so a merchant changes a product’s skin types or ingredients in the admin and the page follows. The seed sets each refill’s template suffix and its
custom.refill_forreference, so the refill page points back to the bottle it tops up.Quality
- Every block reads from fields the merchant edits in the admin
- Refill products use their own template, set by the seed
Surfaces and skills
- Theme
- Data
- Metaobjects
- Metafields
- Theme blocks
- Liquid
02
Sample reviews, done honestly
A demo store needs reviews to show the design, but fake reviews mislead shoppers and search engines.Decision
Review metaobjects linked to products, a star summary and highlights, an always-visible "written with AI" note, and no review structured data.
Reviews live in the
customnamespace on purpose:reviews.ratingis the contract that real review apps write and other surfaces trust, so sample data stays out of it. A rich results test of a product page shows only the product, with no rating.Quality
- No aggregateRating or review in JSON-LD, no microdata, data-nosnippet on the block
- Reviews editable from the product's Reviews metafield
- Stars fill exactly, including the fraction of the last star
Surfaces and skills
- Theme
- Data
- Metaobjects
- Structured data
- Liquid
- Accessibility
03
One brand on every page, checkout included
A brand that stops at the home page breaks at the cart, the 404, the gift card or checkout.Decision
Palette, type, logo, favicon and social image applied to every template, including password, gift card, accounts and checkout, from the brand tokens.
The home page tells the brand in story order: a hero from the routine scene with its own mobile crop, three verifiable facts, collections by routine step, a featured product with its texture, the refills story and the journal.
Quality
- Home narrative uses verifiable facts instead of invented reviews
Surfaces and skills
- Theme
- Checkout
- Design systems
- OS 2.0 settings schema
- Liquid
04
48 packshots without a photo shoot
A skincare catalog needs studio-style images for every size variant, without a photo budget.Decision
48 packshots rendered from SVG templates and the catalog file (six container types, per-size scale, texture swatches), with label contrast checked at render time.
Each product’s container type, size and texture come from the catalog, so the images can’t drift from the data. Files are uploaded by content hash and attached to variants by file id, so rerunning the pipeline never duplicates media.
terminal npm run images -- saelleQuality
- Label contrast checked while rendering
- Per-variant media: switching Rich Cream from 50 to 100 ml changes the image
- Contact sheet generated with every run
Surfaces and skills
- Data
- Node.js
- sharp
- resvg
- Accessibility
- Admin GraphQL API
05
Brand colors and fonts from one file
Hand-copying brand colors and fonts into a theme and checkout drifts within days, and nobody notices until a launch review.Decision
One tokens.json per brand; a script maps it into each theme's settings_data.json through a per-theme mapping and rejects any setting id or value the theme schema doesn't define.
Themes in the Horizon family share a settings model but not the same ids. Each store keeps a small mapping from token roles (
background,text,accent…) to its theme’s settings, and the script validates every write againstsettings_schema.jsonbefore touchingsettings_data.json.terminal npm run theme:brand saelleThe same tokens feed checkout and customer account branding, the favicon set and the Open Graph images, so one change to a brand color reaches every surface.
Quality
- Regenerating from unchanged tokens produces no diff
- Unknown setting ids or select values fail the script instead of writing silently
- Brand book (BRAND.md) generated from the same tokens
Surfaces and skills
- Theme
- Data
- Design systems
- Node.js
- OS 2.0 settings schema
- Automation
06
Brand colors that can't fail accessibility
Accessibility issues in brand colors are usually found after launch, when changing them means a redesign.Decision
Every color pair a brand declares is checked against WCAG 2.2 (4.5:1 for text, 3:1 for large text and UI) in a script that fails the build.
The check is twelve lines of math from the WCAG definition of relative luminance, with no dependencies:
tools/brand/contrast.mjs export function luminance(hex) { const [r, g, b] = hex .replace('#', '') .match(/.{2}/g) .map((h) => parseInt(h, 16) / 255) .map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } export function contrast(a, b) { const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); return (hi + 0.05) / (lo + 0.05); }Each brand declares the pairs it actually uses (text on background, button label on accent, border on surface), so the gate checks real combinations instead of every possible one.
Quality
- Caught two border colors below 3:1 before any theme was styled
- Contrast table published in each brand's generated BRAND.md
Surfaces and skills
- Data
- Accessibility
- WCAG 2.2
- Node.js
- CI
07
The whole store, rebuilt with one command
Rebuilding a store's catalog, custom data and collections by hand takes days and can't be reviewed.Decision
One command loads definitions, metaobjects, products, images, collections and publication from versioned files with the Admin GraphQL API; reruns change nothing.
Products go through
productSet, which takes the whole desired state of a product, so the script describes what the store should look like instead of how to change it. Images are uploaded once by content hash and attached by file id; collections are built from metafield conditions with the collectionsourcesinput from API version 2026-10.terminal npm run seed -- saelle saelle-care.myshopify.comThe result for Saelle Care: 18 products, 30 variants, ingredient metaobjects with INCI names and 9 collections driven by metafields, rebuilt from the repository in one run.
Quality
- Second run reports identical counts and 0 files to upload
- --dry-run prints the plan without writing
- Built-in verify step compares the store with the source files
Surfaces and skills
- Data
- Admin GraphQL API
- productSet
- metaobjectUpsert
- Idempotency
- Node.js
08
Pages, journal and menus from the repository
Pages, journal articles, policies and menus are usually built by hand in the admin and can't be reviewed or rebuilt.Decision
Seed them from JSON like the catalog: menus link by handle and only change when their structure differs; article images are composed from the packshots.
Not every Shopify resource has an upsert mutation. For pages, articles, policies and menus the seed looks the resource up, compares it with the source file and writes only when something changed, so the second run is a no-op like the catalog.
Quality
- Rerunning the seed reports menus and policies as unchanged
- Resources without upsert use lookup, compare, then write
Surfaces and skills
- Data
- Admin GraphQL API
- Idempotency
- Node.js