DDS Vibe Academy / Masterclass / Shopify Theme Upgrade

The DDS Homepage Upgrade Masterclass

An 18-round AI-driven playbook for upgrading a Shopify Online Store 2.0 theme end-to-end. Brand Bible foundation, 8 operating principles, reusable prompt library, 8-check pre-push validation, image optimization, and every Atelier gotcha that burned hours of debugging.

Published May 24, 2026
Read Time 60 minutes
Level Advanced
Authors Robert McCullock + Claude Opus 4.7
Word Count ~17,500
Reproducibility Self-contained playbook
Quick Answer

This masterclass is the reproducible 18-round playbook used to upgrade Design Delight Studio from a generic Atelier Shopify theme to a brand-compliant, dark-themed, SEO-optimized, performance-tuned store in May 2026. The pattern is built around four non-negotiables: a Brand Bible (Verified Facts plus Forbidden Strings) comes before any code; one concern per round so failure attribution stays clean; an 8-check Python pre-push validation gate runs on every push; and a live browser audit via Chrome MCP verifies behavior after every push. The result: a 57.65% image weight reduction, a 4.14-second LCP improvement on product pages, zero brand-bible violations, and a self-contained document a fresh Claude Code instance can use on any Shopify Online Store 2.0 theme.

The 60-Second TL;DR

Six measured outcomes that define what this playbook actually delivered when applied end-to-end to a live Shopify store:

18 Discrete build rounds, each one concern, each gated by validation and live audit
57.65% Image weight reduction across the entire theme via WebP conversion
−4.14s LCP improvement on product pages after the WebP pass and hero-image swap
0 Brand Bible violations in pushed code, enforced by the 8-check pre-push gate
8 Mandatory validation checks before any file lands in the testing theme
~17.5K Words of self-contained documentation a fresh agent can fork onto any Shopify store

5 Strategic Takeaways

  1. A Shopify theme upgrade is never one prompt. It is a sequence of focused operations, each gated by validation and live verification. Mixing concerns guarantees broken attribution when a push fails.
  2. The Brand Bible is the unfair advantage. Without verified facts and forbidden strings, AI generates plausible but non-compliant copy. With them, the agent becomes a reliable brand-voice executor.
  3. Static validation is necessary but not sufficient. The 8-check Python gate catches syntax and structural failures. Live browser audit catches CSS specificity wars, DOM mutation bugs, and Atelier-specific quirks that static checks never see.
  4. Atelier has two killer gotchas. Custom UUID color schemes do not emit CSS, so palette tokens must be hardcoded in section CSS. The framework morphs the DOM during navigation, so event listeners attached to elements die. Both eat hours when undiagnosed.
  5. The playbook compounds. Round 1 took longest because the Brand Bible had to be assembled. Round 18 was fast because the validation script and prompt library were already in place. The next project starts at round 8 because the infrastructure carries over.

Why This Document Exists

A Shopify theme upgrade done well is not a single prompt. It is a series of focused operations, each gated by validation and live verification. This masterclass captures the exact pattern used to upgrade DDS Boston from a generic Atelier installation to a brand-compliant, dark-themed, SEO-optimized, performance-tuned store.

The pattern works because of four reinforcing constraints:

Constraint 1

Brand Bible Comes First

Without a verified-facts whitelist and a forbidden-strings list, AI generates plausible but non-compliant copy. The Brand Bible is assembled before any code is written and acts as both the source of truth for legitimate claims and the abort trigger for violations.

Constraint 2

One Concern Per Round

Mixing concerns (dark theme plus new SEO plus content reframe plus image swap) makes pre-push validation impossible to interpret. If a 12-file push breaks, you cannot tell which concern caused the failure. Push one concern, verify, then push the next.

Constraint 3

Live Audit Gates Every Push

Static validation catches syntax. Live browser audit catches CSS specificity wars and DOM mutation bugs. After every push, a Chrome MCP audit runs against the testing theme: H1 count, JSON-LD validity, OG meta presence, forbidden-string sweep on rendered text, interactive checks for any changed behavior.

Constraint 4

8-Check Pre-Push Is Non-Negotiable

NULL byte sweep, JSON parse and symmetry, schema name limits, no empty text defaults, range step alignment, Liquid block balance, no comment tags inside Liquid blocks, forbidden strings sweep. If any check fails, the push is aborted and the failure is fixed before retry.

The combination is the unlock. Any one of these constraints feels excessive in isolation. Together they make the 18-round upgrade not just possible but predictable.


Foundation: Brand Bible Setup

This is the most important section of the playbook. Skip it at your peril.

Before any code is written, the AI agent must internalize three things: a Verified Facts Whitelist, a Forbidden Strings Whitelist, and a set of Brand Color Tokens plus Typography. The Brand Bible is the document the entire build sequence references — every reframe pulls from it, every pre-push validation checks against it, every live audit verifies the rendered page does not violate it.

Robert's standing rule, captured verbatim because it defines the entire posture: "Don't invent facts, numbers, pricing, certifications, partners, policies, results, reviews, or timelines. If unsure, state 'Not verified' and what to check." Every claim must trace back to the Verified Facts Whitelist. Every reframe must be supplier-attributed. Every number must cite a source.

1. Verified Facts Whitelist

A document listing every claim the brand can make. For DDS specifically, this covers the supply chain, the material specs, the geographic claims, and the certification ownership chain. The shape is more important than the specific values — a fresh agent on a new project re-builds this for the new brand's supply chain.

Some specific commercial figures are redacted on this public page (Printful list pricing, exact OEKO-TEX cert numbers). The structure stays:

Certification ownership chain (the structure that matters)

DDS holds NO direct cert licenses.

Stanley/Stella holds: GOTS + GRS + OCS + OEKO-TEX + PETA-Approved Vegan
econscious holds:     GOTS + OCS + OEKO-TEX + PETA-Approved Vegan
Printful holds:       GRS + OEKO-TEX (for in-house cut & sew on AOP products)

Stanley/Stella OEKO-TEX cert: [supplier-held, verified in OEKO-TEX registry]
                              issued by Centexbel
econscious OEKO-TEX cert:     [supplier-held, verified in OEKO-TEX registry]
                              issued by Hohenstein
Why This Structure Matters

DDS is a print-on-demand brand. The blank apparel ships from Stanley/Stella's mills. Printful prints the designs. DDS does not manufacture, does not hold direct cert licenses, and does not legally control the production chain. Any claim that says "DDS is GOTS certified" is false. Any claim that says "Stanley/Stella holds GOTS certification for the blanks we use" is true and verifiable. The Brand Bible enforces this distinction so the agent cannot drift into legally questionable copy.

Material specs per product line

These are Stanley/Stella and econscious public spec-sheet numbers. They are not commercial secrets and they survive in this masterclass:

SKU Type Composition Weight Size Range
STTU169Unisex tee100% organic ring-spun cotton5.3 oz/yd² (180 g/m²)XS-3XL
SATU007Crafter mid-light tee100% organic combed ring-spun cotton4.6 oz/yd² (155 g/m²)S-2XL
SASU009Drummer 2.0 hoodie80% organic cotton + 20% recycled poly (US) / 85+15 (EU)8.3 oz/yd² (280 g/m²)S-2XL
SASU010Roller sweatshirtSame as SASU0098.3 oz/yd² (280 g/m²)S-2XL
AOP #141AOP recycled sweatshirt96% recycled poly + 4% elastane (US/MX) / 95+5 (EU)9.08 oz/yd² (308 g/m²)XS-2XL
EC8000econscious tote100% organic cotton 3/1 twill8 oz/yd² (272 g/m²)16"×14.5"×5"

Geographic claims allow / deny

Claim Status Reason
Designed in BostonALLOWEDThe design work happens in Boston. Verifiable.
Boston-designedALLOWEDSame as above, just phrasing variant.
Made-to-order, designed in BostonALLOWEDBoth halves verifiable.
Made in BostonFORBIDDENManufacturing happens at Printful's POD network, not in Boston.
Boston-madeFORBIDDENSame reason.
Boston-craftedFORBIDDENSame reason.
Hand-finished in BostonFORBIDDENNo finishing happens in Boston.
Crafted in BostonFORBIDDENSame reason as Made in Boston.
Made-to-order in BostonFORBIDDENProduction is global POD, not Boston-based.

Public list pricing

