Salve developer guide
This page is for developers and agencies who customize Salve for a merchant. It explains how the theme is built, where things live and the conventions to follow so your changes survive theme updates. Merchant-facing setup is in the documentation hub.
Duplicate the theme before you edit code. Code changes are not covered by support, and theme updates don't carry them over. Keep a short list of the files you changed so you can reapply them.
Architecture at a glance
-
Base: built from Shopify's Skeleton theme. Online Store 2.0 with sections everywhere, header and footer section groups, theme blocks (
/blocks) and static blocks for the hidden quick add section. - No build step, no framework. Vanilla ES modules and native CSS. Nothing is minified in the repo; Shopify minifies on delivery. No third-party requests are made by theme code.
-
Section Rendering API for every dynamic update: variant changes, cart, filters, predictive search, recommendations, pickup, quick add and product comparison all fetch server-rendered HTML (
?section_id=or?sections=) and swap it in. There is no client-side templating of product data. -
Health data layer: snippets under
snippets/health-*.liquidread product metafields (namespace from the Metafield namespace setting, defaulthealth) and feed the product blocks, product cards, cart lines and structured data. Missing data renders nothing. -
Faster page loads: when the Faster page loads setting is on,
layout/theme.liquidoutputs Speculation Rules that prefetch same-origin pages with conservative eagerness (on press, not hover). Cart, checkout, account,/apps/,/services/and any URL with a query string are excluded, as are links withrel="nofollow",target="_blank"ordownload. Adddata-no-prefetchto any other link that must not be fetched early. -
B2B:
snippets/b2b-location.liquidrenders the "Buying for" company location switcher in the header and menu drawer whencustomer.b2b?is true, usingcustomer.company_available_locationsand each location'surl_to_set_as_current. It renders nothing for other customers. -
Works without JavaScript: script-only controls carry the class
u-js-only; their fallbacks carryu-no-js. A one-line head script swapshtml.no-jsforhtml.js.
File map
| Folder | What is in it |
|---|---|
layout/ |
theme.liquid (head order: canonical, fonts, CSS variables, critical.css, base.css, content_for_header, module scripts, structured data) and password.liquid. |
assets/ |
critical.css (tokens, reset, typography, core components; kept under 14 KB), base.css (the rest of the shared styles), section-*.css and snippet-*.css (styles for rarely used sections, loaded only where they render), and one ES module per component (see below). |
sections/ |
Main templates (main-*.liquid), content sections, section groups (header-group.json, footer-group.json) and hidden fragments used by the Section Rendering API: cart-drawer, cart-icon-bubble, predictive-search, pickup-availability, quick-add, compare-data, product-card-fragment. |
blocks/ |
Theme blocks. Product-only blocks start with an underscore (_price, _safety-information, _documents…). Public blocks (heading, text, button, image, icon-text, group, spacer, custom-liquid) can be used in any content section that accepts @theme. |
snippets/ |
Shared markup. Every snippet starts with a {% doc %} block listing its parameters and an example call. |
templates/ |
JSON templates (customer accounts are handled by Shopify's new customer accounts and the <shopify-account> component in the header, so there are no templates/customers/* files), including the alternates product.device, product.supplement, collection.compare, page.contact, page.about, page.documents, page.safety-notices, page.hsa-fsa and page.quick-order. |
locales/ |
Storefront strings (en.default.json, fr, de, es, it) and editor strings (*.schema.json). No storefront text is hard-coded in Liquid. |
listings/ |
Preset templates: listings/<preset>/templates/ holds the index, product and collection templates for Salve, Gauge and Remedy. When a merchant installs a preset, these replace the root templates of the same name. The Gauge product template uses the device layout and the Remedy one the supplement layout. |
Conventions
-
CSS classes:
s-sections (s-hero),b-blocks (b-price),c-shared components (c-button,c-card,c-media),u-utilities (u-page,u-visually-hidden). BEM-lite modifiers:c-button--secondary. -
Color schemes: each scheme outputs a class
color-scheme-Nthat defines--color-background,--color-foreground,--color-foreground-muted,--color-surface,--color-accent,--color-on-accent,--color-border,--color-notice-background,--color-notice-foregroundand--color-focusas RGB triplets, sorgb(var(--color-foreground) / 0.12)works for alpha variants. -
Tokens (set in
snippets/css-variables.liquidfrom theme settings): spacing--space-1…--space-32(4 px grid), type--text-display,--text-h1…--text-h6,--text-body,--text-small,--text-label, radii--radius-card,--radius-button,--radius-input,--radius-chip, layout--page-width,--gutter,--grid-gap, and--target-min(44 px touch targets). -
Section CSS: put styles that a section always needs in its
{% stylesheet %}tag (Shopify bundles them into one file). Styles for a section that appears on few pages go inassets/section-<name>.css, loaded withstylesheet_tagat the top of the section. -
Inline styles are only used for CSS custom properties driven by settings, for example
style="--ratio: 1"on.c-media. -
IDs derive from
section.idandblock.id; never hard-code an id that could repeat on a page. -
Accessibility: WCAG 2.2 AA. Dialogs use native
<dialog>throughDialogControlleringlobal.js(focus trap, Esc, return focus). Status messages go throughannounce(), which writes to the#a11y-statuslive region. Keep focus rings; never removeoutline.
JavaScript
Every interactive part is a custom element that owns its listeners and re-initializes in connectedCallback, so swapped markup from the Section Rendering API just works. assets/global.js exports the shared helpers:
| Export | Use |
|---|---|
publish(topic, payload), subscribe(topic, handler), EVENTS
|
A 30-line pub/sub. Topics (all listed in Salve.EVENTS): cart:updated (payload: the cart JSON and rendered sections), variant:changed, planner:suggestion, compare:changed, header:ready ({ height }, after the header sets --header-height) and selling-plan:changed ({ planId }, empty for one-time purchase). Subscribe to cart:updated to react to any cart change. Strings for scripts come from Salve.strings; text that includes a value uses a placeholder such as [title] or [count] so each language keeps its own word order. |
fetchSection(sectionId, url, params), fetchSections(ids, url)
|
Section Rendering API helpers (HTML for one section, JSON for several). |
parseHTML(html), swapInnerFrom(html, selector, target)
|
Parse fetched markup (noscript fallbacks are dropped) and swap a region by selector. |
trapFocus(), focusables(), DialogController
|
Dialog and drawer behavior. |
announce(text) |
Polite screen reader announcement. |
debounce(), storage, prefersReducedMotion(), rootUrl()
|
Utilities. storage.get/set wrap localStorage in try/catch; every feature works without storage. |
Components and their files: <product-form> (product-form.js), <variant-picker> (variant-picker.js), <media-gallery> (media-gallery.js), <cart-drawer> and <cart-items> (cart.js), <predictive-search>, <facet-filters> (facets.js), <supply-planner>, <compare-tray> and <compare-dialog> (compare.js), <product-selector> (selector.js), <quick-order-list> (quick-order.js), <quick-add>, <pickup-availability>, <product-recommendations> (recommendations.js), <text-size-control> (text-size.js), <dismissible-notice> (notice.js), <sticky-buy-bar> (sticky-buy.js), <recently-viewed>, <shipping-estimator>, <delivery-estimate>, <back-to-top>, <slide-show> (slideshow.js), and details-disclosure.js for menus built on <details>. Scripts load as type="module" only on templates or sections that need them; an import map in the head keeps shared modules cache-busted.
Variant change contract: <variant-picker> fetches the product section with ?option_values= and swaps the price, regulatory statements, identifiers, inventory status, buy buttons, pickup, installments, quantity rules, supply planner and media selection by id, then publishes variant:changed. If you add a block that depends on the variant, give its root a stable id and add it to the swap list in variant-picker.js.
Health data snippets
| Snippet | Renders |
|---|---|
health-field |
One field: a block setting override (dynamic source) first, then the conventional metafield. Handles rich text, multi-line and list types. |
health-status |
Regulatory status chips or statement lines, filtered by the region setting. Only us_pma_approved ever prints "FDA approved". |
health-safety |
The "Important safety information" band. The _buy-buttons and _price blocks render it as a fallback when a product has safety data but the merchant removed the block. |
health-documents |
Documents grouped by type, current versions first, previous versions in a disclosure. Falls back to the simple file list metafield. |
health-identifiers, health-parties, health-units, health-hsa, health-notice
|
Identifiers, responsible parties, units per pack and price per unit, HSA/FSA eligibility, and safety notices in product, card and cart contexts. |
structured-data |
The page's JSON-LD (snippets/structured-data.liquid, rendered from layout/theme.liquid): Organization data with the return and shipping policies from the Search engines theme settings, WebSite data with the search action, and Product data with offers, identifiers and ratings. |
health-setup-checker |
The Health data setup panel shown only in the theme editor (request.design_mode). It lists which fields have values and refers merchants to Health data setup in the documentation; it contains no external links. |
The full metafield list, types and choice values are in Health data setup. Shipped JSON never references custom metafields or metaobjects; only Liquid reads them, defensively, at render time.
Adding a section or block
- Copy the closest existing section. Keep
color_scheme,padding_top,padding_bottomand, if it has a heading,heading_level. - Put every label in
locales/en.default.schema.jsonundert:sections.<name>and every storefront string inlocales/en.default.json, then mirror the keys in the four other locale files (the theme ships a locale parity check insalve-qa). - Use sentence case, American English, no ampersands, and Shopify's terminology for labels.
- Give the section a preset with placeholder content so it appears in the section picker and renders sensibly before the merchant adds content.
- If the section is interactive, make it a custom element and initialize it in
connectedCallback. Listen forshopify:section:loadonly if the element cannot reinitialize itself. - Run
shopify theme checkbefore you push. The theme ships with 0 offenses.
Quality checks the theme ships with
The salve-qa folder (not part of the theme zip) contains the checks used before every release: Theme Check, a static pass (Liquid and HTML parse, JS and CSS syntax, translation keys, upload gotchas, no-JavaScript rule, performance budgets), content gates (no claims language, no demo resources in shipped JSON), color contrast for all preset schemes, and locale parity. Budgets: critical.css under 14 KB, home page JS under 25 KB gzipped, product page JS under 45 KB gzipped.
Things not to do
- Don't add scripts from third-party hosts inside theme files; use app blocks or the Custom Liquid section so the merchant controls them.
- Don't output health or regulatory claims from theme code. The theme prints merchant data and fixed, neutral labels only.
- Don't add file uploads, date of birth or symptom fields to storefront forms.
- Don't remove the safety band fallback in
_buy-buttonsand_price; it is what keeps safety information visible when a block is deleted.