Specific Printful list prices and shipping rates are redacted in this public version. The pattern that matters for any agent applying this playbook: every price quoted in the storefront must be reconciled monthly against the supplier's public catalog, with a dated annotation in the Verified Facts document.

2. Forbidden Strings Whitelist (Pre-Push Blocker)

Any string in this list found in any pushed file triggers an immediate ABORT at the pre-push gate. The list is paste-ready Python. Copy it directly into the validation script.

FORBIDDEN_STRINGS = [
    # === Geographic — manufacturing claims (DDS doesn't manufacture) ===
    'Made in Boston', 'Boston-made', 'Boston-crafted',
    'Hand-finished in Boston', 'Made-to-order in Boston',
    'Crafted in Boston',

    # === Certification ownership (DDS holds no direct cert licenses) ===
    'We hold 5 certifications', 'We hold 6 certifications',
    'Our products are GOTS', 'DDS is X-certified',
    'DDS is GOTS', 'DDS is GRS', 'DDS is OEKO',
    'OEKO-TEX certified', 'OEKO-TEX-certified',
    'Fair Trade certified', 'Fair Trade-certified',
    'Fair Trade Manufacturing',
    'Verify Our Certifications',

    # === Absolute / unverifiable claims ===
    '100% Defect Guarantee', 'Toxin Free Dyes',
    'Toxin-Free Dyes', 'Sustainable fashion only',
    'Zero waste', 'Zero-waste', 'Zero-Waste',
    'Zero-Waste Packaging', 'sustainable packaging',
    'recyclable packaging', 'exclusively organic',
    'exclusively use that 1%',

    # === Stale marketing copy ===
    '10-Year Care Guide', '10+ Year Lifespan',
    '10-Year Lifespan', '10-Year Organic Cotton Care Guide',
    'Free 10-Year', 'Weekly sustainable style tips',
    '95% Less Water', '95% less water',
    '91% less water', '% less water',

    # === Geographic shipping false-claims ===
    'Free US Shipping', 'Free U.S. Shipping',
    'U.S.A delivery', 'U.S.A shipping',

    # === Old CTA copy (R3.17 reframed) ===
    'Social Media Ready',  # use lowercase 'Social-media ready'
    '100% Private',  # use 'Private' without the absolute
    'GOTS Organic. Sustainable fashion only',
    'GOTS certified organic cotton apparel',  # too narrow
    'Shop Sustainable Fashion with Total Confidence',
]

Compliant reframe pairs

Every forbidden string maps to a verified, supplier-attributed alternative. The reframe table that drove the R3.13 bulk sweep:

Forbidden Compliant Reframe Why
OEKO-TEX certifiedOEKO-TEX Standard 100 substrateAttributes cert to the material, not the brand
Fair Trade certifiedFair Trade supplier-certifiedCert is held by supplier, not by DDS
Made-to-order in BostonMade-to-order, designed in BostonSplits design (verifiable) from production (POD network)
95% Less WaterLower water footprintRemoves uncited absolute percentage
10+ Year LifespanBuilt to last for yearsRemoves unverifiable timeline
Free US ShippingFree Worldwide ShippingDDS ships globally, not US-only
Verify Our CertificationsVerify Material CertificationsCert ownership at material level
Shop Sustainable Fashion with Total ConfidenceSee the Fit. Skip the Returns.Substrate-attributed copy that works across product types

3. Brand Color Tokens + Typography

The locked palette and typography stack. Hardcode these in every section CSS. Do not rely on Atelier emitting them via custom color schemes — Principle 5 explains why.

/* DDS Brand Palette */
--dds-deep:        #0a1a0f;  /* darkest forest */
--dds-forest:      #0d2818;  /* card surface bg */
--dds-teal:        #1a5c45;  /* secondary success */
--dds-emerald:     #2d8a6e;  /* CTA accent */
--dds-mint:        #4fb38c;  /* meta text on dark */
--dds-gold:        #c9a84c;  /* primary CTA */
--dds-gold-bright: #e8c84a;  /* hover state */
--dds-cream:       #f5f0e8;  /* primary text on dark */
--dds-cream-soft:  rgba(245, 240, 232, 0.72);  /* muted text */
--dds-white:       #fafaf7;  /* high-contrast text */

/* Typography — self-hosted WOFF2 only, NEVER Google Fonts CDN */
font-display:      'Playfair Display', Georgia, serif;
font-body:         'DM Sans', system-ui, -apple-system, sans-serif;
font-mono:         'JetBrains Mono', 'Menlo', 'Consolas', monospace;

Self-hosted, not Google Fonts CDN. Atelier 2.1.1 ships with Playfair Display and DM Sans pre-loaded as WOFF2. Adding a Google Fonts CDN link would duplicate the loads, hurt LCP, and introduce a cross-origin dependency. The stack uses what the theme already serves.

The Critical Insight

"Don't invent facts, numbers, pricing, certifications, partners, policies, results, reviews, or timelines. If unsure, state 'Not verified' and what to check."

Every claim must come from the Verified Facts Whitelist. Every reframe must be supplier-attributed. Every number must cite a source. This is the rule that makes AI-generated brand copy actually publishable instead of plausibly fabricated.


The 8 Operating Principles

These principles govern every build round. Some are workflow discipline (one concern per round). Some are framework-specific gotchas (Atelier custom-scheme CSS does not emit). All eight had to be in place before the 18-round sequence could run cleanly. Skip any one of them and the validation gate stops being a gate.

Principle 1 — Silent Multi-Phase Builds

For any task touching more than 4 files or more than 1,000 lines of code, the agent works silently in a scratch workspace, validates each phase, and only presents the final assembled output via present_files. Intermediate code is never shown.

Report format during silent build: "Phase N complete (one-line description)."

Principle 2 — One Concern Per Round

Each push must address exactly one concern:

Round typeWhat it does
R3.0 — Reframe sweepBulk string replacement for verified-facts compliance
R3.6 — New sectionBuild a new Liquid section + wire into templates
R3.10 — Dark themeConvert one section's CSS palette
R3.13 — Audit cleanupFix violations from a live audit run
R3.15 — JS interaction fixFix one broken interaction (size guide, variant picker)

Mixing concerns makes failure attribution impossible. If a 12-file push breaks, was it the dark theme tokens or the JSON-LD parse fix or the cert reframe? Push one concern, verify, then push the next concern.

Principle 3 — 8-Check Pre-Push Validation (Mandatory Gate)

Every file in a push set must pass all 8 checks. The full Python script lives in the Validation Toolkit section. The check list:

[1] NULL byte sweep              — strip any \x00 bytes that snuck in
[2] JSON validity + symmetry     — parse cleanly; sections.keys() == order
[3] Schema validity              — JSON parses; name ≤ 25 chars
[4] No "default": "" on text     — Shopify rejects empty text defaults
[5] Range step alignment         — default value aligned to step from min
[6] Liquid block balance         — if/endif, for/endfor, etc. paired
[7] No comment/endcomment inside {% liquid %} block (Shopify parser bug)
[8] Forbidden strings sweep      — ABORT if any banned term present

If any check fails, the push is aborted and the failure is fixed before retry. Not "fix and push anyway." Not "fix as part of the next push." Stop, fix, re-validate, then push.

Principle 4 — Live Browser Audit Between Rounds

After every push, run a Chrome MCP audit against the testing theme preview URL. The audit captures:

  • H1 count per page — must be exactly 1
  • JSON-LD valid count + error count — 0 errors
  • OG and Twitter meta presence — page-type appropriate
  • Robots meta — noindex on /404 and /search
  • Forbidden-string sweep on rendered text — not just source code
  • Color contrast spot-checks — no dark-on-dark
  • Interactive checks — size guide accordions open, variant pickers swap images, color toggles work

Static validation passes ≠ live behavior works. Atelier morphs DOM, hides selectors, overrides CSS. Only live audit catches these.

Principle 5 — Atelier Does NOT Auto-Emit Custom Color-Scheme CSS

This One Burned 5 Hours of Debugging
  • Atelier emits CSS for built-in schemes (scheme-1 through scheme-6)
  • Atelier does NOT emit CSS for custom UUID schemes (scheme-09beadd5-…)
  • Even if you add color_scheme to a section schema and default it to a UUID scheme, --color-background falls back to white at runtime
  • The fix is to HARDCODE the palette in the section's CSS, not rely on scheme emission

Read it twice. This is the single most expensive Atelier gotcha in the entire project.

Principle 6 — Atelier Morphs the DOM, Killing Event Listeners

The Second Killer Gotcha

Stock section IIFE pattern ((function(){...})() at end of section file) adds event listeners on page load. Atelier morphs sections during navigation — listeners attached to morphed elements no longer fire.

Solution: Document-level event delegation with capture phase. Listeners on document survive any morphing.

WRONG — dies on morph
// IIFE at end of section file
(function(){
  var container = document.querySelector(
    '.ops-' + sectionId
  );
  if (!container) return;
  var trigger = container.querySelector(
    '[data-sizeguide-trigger]'
  );
  trigger.addEventListener('click', function(){
    // toggle accordion — dies after navigation
  });
})();
RIGHT — survives morph
// Document-level, capture phase
document.addEventListener('click', function(e){
  var trigger = e.target.closest &&
    e.target.closest('[data-sizeguide-trigger]');
  if (!trigger) return;
  var content = trigger.parentElement
    .querySelector('[data-sizeguide-content]');
  if (content) {
    content.classList.toggle('is-open');
    trigger.setAttribute('aria-expanded',
      content.classList.contains('is-open'));
  }
}, true);  // capture phase

Principle 7 — Section Wrapper Must Set Its Own background-color

Do not assume the wrapper inherits a background. If your section has dark theme tokens but no background-color: var(--ops-bg) on the root wrapper, the section sits transparent over Atelier's white body.

.ops-{{ section.id }} {
  background-color: var(--ops-bg) !important;  /* !important defeats Atelier */
  color: var(--ops-text);
  /* ...rest of layout... */
}

The !important is not stylistic — it is functional. Atelier's body-level rules have higher specificity than ordinary section wrapper rules. The override is the only thing that ensures the dark palette wins.

Principle 8 — Settings Persist Even When Schema Changes

If a section instance's stored settings include text_color: "#1f2937" (dark gray, from an old light-theme default) and you later change the section schema default to #f5f0e8 (cream), the OLD value persists on existing instances. Your | default: '#f5f0e8' Liquid fallback only fires when the setting is blank, not when it is set to the wrong value.

WRONG — old value persists
--ops-text: {{
  section.settings.text_color
    | default: '#f5f0e8'
}};
RIGHT — guaranteed cream
--ops-text: #f5f0e8;

Hardcode brand colors in CSS. Do not read from section.settings.text_color for tokens that should be brand-locked. The schema setting can stay for compatibility, but the runtime CSS variable should never depend on it.


The 18-Round Build Sequence

Each round is a discrete push. The trigger shows what user signal or audit finding kicked it off. The prompt shows what the AI agent was asked, paste-ready for re-use on the next project. The pattern describes what actually shipped.

Read the rounds in order the first time. After that, treat them as a reference index — most upgrade projects will not need all 18 rounds, but knowing which one matches your current signal saves the design work.

R3.0

Verified Facts Reframe Sweep

Trigger: User-pasted Brand Bible + audit report
Files: 13
Pattern: Bulk find/replace via Python script
The Prompt Scan every file in sections/ and templates/. For every match against the forbidden-strings list, replace with the supplier-attributed equivalent from the verified-facts whitelist. Report all changes in a diff. Validate JSON files still parse. Do not push yet — show me the diff first.

The Round 0 of any upgrade. Forbidden → reframed mapping defined upfront, mass-replaced, and gated on diff review before push. This is the round where you discover how much non-compliant copy lives in your theme.

R3.1

Build Cert Verification Table for Transparency Page

Trigger: Audit revealed transparency page lacked outbound cert registry links
Files: 2 (1 new section + 1 template)
The Prompt Build a new section dds-cert-verification-table.liquid. Render a 6-row table: GOTS / GRS / OCS / OEKO-TEX / PETA / Fair Trade. Each row has supplier name, cert number, registry URL, and a "Verify" link that opens the registry in a new tab. Use DDS dark palette. Include WebPage JSON-LD with about[] as DefinedTerm array. Wire into templates/page.product-transparency.liquid after the main content block.
R3.2

Answer-First Product Spec Block

Trigger: Product pages had no answer-first card; AI Overview visibility was zero
Files: 1 new section + 3 product templates
The Prompt Build dds-product-spec-block.liquid rendering a "Core Specifications" card. Read from product.metafields.specs.material, specs.weight, specs.fit, etc., with section-setting fallbacks. Render as semantic <dl>/<dt>/<dd> for AI extraction. Visible eyebrow tag "Answer First". Dark palette. Add section setting for product handle if used as a static block. Wire into 3 product templates.
R3.3

Product Passport JSON-LD Section

Trigger: EU Digital Product Passport alignment + structured-data crawler optimization
Files: 1 new section + 3 product templates
The Prompt Build dds-product-passport.liquid. Emit a single <script type="application/ld+json"> with @type: Product, full DPP-aligned fields: material, weight (QuantitativeValue), manufacturer (Printful), brand (DDS), and additionalProperty[] array for each cert. Schema settings: brand, manufacturer, supplier_default, cert toggles (cert_gots, cert_grs, cert_ocs, cert_oeko, cert_peta, cert_fairtrade), weight_oz, weight_gsm, oeko_cert_no, oeko_testing_institute, country_of_print. Wire into 3 product templates.
R3.5

SEO Optimizer Product (the SEO Magnet V2 product layer)

Trigger: Product pages needed full SEO Magnet — OG product card + FAQPage + BreadcrumbList + Speakable + visible Quick Answer
Files: 1 new section + 3 product templates
The Prompt Build seo-optimizer-product.liquid. Emit OG product meta (og:type=product with price + currency), Twitter product card, Organization JSON-LD, WebSite JSON-LD, BreadcrumbList (Home → Collection → Product), FAQPage with 6 question/answer slots, Speakable on product title + spec block, visible Quick Answer card + FAQ accordion (template-scoped CSS). Schema name: "SEO Optimizer (Product)" (must be ≤ 25 chars). Wire into 3 product templates as section id dds_seo_optimizer_product.
R3.6

SEO Optimizers for Homepage, Article, Search

Trigger: Audit found homepage missing OG/Twitter meta entirely, article missing OG, search missing noindex + base canonical
Files: 3 new sections + 3 JSON templates
The Prompt Build 3 new sections:

seo-optimizer-homepage.liquid — OG/Twitter meta + WebSite + SearchAction JSON-LD + Speakable hint. Default to og:type=website.
seo-optimizer-article.liquidog:type=article + article:author/published_time/modified_time/tag + scoped BlogPosting JSON-LD + Breadcrumb. Visible Quick Answer card.
seo-optimizer-search.liquid<meta name="robots" content="noindex,follow"> + canonical to base /search (no query string) + visible H1 + WebPage JSON-LD with SearchResultsPage mainEntity.

Wire each into respective JSON templates (index.json, article.json, search.json).
R3.7

Post-Audit Cleanup (6 items, one push)

Trigger: Full-store audit findings
Pattern: Each finding becomes one fix; bundle into one push because they share the same concern: audit-driven cleanup

Specific actions taken:

  1. collection-hero-grid-v4.liquid line 573 — demote <h1> to <h2> (was producing 2 H1s on collection pages)
  2. dds-collection-hero-genesis.liquid — inject defensive JS that demotes any non-canonical H1 to H2 on DOMContentLoaded (defense against Atelier main-collection-list H1)
  3. dds-404.liquid — add <meta name="robots" content="noindex,nofollow">
  4. 3 product templates — remove duplicate orphan seo_optimizer_product_* instances (keep only dds_seo_optimizer_product)
  5. seo-optimizer-search.liquid — defensive client-side canonical strip (removes any <link rel="canonical"> containing /search?… and installs clean base URL)
R3.10

Dark Theme Conversion (Per-Section)

Trigger: Brand direction shift to dark theme; existing sections rendered white on white
Pattern: One section's palette per push; hardcode tokens, do not depend on Atelier scheme emission (Principle 5)
The Prompt Convert sections/<name>.liquid from light theme to dark. Hardcode the DDS palette (--ops-bg: #0a1a0f, --ops-text: #f5f0e8, etc.) in the section's <style> block. Add background-color: var(--ops-bg) !important on the .ops-{{ section.id }} wrapper. Add defensive color: var(--ops-text) !important on all text children. Do NOT rely on Atelier emitting CSS for the custom UUID scheme — it does not. Push, then run live audit to verify the section renders dark in browser.
R3.10g — The Blanket !important Trap

Applying color: var(--ops-text) !important to all label elements broke the size-guide unit toggle in R3.15b: the toggle uses a cream-on-cream pill to indicate the selected state, which became invisible. The fix was a more-specific !important rule for the checked-state label. Lesson: defensive blanket rules need defensive exceptions.

R3.12

Cert-Bar to Marquee Swap on Collections

Trigger: User screenshot showed white cert-bar strip on collection pages (light theme leak)
Pattern: Disable the old section instance — do not delete, do not migrate, just disable
The Prompt Pull templates/collection.organic-t-shirts.json (and other collection templates) from live. Find the cert_bar_* section instance. Disable it (set "disabled": true). The existing dds_trust_bar_v2_* at the top of each collection template already provides the scrolling marquee — no replacement needed, just disable the old cert-bar. Validate symmetry + push.
R3.13

Audit Cleanup From Antigravity Browser Agent Report

Trigger: External audit revealed 5 forbidden-string categories across 12+ files
Pattern: Map each finding to specific files; bulk-reframe; validate; push

The reframe table that drove this round, paste-ready Python:

# OEKO-TEX brand-level → substrate-only
('OEKO-TEX certified', 'OEKO-TEX Standard 100 substrate'),

# Water/lifespan unverified claims
('95% Less Water',     'Lower water footprint'),
(r'(\d+)% less water', 'lower water footprint'),
('10+ Year Lifespan',  'Built to last for years'),

# Fair Trade attribution
('Fair Trade certified', 'Fair Trade supplier-certified'),

# US-only shipping → worldwide
('Free US Shipping', 'Free Worldwide Shipping'),
(r'(\d+)-(\d+) days U\.S\.A delivery',
 r'\1-\2 days US delivery (worldwide 5-22 days)'),
R3.15

Document-Level Event Delegation (Size Guide + Variant Picker)

Trigger: Live test showed size guide accordion does not expand, color picker does not swap image despite earlier fix
Diagnosis: IIFE bindings don't survive Atelier DOM morphing
Fix: Document-level delegation in capture phase (Principle 6)

This is the round that operationalized Principle 6 across the rest of the project. Every interactive element built after this round uses document-level delegation by default.

R3.15b

Size Guide Unit Toggle Text Contrast

Trigger: R3.10g blanket !important rule on labels broke the unit toggle (cream text on cream pill = invisible)
Pattern: More-specific !important override for the toggle
.ops-{id} .ops-sizeguide__toggle-radio:checked + .ops-sizeguide__toggle-label {
  color: var(--ops-bg) !important;        /* dark text on cream pill */
  background: var(--ops-text) !important;
  border-color: var(--ops-text) !important;
}

The general lesson: defensive blanket CSS needs scoped exceptions. The validation gate catches structural failures; only live audit catches contrast violations.

R3.16

Academy Tab-Nav Phantom Gap Fix

Trigger: Custom Academy page had sticky tab-nav with top: 64px to clear store header
Diagnosis: When store header auto-hides on scroll, the nav left a 64px transparent gap at viewport top
Fix: Change top: 64px to top: 0. Trust the store header's higher z-index to cover the nav when header is visible.
R3.17

CTA Copy Revision Across 11 Templates

Trigger: Shared CTA block had stale generic copy that didn't work across product types
Old (stale)
Heading:
  "Shop Sustainable Fashion with
   Total Confidence."

Description:
  "🌿 GOTS Organic.
   Sustainable fashion only."
New (substrate-attributed)
Heading:
  "See the Fit. Skip the Returns."

Description:
  "Virtually try on our
   certified-substrate apparel —
   GOTS organic cotton,
   GRS recycled polyester,
   OEKO-TEX Standard 100 — to see
   the perfect fit before you buy.

   Verifiable certifications.
   Marketing-fluff free."

The new copy is substrate-attributed (so it works across cotton + recycled poly + AOP product types) and explicitly anti-fluff. That's the brand voice locked in: verifiable certifications and marketing-fluff free.

R3.18

Reassign Collection Template via Shopify Admin GraphQL

Trigger: Antigravity audit flagged a collection as missing ItemList schema
Diagnosis: Collection was using default Atelier collection.json template instead of the custom suffix
Fix: One GraphQL mutation, no theme push
mutation {
  collectionUpdate(input: {
    id: "gid://shopify/Collection/<ID>",
    templateSuffix: "organic-sweatshirts"
  }) {
    collection { templateSuffix }
    userErrors { field message }
  }
}

No theme file changes. No push. Pure metadata swap. The collection now renders via templates/collection.organic-sweatshirts.json and inherits its full section stack including the ItemList schema.

R3.WebP

Image Optimization Pass (Antigravity Agent)

Trigger: User asked for WebP conversion across all theme-set images
Outcome: 71.54 MB saved (57.65% reduction); product page LCP −4.14s
Pattern: 5-phase workflow — Inventory, Convert, Upload, Replace, Push + Lighthouse

Full 5-phase workflow documented in the Image Optimization Playbook section below.


Reusable Prompt Library

Seven prompts that compose into the 18-round upgrade workflow. Each is paste-ready into Antigravity 2.0, Claude Code, or any agent with file-system and Shopify CLI access. The placeholders (in <ANGLE_BRACKETS>) get filled with your project specifics.

Prompt 1 — Build a New Liquid Section

Build sections/<NAME>.liquid following these requirements:

1.  Schema name ≤ 25 chars (Shopify validation).
2.  NO 'default': '' on text/textarea fields (Shopify rejects).
3.  Range fields: default must align to (min + N × step).
4.  Number fields use JSON numbers, not strings.
5.  Use # comments inside {% liquid %} blocks (NOT comment/endcomment).
6.  Render block wrapped in <section id="<PREFIX>-{{ section.id }}"> with the
    DDS dark palette CSS tokens hardcoded (don't depend on Atelier scheme emit).
7.  Include `color_scheme` schema setting for future use, but don't depend on it.
8.  background-color: var(--ops-bg) !important on wrapper (defeats Atelier body).
9.  Defensive `color: var(--ops-text) !important` rules on all text children.
10. If section has JS interactivity, use document-level event delegation in
    capture phase (NOT IIFE addEventListener which dies on Atelier DOM morph).
11. Schema settings include section_id-scoped CSS variables only.
12. Include a working preset block at end of schema.
13. Validate against the 8-check pre-push gate before declaring done.

Section purpose:        <PURPOSE>
Settings needed:        <LIST>
HTML structure:         <SKETCH>
JSON-LD schemas to emit: <LIST>

Prompt 2 — Bulk Reframe Forbidden Strings

Scan every .liquid and .json in sections/ and templates/.

For each forbidden string in the list below, find/replace with the verified
equivalent. Then re-grep to confirm zero remaining matches.

Validate every JSON file still parses after the swap (strip /*...*/ headers
first if present, then json.loads).

Show me a diff before pushing. Do not push if validation fails on any file.

Forbidden → Reframed:
  "OEKO-TEX certified"      → "OEKO-TEX Standard 100 substrate"
  "Made-to-order in Boston" → "Made-to-order, designed in Boston"
  "95% Less Water"          → "Lower water footprint"
  "Free US Shipping"        → "Free Worldwide Shipping"
  <...rest of the swap table...>

Bundle all changes into one push.

Prompt 3 — Live Browser Audit a Single Page

Use Claude in Chrome to:

1. Navigate to <URL>?preview_theme_id=<THEME_ID>
2. Wait 6 seconds for full render.
3. Capture a screenshot (save to disk).
4. Run this JS audit and return result as JSON:

(function(){
  const all = s => Array.from(document.querySelectorAll(s));
  const ldB = all('script[type="application/ld+json"]');
  let v = 0, e = 0, types = [];
  for (const b of ldB) {
    try { const j = JSON.parse(b.textContent); types.push(j['@type']); v++; }
    catch (x) { e++; }
  }
  const meta = n => document.querySelector('meta[' + n + ']')?.getAttribute('content');
  return JSON.stringify({
    page:        location.href,
    title:       document.title,
    h1Count:     all('h1').length,
    h1Texts:     all('h1').map(h => h.textContent.trim().slice(0, 60)),
    ogType:      meta('property="og:type"'),
    twitterCard: meta('name="twitter:card"'),
    robotsMeta:  meta('name="robots"'),
    canonical:   document.querySelector('link[rel="canonical"]')?.href,
    jsonLdValid: v,
    jsonLdErr:   e,
    jsonLdTypes: types.slice(0, 15)
  });
})()

5. Report findings as a scorecard:
   - PASS if H1=1, jsonLdErr=0, og type matches expectation
   - FAIL otherwise with specific deltas

Pages to audit:
- Homepage:         /
- Article:          /blogs/news/<slug>
- Search:           /search?q=organic
- 404:              /this-does-not-exist
- Collections list: /collections
- Collection page:  /collections/<slug>
- Product page:     /products/<slug>
- Cart (empty):     /cart
- Transparency:     /pages/<custom-page>

Prompt 4 — Push to Shopify CLI

Push the following files to testing theme:

shopify theme push --store <STORE>.myshopify.com `
  --theme <THEME_ID> `
  --nodelete `
  --only <file1> `
  --only <file2> `
  ...

Verify push completes with no errors. Save the CLI output to push-log.txt.

If push fails on a specific file, abort. Do not push partial.

After push succeeds, immediately run the live browser audit prompt on the
affected pages.

Prompt 5 — Pull Files From Live Theme (When Local Rebuild Is Stale)

Use Shopify Admin GraphQL to pull these files from theme <THEME_ID> into
./rebuild/<original-path>:

query {
  theme(id: "gid://shopify/OnlineStoreTheme/<THEME_ID>") {
    files(filenames: <LIST>, first: 50) {
      nodes {
        filename
        body { ... on OnlineStoreThemeFileBodyText { content } }
      }
    }
  }
}

If the response exceeds your context limit, the tool will save to disk. Read
the disk file, parse the JSON, and write each file's body to the matching
local path (preserving the original directory structure).

WARNING: this overwrites local edits. Diff local vs pulled before overwriting
if local has unpushed work.

Prompt 6 — Reassign Shopify Collection Template

Use Shopify Admin GraphQL:

mutation {
  collectionUpdate(input: {
    id: "gid://shopify/Collection/<ID>",
    templateSuffix: "<custom-template-suffix-without-collection.-prefix>"
  }) {
    collection { id handle templateSuffix }
    userErrors { field message }
  }
}

If userErrors is non-empty, abort and report. Otherwise verify the change
took effect by re-running the audit prompt on the affected collection URL.

Prompt 7 — Sweep All CTA / Trust / Promise Copy

For every template in templates/, inspect every section instance whose
type matches CTA-Block, cta-block, dds-trust-bar, or contains "cta" or
"trust" or "promise" in the type name.

Pull ALL text-bearing settings (heading, subheading, eyebrow_text,
description, button_text, feature_*, benefit_*, manifesto_*).

For each text value:
1. Run against the forbidden-strings whitelist
2. Check brand-voice compliance (no marketing-fluff claims;
   supplier-attributed certs; verifiable specifics only)
3. If issue found, propose a brand-compliant rewrite

Output a CSV: file_path, section_id, setting_key, current_value,
proposed_value, reason.

Wait for approval before applying.

The 8-Check Pre-Push Validation Script

The complete validate.py. Save this to your project root. Run before every push. If exit code is non-zero, fix the failure and re-run before pushing.

import json, re, sys

push_set = {
    'sections/file1.liquid': 'liquid',
    'templates/file2.json':  'json',
    # ... fill with your push set
}

forbidden = [
    'Made in Boston', 'Boston-made', 'Boston-crafted',
    'OEKO-TEX certified', 'Fair Trade certified',
    'Fair Trade Manufacturing', 'Toxin Free Dyes',
    '10-Year Care Guide', '10+ Year Lifespan',
    'Weekly sustainable style tips', 'Zero waste',
    '95% Less Water', '% less water',
    'Free US Shipping', 'U.S.A delivery', 'U.S.A shipping',
    'We hold 5', 'We hold 6', '100% Defect Guarantee',
    'Verify Our Certifications', 'exclusively organic',
    'Sustainable fashion only',
]

fail = False

# [1] NULL bytes
for f in push_set:
    with open(f, 'rb') as fh:
        n = fh.read().count(b'\x00')
    if n:
        print(f'NULL: {f} has {n} bytes ✗')
        fail = True

# [2] JSON validity + sections/order symmetry
for f, t in push_set.items():
    if t != 'json':
        continue
    with open(f) as fh:
        raw = fh.read()
    body = re.sub(r'^/\*.*?\*/\s*', '', raw, flags=re.S)
    try:
        d = json.loads(body)
        if 'sections' in d and 'order' in d:
            sym = set(d['sections'].keys()) == set(d['order'])
            if not sym:
                print(f'SYM: {f} sections != order ✗')
                fail = True
    except Exception as e:
        print(f'JSON: {f} ✗ {e}')
        fail = True

# [3] Schema validity + name ≤ 25
for f, t in push_set.items():
    if t != 'liquid':
        continue
    with open(f) as fh:
        src = fh.read()
    m = re.search(r'{%\s*schema\s*%}(.*?){%\s*endschema\s*%}',
                  src, flags=re.S)
    if not m:
        continue
    try:
        sch = json.loads(m.group(1))
        n = sch.get('name', '')
        if len(n) > 25:
            print(f'SCHEMA NAME: {f} "{n}" {len(n)} chars > 25 ✗')
            fail = True
    except Exception as e:
        print(f'SCHEMA PARSE: {f} ✗ {e}')
        fail = True

# [4] No "default": "" on text/textarea
for f, t in push_set.items():
    if t != 'liquid':
        continue
    with open(f) as fh:
        src = fh.read()
    m = re.search(r'{%\s*schema\s*%}(.*?){%\s*endschema\s*%}',
                  src, flags=re.S)
    if not m:
        continue
    sch = json.loads(m.group(1))
    for s in sch.get('settings', []):
        if s.get('type') in ('text', 'textarea') and s.get('default') == '':
            print(f'EMPTY DEFAULT: {f} setting={s.get("id")} ✗')
            fail = True

# [5] Range step alignment
for f, t in push_set.items():
    if t != 'liquid':
        continue
    with open(f) as fh:
        src = fh.read()
    m = re.search(r'{%\s*schema\s*%}(.*?){%\s*endschema\s*%}',
                  src, flags=re.S)
    if not m:
        continue
    sch = json.loads(m.group(1))
    for s in sch.get('settings', []):
        if s.get('type') == 'range':
            mn = s.get('min', 0)
            st = s.get('step', 1)
            df = s.get('default')
            if df is not None and (df - mn) % st != 0:
                print(f'RANGE STEP: {f} {s.get("id")} '
                      f'default={df} not aligned to step={st} ✗')
                fail = True

# [6] Liquid block balance
pairs = [('if', 'endif'), ('for', 'endfor'), ('case', 'endcase'),
         ('comment', 'endcomment'), ('schema', 'endschema'),
         ('style', 'endstyle'), ('javascript', 'endjavascript'),
         ('paginate', 'endpaginate'), ('form', 'endform'),
         ('capture', 'endcapture'), ('unless', 'endunless')]
for f, t in push_set.items():
    if t != 'liquid':
        continue
    with open(f) as fh:
        src = fh.read()
    for o, c in pairs:
        oc = len(re.findall(r'{%-?\s*' + o + r'\b', src))
        cc = len(re.findall(r'{%-?\s*' + c + r'\b', src))
        if oc != cc:
            print(f'BALANCE: {f} {o}={oc}/{c}={cc} ✗')
            fail = True

# [7] No comment/endcomment inside {% liquid %} blocks
for f, t in push_set.items():
    if t != 'liquid':
        continue
    with open(f) as fh:
        src = fh.read()
    blocks = re.findall(r'{%-?\s*liquid\b(.*?)-?%}',
                       src, flags=re.S)
    for i, b in enumerate(blocks):
        if re.search(r'\bcomment\b|\bendcomment\b', b):
            print(f'LIQUID BLOCK COMMENT: {f} block {i} contains '
                  f'comment/endcomment ✗')
            fail = True

# [8] Forbidden strings
for f in push_set:
    with open(f) as fh:
        src = fh.read()
    hits = [s for s in forbidden if s in src]
    if hits:
        print(f'FORBIDDEN: {f} contains {hits} ✗')
        fail = True

print('\nRESULT:', 'FAIL — DO NOT PUSH' if fail else 'PASS — safe to push')
sys.exit(1 if fail else 0)

How to Run It

python3 validate.py && shopify theme push --store ... --only ...

The && means the push only runs if validation exits zero. This is the gate. Without it, you are pushing on hope.


Live Browser Audit Workflow

After every push, this 4-step verification runs against the testing theme. Use Chrome MCP (or any browser-control agent — the pattern works the same).

Step 1 — Resize Chrome to Standard Viewport

mcp__Claude_in_Chrome__resize_window {
  width: 1440,
  height: 900
}

Step 2 — Navigate + Screenshot + JS Audit (Batched)

mcp__Claude_in_Chrome__browser_batch [
  { name: "navigate",
    input: { url: "<URL>?preview_theme_id=<ID>" }},
  { name: "computer",
    input: { action: "wait", duration: 6 }},
  { name: "computer",
    input: { action: "screenshot", save_to_disk: true }},
  { name: "javascript_tool",
    input: { text: "<audit JS — same as Prompt 3 above>" }}
]

Step 3 — Scroll-Screenshot for Long Pages

mcp__Claude_in_Chrome__browser_batch [
  { name: "computer",
    input: { action: "scroll", scroll_direction: "down",
             scroll_amount: 10 }},
  { name: "computer",
    input: { action: "wait", duration: 2 }},
  { name: "computer",
    input: { action: "screenshot", save_to_disk: true }},
  // repeat 2-3 times for full-page coverage
]

Step 4 — Interactive Checks (Only If Behavior Changed)

// Test variant picker
const sel = document.querySelector('select[name="options[Color]"]');
sel.value = sel.options[1].value;
sel.dispatchEvent(new Event('change', { bubbles: true }));
// Then verify image src changed, label updated, URL updated

// Test size guide
document.querySelector('[data-sizeguide-trigger]').click();
// Then verify .ops-sizeguide__content has .is-open class
Chrome MCP Blocks Responses Containing Query Strings

If your JS returns a string that includes ?width=1200 or similar query-string-looking content, the response gets blocked with [BLOCKED: Cookie/query string data]. Workaround: Strip query strings in the JS output. Replace s with s.replace(/[?&].*$/, '') or output a hash/fingerprint instead of raw URLs.


Architecture Reference

The final shape of the rebuilt DDS theme: which sections exist, what each does, where each is wired, and the standardized section orders for product, collection, and homepage templates. Use this as a transplant manifest when porting the pattern to another store.

Section Files (Final Set)

File Purpose Wired Into
dds-cert-verification-table.liquidOutbound cert registry links + supply-chain disclosure + WebPage JSON-LDtransparency page template
dds-product-spec-block.liquidAnswer-First Core Specifications card7 product templates
dds-product-passport.liquidDPP-aligned Product JSON-LD7 product templates
seo-optimizer-product.liquidProduct OG/Twitter + FAQPage + Breadcrumb + Speakable + visible Quick Answer7 product templates
seo-optimizer-homepage.liquidHomepage OG/Twitter + WebSite + SearchActionindex.json
seo-optimizer-article.liquidArticle OG + BlogPosting + Breadcrumb + Quick Answerarticle.json
seo-optimizer-search.liquidSearch noindex + base canonical + H1 + SearchResultsPage JSON-LDsearch.json
seo-optimizer-collection.liquidCollection OG + CollectionPage + ItemList + OfferCatalogall collection templates
dds-blog-articles.liquidBlog filter + search + 3-col grid with manual paginationblog.json
seo-optimizer-blog-news.liquidBlog SEO Magnetblog.json
dds-collection-hero-genesis.liquidPer-collection branded hero + dual CTAs + defensive H1 demote JSall collection templates
dds-collection-editorial.liquidDark-themed editorial section under product gridall collection templates
dds-collection-cross-strip.liquidCross-collection cert stripall collection templates
dds-collection-quick-answer.liquidAnswer-First card for collectionsall collection templates
dds-collection-trust-strip.liquidTrust signals stripall collection templates
organic-product-v2.liquidMain product card (overrides Atelier's product-information)7 product templates

Standardized Product Template Section Order

0. dds-trust-bar-v2          (scrolling marquee, dark)
1. organic-product-v2         (main product card — image, variants, price, Add to Cart)
2. main (product-information) [disabled]
3. dds-product-spec-block     (Answer-First Core Specifications)
4. dds-product-passport       (Product JSON-LD)
5. CTA-Block                  (Try Before You Buy / brand CTA)
6. product-recommendations
7. seo_optimizer_product_*    [disabled legacy]
8. dds_seo_optimizer_product  (R3.5 SEO Magnet, enabled)

Standardized Collection Template Section Order

0. dds-collection-hero-genesis  (per-collection branded hero)
1. dds-trust-bar-v2             (scrolling marquee)
2. dds-collection-quick-answer  (Answer-First card)
3. dds-collection-trust-strip   (trust signals)
4. collection-hero-grid-v4      (product grid with filters)
5. dds-collection-editorial     (editorial section)
6. dds-collection-cross-strip   (cross-cert strip)
7. CTA-Block                    (brand CTA)
8. main (main-collection)       [disabled]
9. seo-optimizer-collection     (CollectionPage + ItemList + OfferCatalog)

Standardized Homepage Section Order

0. dds-hero-genesis              (genesis hero with ClothingStore + FAQPage + Speakable)
1. dds-trust-bar-v2              (marquee)
2. dds-showcase-v3               (featured collections)
3. judge-account-cta             (account CTA)
4. cta-block                     (brand CTA)
5. dds-organic-quiz-v2           (interactive quiz)
6. dds-signal-beacon             (Boston local time + cert count)
7. dds-platform-links            (social/external)
8. ai-homepage-seo-manager-v5    (rich JSON-LD for AI overviews)
9. dds_seo_home                  (R3.6 OG/Twitter + WebSite + SearchAction)

Custom Color Scheme (config/settings_data.json)

"scheme-09beadd5-6088-4295-adf3-071103c616a9": {
  "settings": {
    "background":                      "#0a1a0f",
    "foreground_heading":              "#fafaf7",
    "foreground":                      "#f5f0e8",
    "primary":                         "#c9a84c",
    "primary_hover":                   "#e8c84a",
    "border":                          "#1a5c45",
    "input_background":                "#0d2818",
    "input_text_color":                "#f5f0e8",
    "input_border_color":              "#1a5c45",
    "primary_button_background":       "#c9a84c",
    "primary_button_text":             "#0a1a0f",
    "primary_button_border":           "#c9a84c",
    "primary_button_hover_background": "#e8c84a",
    "primary_button_hover_text":       "#0a1a0f"
  }
}
Atelier Does NOT Auto-Emit CSS For This Custom Scheme

The scheme exists in settings_data but does not produce --color-background: 10 26 15 at runtime. Always hardcode in section CSS instead of depending on --color-* vars. See Principle 5 for the full diagnosis.


Shopify Liquid + JSON Gotchas

The traps that the 8-check pre-push validation exists to catch. Every gotcha below caused at least one failed push during the DDS rebuild before the gate was built. The gate exists because of these.

Liquid: Don't Use comment / endcomment Inside {% liquid %} Shorthand Blocks

The Shopify parser breaks with "comment tag was never closed" when comment tags appear inside the liquid shorthand block. Use the # single-line comment syntax instead.

WRONG — parser breaks
{%- liquid
  comment
    Why this section exists
  endcomment
  assign x = 1
-%}
RIGHT — # comments
{%- liquid
  # Why this section exists
  assign x = 1
-%}

Top-level {%- comment -%} outside any liquid block works fine. The bug is specifically the liquid shorthand parser failing on nested comment tags.

Schema JSON: No "default": "" on Text/Textarea Fields

Shopify rejects empty string defaults on text and textarea settings. Omit the default entirely instead.

WRONG — push rejected
{
  "type":    "text",
  "id":      "foo",
  "default": ""
}
RIGHT — omit default
{
  "type": "text",
  "id":   "foo"
}

Range default Must Align to step

The default value must satisfy (default - min) % step == 0. Otherwise Shopify rejects the schema.

WRONG — 33 not aligned to step 5
{
  "type":    "range",
  "id":      "x",
  "min":     0,
  "max":     100,
  "step":    5,
  "default": 33
}
RIGHT — 30 or 35
{
  "type":    "range",
  "id":      "x",
  "min":     0,
  "max":     100,
  "step":    5,
  "default": 30
}

Schema name Max 25 Characters

Anything longer fails push. Names like "SEO Optimizer (Product)" are exactly 23 chars and pass; "SEO Optimizer for Products" is 26 chars and fails. The validation script catches this before the CLI ever sees the file.

Template JSONs: sections Keys Must Match order Array Exactly

Orphan sections (present in sections object but not in order array) cause "Section id 'X' must exist in order" rejection. Same for the reverse — IDs in order that don't exist in sections.

# Always validate after edits:
assert set(d['sections'].keys()) == set(d['order'])

Number Fields Must Be JSON Numbers, Not Strings

WRONG — string
"free_shipping_threshold": "0"
RIGHT — number
"free_shipping_threshold": 0

Template JSONs May Have /* … */ Comment Headers — Strip Before Parsing

import re

with open(p) as f:
    raw = f.read()

# Strip the header before json.loads
body = re.sub(r'^/\*.*?\*/\s*', '', raw, flags=re.S)
d = json.loads(body)

# Preserve header when writing back
out = (re.match(r'^(/\*.*?\*/\s*)', raw, flags=re.S).group(1) +
       json.dumps(d, indent=2) + '\n')

File Writing: The 17KB Silent Truncation Risk

The Write Tool Can Silently Truncate Files ≥ 17KB

Use bash cat > file << 'EOF' heredoc for large files. Always verify with ls -la after writing. The truncation is silent — no error, no warning — and the truncated file passes JSON.parse on its head but renders broken in the browser. This burned an entire afternoon during the DDS rebuild.

NULL Bytes: Grep After Every File Write

Some tools (Edit, certain bash redirects) can append trailing NULL bytes. NULL bytes break JSON parsers and Shopify validation. Always grep for \x00 after any file write.

with open(f, 'rb') as fh:
    if b'\x00' in fh.read():
        # Strip them, then rewrite the file
        ...

Image Optimization Playbook

The WebP conversion that achieved 57.65% storage savings and −4.14s LCP on product pages followed this 5-phase pattern. The full pass took roughly 90 minutes of Antigravity agent time on a theme with ~150 images.

Phase 1 — Inventory

Pull the live theme. Scan every templates/*.json + sections/*.liquid + snippets/*.liquid + assets/* for image references:

  • Collection record images (Shopify Admin GraphQL: collections { image { url } })
  • Template JSON keys matching image, background_image, hero_image, og_image, featured_image, *_image
  • Section Liquid: {{ section.settings.X | image_url }} patterns
  • Section Liquid: hardcoded https://cdn.shopify.com/... URLs
  • assets/*.{png,jpg,jpeg}

Save inventory to CSV: file_path, setting_key, image_url, source_format, current_bytes, dimensions.

Phase 2 — Convert (sharp encoder)

import sharp from 'sharp';

// Apparel + lifestyle photography
await sharp(input)
  .webp({ quality: 82, smartSubsample: true, effort: 6 })
  .toFile(output);

// Cert badges / icons / logos (small, transparent PNG)
await sharp(input)
  .webp({ lossless: true, alphaQuality: 100, effort: 6 })
  .toFile(output);

// OG share images (1200×630)
await sharp(input)
  .webp({ quality: 85, effort: 6 })
  .toFile(output);
// Keep original PNG/JPG as fallback (LinkedIn/Twitter inconsistent on WebP cards)

Skip if:

  • Source < 10 KB
  • Source is SVG
  • Source already WebP
  • Conversion increased file size (re-check after encode)

Phase 3 — Upload

Use Shopify Admin GraphQL stagedUploadsCreate → upload to S3 staging URL → fileCreate mutation:

mutation StagedUploadsCreate($input: [StagedUploadInput!]!) {
  stagedUploadsCreate(input: $input) {
    stagedTargets {
      url
      resourceUrl
      parameters { name value }
    }
    userErrors { field message }
  }
}

mutation FileCreate($files: [FileCreateInput!]!) {
  fileCreate(files: $files) {
    files {
      ... on MediaImage { id image { url } }
    }
    userErrors { field message }
  }
}
Key Trick

Use original filename with .webp extension so shopify://shop_images/foo.webp URLs in template JSON keep working without editing every reference. Build url-map.json mapping old URL → new WebP URL.

Phase 4 — Replace URLs

For each entry in url-map.json:

  1. Scan modified templates — replace exact old URL with new WebP URL
  2. Scan modified sections — same for any hardcoded CDN URLs (NOT for image_url filtered references — those auto-negotiate)
  3. For collection record images: collectionUpdate mutation with new image URL

Validate after each batch:

  • Every old URL appears ZERO times in modified files
  • Every JSON still parses

Phase 5 — Push + Lighthouse

shopify theme push --store ... --theme ... --nodelete --only <changed files>

# Then run Lighthouse 3 times per page and average (avoid dev-mode variance)
npx lighthouse "<URL>" --output json --output-path before/after.json

Expected Savings

Image Type Quality Setting Expected Savings
Apparel JPGs (hero/lifestyle)WebP q8260-75%
Cert badges/logos (PNG with alpha)WebP lossless30-50%
OG share imagesWebP q85 (keep original fallback)20-40%
Tiny mono-color PNGs(often skipped)0% — sometimes WebP is larger

Total theme image weight should drop 55-70%. The DDS pass landed at 57.65%, square in the expected range.

Why Some Page LCPs Don't Improve

Shopify's CDN auto-negotiates WebP via Accept: image/webp header on {{ image | image_url }} calls. Pages using that filter were ALREADY serving WebP to modern browsers — your storage savings are real, but transfer-byte savings to end users are zero.

Pages with hardcoded <img src="https://cdn.shopify.com/..."> (direct URLs, no image_url filter) DID transfer the raw original. Those are where you see LCP improvements after the swap. Product hero images are the most common case — which is exactly why the DDS −4.14s win landed on product pages specifically.


Troubleshooting

Five specific failure modes encountered during the DDS rebuild, with the exact diagnosis sequence that resolved each one. If you hit a symptom matching one of these, walk the checklist before debugging from scratch.

Q: My dark theme section renders white. What's wrong?

Walk this checklist in order:

  1. Does .ops-{{ section.id }} (or your wrapper class) have background-color: var(--ops-bg) !important?
  2. Does the section root <div> actually have your wrapper class? (Inspect in DevTools — Atelier sometimes nests sections differently than expected.)
  3. Are your --ops-* CSS variables HARDCODED in the section's CSS, not rgb(var(--color-background))? (Atelier doesn't emit CSS for custom UUID schemes.)
  4. Does section.settings.text_color persist an old #1f2937 value from a previous schema iteration? Hardcode your text color in CSS, don't read from settings.

If all 4 pass and it's still white, run the live DOM inspection JS from R3.10g to see what's actually computed.

Q: My event listeners don't fire. What's wrong?

  1. Is your IIFE running? (Check by adding window.__myFlag = true; early in the IIFE and inspect in console.)
  2. Is container null? (Atelier wraps sections in shopify-section-{id} div — your document.querySelector('.ops-' + sectionId) might miss.)
  3. Are the elements you're binding to inside container at IIFE-run time? (Watch for sub-section rendering.)
  4. Most likely: Atelier morphs the DOM during navigation. Switch to document-level event delegation in capture phase. See Principle 6.

Q: Push succeeds but my changes don't appear. What's wrong?

  1. Shopify CDN serves CSS with a few-minute cache. Hard-refresh (Ctrl+Shift+R) or visit with ?cache_bust=<random> query string.
  2. You may be viewing the LIVE theme, not the testing theme. Always include ?preview_theme_id=<ID> in the URL.
  3. Check the section file was actually saved (ls -la <file>) and the push command output included your file.

Q: JSON-LD parse errors on /collections list page

Live test via JSON.parse on every <script type="application/ld+json">. Most common cause: a Liquid variable inserts blank/undefined in a position that needs a number or quoted string:

WRONG — blank breaks JSON
"numberOfItems": {{ product_count }}
RIGHT — default to 0
"numberOfItems": {{ product_count | default: 0 }}

Q: My OG image URL fallback isn't appearing in twitter:image

  1. Verify the section setting og_image_url is populated in the template JSON.
  2. Verify the section Liquid emits <meta name="twitter:image"> in an {%- elsif og_image_url != blank -%} branch.
  3. Re-test by pasting the URL into LinkedIn's Post Inspector — that forces a fresh fetch.

Closing Notes for the Next Agent

If you're a fresh Claude Code or Antigravity instance reading this, here's how to use this playbook on a new Shopify store:

  1. Read the entire foundation section first. Brand Bible + forbidden strings + operating principles before any code.
  2. Adapt the Verified Facts to the new brand. Most projects will have their own supplier chain, certs, materials. The PATTERN is the same; the specific facts change.
  3. Don't skip the live audit between rounds. Static validation is necessary but not sufficient. The Atelier morphing bug doesn't show in static checks.
  4. One concern per push. Resist the temptation to batch unrelated fixes. If the push fails, you'll thank yourself.
  5. The 8-check pre-push gate is non-negotiable. Build the validation script first, then build the features it gates.
  6. Save all reports to rebuild/docs/. Future agents (or human auditors) need the trail.
  7. When you don't know if something works, ASK THE USER TO TEST. Push to a TESTING theme, never directly to live. Let the user verify before publishing.
The Compounding Pattern

The pattern that took 18 rounds on DDS Boston should take fewer rounds on the next project because:

  • Brand Bible is upfront, no mid-stream reframing
  • Forbidden strings list is comprehensive
  • Atelier scheme/morphing/cascade gotchas are documented
  • The 8-check gate catches all classes of failure before push

Build, validate, push, audit, repeat. Then write the next masterclass.


Frequently Asked Questions

The 18 most common questions about applying this playbook on a new Shopify store. The FAQPage JSON-LD schema at the top of the page surfaces these in AI overviews and rich results.

What is the DDS Homepage Upgrade Masterclass?

A reproducible AI-driven playbook for upgrading a Shopify Online Store 2.0 theme end-to-end. It captures the exact pattern used to upgrade DDS Boston from a generic Atelier installation to a brand-compliant, dark-themed, SEO-optimized store: foundation Brand Bible, 8 operating principles, 18 build rounds, reusable prompt library, 8-check pre-push validation, image optimization workflow, and the Atelier-specific gotchas that burned hours of debugging.

Why do you need a Brand Bible before writing any code?

Without verified facts and a forbidden-strings whitelist, AI generates plausible but non-compliant copy. The Brand Bible includes three things: a Verified Facts Whitelist (every claim the brand can make), a Forbidden Strings Whitelist (every claim that triggers an abort), and Brand Color Tokens plus Typography. Skipping this step guarantees compliance violations downstream.

Why one concern per round?

Mixing concerns (dark theme plus new SEO plus content reframe plus image swap) makes pre-push validation impossible to interpret. If a 12-file push breaks, you cannot tell whether the dark theme tokens or the JSON-LD parse fix or the cert reframe caused the failure. Push one concern, verify, then push the next concern.

What are the 8 pre-push validation checks?

(1) NULL byte sweep. (2) JSON validity plus sections-equals-order symmetry. (3) Schema validity plus name 25-char limit. (4) No default empty-string on text fields. (5) Range step alignment. (6) Liquid block balance. (7) No comment/endcomment inside liquid blocks. (8) Forbidden strings sweep. If any check fails, the push is aborted and the failure is fixed before retry.

Why does Atelier not auto-emit CSS for custom color schemes?

Atelier emits CSS for built-in schemes (scheme-1 through scheme-6) but does NOT emit CSS for custom UUID schemes. Even if you add color_scheme to a section schema and default it to a UUID scheme, color-background falls back to white at runtime. The fix is to hardcode the palette in the section's CSS, not rely on scheme emission. This burned 5 hours of debugging on the DDS project.

Why do event listeners stop firing after Atelier navigation?

Stock section IIFE patterns add event listeners on page load. Atelier morphs sections during navigation, so listeners attached to morphed elements no longer fire. The solution is document-level event delegation in capture phase: document.addEventListener('click', ...) with closest data-trigger and capture phase true. Listeners on document survive any morphing.

How much can WebP conversion improve theme performance?

The DDS conversion saved 71.54 MB of image weight (57.65% reduction) and improved product page LCP by 4.14 seconds. Apparel and lifestyle JPGs at WebP quality 82 yield 60-75% savings. Cert badges and logos at lossless WebP yield 30-50%. OG share images at quality 85 yield 20-40%. Pages using image_url filter were already serving WebP via Shopify CDN auto-negotiation, so storage savings are real but transfer savings to modern browsers were zero on those paths.

Can you reassign a Shopify collection to a different template without rebuilding?

Yes. Use one GraphQL mutation against Shopify Admin API: collectionUpdate with id and templateSuffix. The collection now renders via templates/collection.<suffix>.json. No theme push needed, no file changes. Verify in DevTools that the new template sections appear.

How do you stop AI from generating fake certifications?

Build the Forbidden Strings Whitelist before the AI sees the codebase. Any push that contains a banned phrase (We hold 5 certifications, OEKO-TEX certified at the brand level, Made in Boston when the brand is POD-based) aborts at the pre-push gate. The whitelist is comprehensive and gets updated every time a new violation is caught in audit.

What is the live browser audit workflow?

Four steps after every push: (1) Resize Chrome to standard viewport. (2) Navigate, screenshot, run a JS audit returning H1 count, JSON-LD valid count, OG meta, canonical, robots meta, and forbidden-string sweep on rendered text. (3) Scroll-screenshot for long pages. (4) Interactive checks for any user-facing interaction that changed (variant picker, size guide accordion). Static validation passes do not equal live behavior works.

Can a fresh Claude Code instance use this playbook?

Yes. The playbook is intentionally self-contained: all prompts, validation criteria, gotchas, and architectural decisions are documented inline. A fresh agent reads the foundation first (Brand Bible, forbidden strings, operating principles), adapts the Verified Facts to the new brand, then proceeds through the 18 rounds. The PATTERN is reusable, the specific facts vary per project.

How do you keep settings from breaking when schemas change?

If a section instance has a stored text_color value like dark gray from an old light-theme default, and you later change the schema default to cream, the old value persists on existing instances. The Liquid default filter only fires when the setting is blank, not when it is set to the wrong value. Solution: hardcode brand colors in CSS, do not rely on section.settings.text_color. Use CSS variables defined in the section's style block instead.

What is the Shopify schema name limit?

25 characters maximum. Anything longer fails push. This is one of the most common gotchas when migrating section schemas. The 8-check pre-push validation script catches this before the file ever hits the CLI.

Why can templates JSON fail with "Section id must exist in order"?

Templates have two parallel keys: sections (object of section instances) and order (array of section IDs). Both must reference the same keys exactly. Orphan sections in sections but not in order cause rejection. Same for IDs in order that don't exist in sections. Always validate after edits with assert set(sections.keys()) == set(order).

What is the silent multi-phase build pattern?

For any task touching more than 4 files or more than 1,000 lines of code, the agent works silently in a scratch workspace, validates each phase, and only presents the final assembled output via present_files. Intermediate code is never shown. Report format during silent build: "Phase N complete" with a one-line description. This prevents context bloat and lets the user focus on the final deliverable.

Should you push directly to your live theme?

Never. Always push to a testing theme first, run the live browser audit, verify the affected pages render correctly, and only then publish via Shopify admin. Even production-grade tools can ship breaking behavior on edge cases. The testing theme is the buffer between a bad push and a customer-visible regression.

How do you handle NULL bytes in pushed files?

Some tools (Edit, certain bash redirects) can append trailing NULL bytes that break JSON parsers and Shopify validation. The pre-push validation script reads each file in binary mode and counts NULL bytes. Any non-zero count aborts the push. The fix is to rewrite the file using a clean writer (Python open in text mode, or bash heredoc with EOF guard).

What is the reframe pattern for OEKO-TEX claims?

DDS holds NO direct cert licenses. OEKO-TEX is held at the substrate level by Stanley/Stella, econscious, and Printful. The forbidden phrase is "OEKO-TEX certified" at the brand level. The compliant reframe is "OEKO-TEX Standard 100 substrate", which attributes the certification accurately to the material rather than the brand. Same pattern for Fair Trade: forbidden "Fair Trade certified", compliant "Fair Trade supplier-certified".