[object Object]

← back to Designer Wallcoverings

Stage restrained hero 'Book an Appointment' secondary CTA on dev theme 144076931123 (DTD verdict C)

79b1b848999b6f075ddad686e8933ed27ec17cdc · 2026-06-29 16:59:08 -0700 · Steve

Files touched

Diff

commit 79b1b848999b6f075ddad686e8933ed27ec17cdc
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jun 29 16:59:08 2026 -0700

    Stage restrained hero 'Book an Appointment' secondary CTA on dev theme 144076931123 (DTD verdict C)
---
 shopify/hero-appt-cta-dev/README.md                |  26 ++
 .../hero-appt-cta-dev/home-slideshow-slide.liquid  |  78 ++++++
 shopify/hero-appt-cta-dev/home-slideshow.liquid    | 281 +++++++++++++++++++++
 shopify/hero-appt-cta-dev/make-dev-copy.py         |  67 +++++
 shopify/hero-appt-cta-dev/push-edits-v2.py         |  34 +++
 shopify/hero-appt-cta-dev/push-edits.py            |  78 ++++++
 6 files changed, 564 insertions(+)

diff --git a/shopify/hero-appt-cta-dev/README.md b/shopify/hero-appt-cta-dev/README.md
new file mode 100644
index 00000000..0d521a50
--- /dev/null
+++ b/shopify/hero-appt-cta-dev/README.md
@@ -0,0 +1,26 @@
+# Hero "Book an Appointment" secondary CTA — DEV theme staging (DTD verdict C, 2026-06-29)
+
+Restrained secondary CTA added to the DW homepage hero (home-slideshow section
+`home_slideshow_EXmkyD`, first slide `slide_8gwUz8`) linking to `/pages/book-appointment`.
+
+STAGED ONLY — never published. Dev theme: **144076931123 "5.9.1 [Dev] hero-appt-CTA"**
+(unpublished copy of live theme 144070803507 "Steves Version 5.9.0 [Dev]").
+
+## Files
+- `make-dev-copy.py`        — created the unpublished dev theme by asset-copy (401 assets).
+- `home-slideshow-slide.liquid` — snippet: renders the secondary CTA in its own `.home-slideshow-slide-cta2-row`.
+- `home-slideshow.liquid`   — section: added `cta2`/`url2` schema fields + the restrained hairline-pill CSS.
+- `push-edits.py` / `push-edits-v2.py` — pushed the 3 assets to the dev theme (backup + verify, never main).
+
+## What changed
+- index.json (data) first hero slide: `cta2="Book an Appointment"`, `url2="/pages/book-appointment"`.
+- Snippet renders cta2 only when both cta2+url2 are set (other slides unaffected).
+- Pill is deliberately subordinate to the primary "View Collection": Lora 400 (vs 700), ~12px,
+  0.18em tracking, hairline border-radius 999px, translucent `rgba(0,0,0,.46)` backdrop-blur backing.
+  APCA Lc 79.7 worst-case (white on the backing over the lightest photo region) — clears the
+  Lc>=75 small-text gate.
+
+## Preview
+https://designer-laboratory-sandbox.myshopify.com/?preview_theme_id=144076931123
+
+## NOT published. Go-live + rollback are Steve's call (see task report).
diff --git a/shopify/hero-appt-cta-dev/home-slideshow-slide.liquid b/shopify/hero-appt-cta-dev/home-slideshow-slide.liquid
new file mode 100644
index 00000000..d0bf759f
--- /dev/null
+++ b/shopify/hero-appt-cta-dev/home-slideshow-slide.liquid
@@ -0,0 +1,78 @@
+{% assign block = block | default: nil %}
+{% assign image = onboard_image %}
+{% assign contentAlignment = contentAlignment %}
+{%- assign url = '' -%}
+{%- assign heading = 'onboarding.slideshow.title' | t -%}
+{%- assign subheading = 'onboarding.slideshow.subtitle' | t -%}
+{%- assign cta = 'onboarding.slideshow.cta' | t -%}
+{%- assign cta2 = '' -%}
+{%- assign url2 = '' -%}
+
+{% if block %}
+  {%- assign contentAlignment = block.settings.content-alignment -%}
+  {%- assign overlayColor = block.settings.overlay-color -%}
+  {%- assign heading = block.settings.title | escape -%}
+  {%- assign subheading = block.settings.subheading | escape -%}
+  {%- assign cta = block.settings.cta | escape -%}
+  {%- assign url = block.settings.url -%}
+  {%- assign cta2 = block.settings.cta2 | escape -%}
+  {%- assign url2 = block.settings.url2 -%}
+  {%- if block.settings.image -%}
+    {%- capture image -%}
+        {%
+          render 'rimg',
+          img: block.settings.image,
+          size: '1440x640',
+          lazy: true,
+          focal_point: block.settings.image.presentation.focal_point,
+        %}
+    {%- endcapture -%}
+    {%- endif -%}
+{%- endif -%}
+
+<article
+  class="
+    home-slideshow-slide
+    home-slideshow-slide-{{ slideIndex }}
+    home-slideshow-slide-alignment-{{ contentAlignment }}
+  "
+  data-slideshow-slide
+  data-slide-index="{{ slideIndex | minus: 1 }}"
+  {{ block.shopify_attributes }}
+>
+    <figure class="home-slideshow-slide-image">
+      {%- if url != blank -%}
+        <a href="{{ url }}">
+          {{ image }}
+        </a>
+      {%- else -%}
+        {{ image }}
+      {%- endif -%}
+    </figure>
+
+    {% if heading != blank or subheading != blank or cta != blank %}
+      <div class="home-slideshow-slide-content">
+        <div class="home-slideshow-slide-content-inner">
+          {% if heading != blank %}
+            <h1 class="home-slideshow-slide-heading" style="{%- if overlayColor -%}color: {{ overlayColor }};{%- endif -%}">{{ heading }}</h1>
+          {% endif %}
+
+          {% if subheading != blank %}
+            <p class="home-slideshow-slide-subheading" style="{%- if overlayColor -%}color: {{ overlayColor }};{%- endif -%}">{{ subheading }}</p>
+          {% endif %}
+
+          {% if cta != blank %}
+            <a class="home-slideshow-slide-cta button" {% if url != blank %}href="{{ url }}"{% endif %}>{{ cta }}</a>
+          {% endif %}
+
+          {%- comment -%} DW restrained secondary CTA — hairline consultation pill on its own row (DTD verdict C, 2026-06-29) {%- endcomment -%}
+          {% if cta2 != blank and url2 != blank %}
+            <div class="home-slideshow-slide-cta2-row">
+              <a class="home-slideshow-slide-cta2" href="{{ url2 }}" style="{%- if overlayColor -%}color: {{ overlayColor }}; border-color: {{ overlayColor }};{%- endif -%}">{{ cta2 }}</a>
+            </div>
+          {% endif %}
+        </div>
+      </div>
+    {% endif %}
+  </figure>
+</article>
diff --git a/shopify/hero-appt-cta-dev/home-slideshow.liquid b/shopify/hero-appt-cta-dev/home-slideshow.liquid
new file mode 100644
index 00000000..5849d951
--- /dev/null
+++ b/shopify/hero-appt-cta-dev/home-slideshow.liquid
@@ -0,0 +1,281 @@
+<script
+  type="application/json"
+  data-section-type="dynamic-slideshow"
+  data-section-id="{{ section.id }}"
+  data-section-data
+>
+{
+    "autoplay": {{ section.settings.autoplay }},
+    "autoplayDelay": {{ section.settings.autoplay_delay }},
+    "autoplayHoverPause": {{ section.settings.autoplay-hover-pause }}
+  }
+</script>
+
+{%- assign slideIndex = 0 -%}
+{%- assign slide_count = section.blocks.size -%}
+
+<section
+  class="home-slideshow home-slideshow-layout-{{ section.settings.layout }}"
+  data-slideshow
+  data-slideshow-layout="{{ section.settings.layout }}"
+  data-slideshow-slides="{{ slide_count }}"
+>
+  {%- unless section.blocks.size == 0 -%}
+    {%- for block in section.blocks -%}
+      {%- case block.type -%}
+      {%- when 'slide' -%}
+        {%- assign slideIndex = slideIndex | plus: 1 -%}
+        {%- assign lifestyleIndex = slideIndex | modulo: 2 | plus: 1 -%}
+        {%- assign lifestyleImage = 'lifestyle-' | append: lifestyleIndex -%}
+          {%- capture image -%}
+            {{ lifestyleImage | placeholder_svg_tag: 'placeholder-svg' }}
+          {%- endcapture -%}
+
+        {%-
+          render 'home-slideshow-slide',
+          block: block,
+          slideIndex: slideIndex,
+          onboard_image: image,
+        -%}
+      {%- endcase -%}
+    {%- endfor -%}
+  {%- else -%}
+    {%- for i in (1..slide_count) -%}
+      {%- assign slideIndex = slideIndex | plus: 1 -%}
+      {%- assign lifestyleIndex = slideIndex | modulo: 2 | plus: 1 -%}
+      {%- assign lifestyleImage = 'lifestyle-' | append: lifestyleIndex -%}
+      {%- capture image -%}
+          <canvas class="home-slideshow-svg-ie11-fix" width="50" height="19"></canvas>
+          {{ lifestyleImage | placeholder_svg_tag: 'placeholder-svg' }}
+      {%- endcapture -%}
+      {%- capture contentAlignment -%}
+        {%- cycle 'left', 'center', 'right' -%}
+      {%- endcapture -%}
+
+      {%-
+        render 'home-slideshow-slide',
+        slideIndex: slideIndex,
+        contentAlignment: contentAlignment,
+        onboard_image: image,
+      -%}
+    {%- endfor -%}
+  {%- endunless -%}
+
+  <style>
+    #shopify-section-{{ section.id }} .home-slideshow .flickity-prev-next-button .arrow {
+      fill: {{ section.settings.controls-color | default: '#000' }};
+    }
+
+    #shopify-section-{{ section.id }} .home-slideshow .flickity-page-dots .dot {
+      background: {{ section.settings.controls-color | default: '#000' }};
+    }
+
+    /* DW front-page sprint 2026-06-23: hero legibility scrim + mobile text overlay (vp-dw-commerce) */
+    #shopify-section-{{ section.id }} .home-slideshow-slide { position: relative; }
+    #shopify-section-{{ section.id }} .home-slideshow-slide-image { position: relative; }
+    #shopify-section-{{ section.id }} .home-slideshow-slide-image::after {
+      content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1;
+      background: linear-gradient(to top, rgba(0,0,0,.42) 0%, rgba(0,0,0,.16) 32%, rgba(0,0,0,0) 62%);
+    }
+    #shopify-section-{{ section.id }} .home-slideshow-slide-content { z-index: 2; }
+
+    /* DW restrained secondary CTA — luxury "Book a consultation" hairline pill (DTD verdict C, 2026-06-29).
+       Deliberately quieter than the primary .home-slideshow-slide-cta: lighter weight, smaller, wider
+       tracking, hairline outline, ample whitespace — a premium secondary, never a competing banner. */
+    /* Wrapper forces the secondary onto its own row beneath the primary button and inherits the
+       slide's text-align (left/center/right) so the pill stays aligned with the heading. The primary
+       .home-slideshow-slide-cta is an inline text-button, so without this the secondary would sit
+       beside it as a visual peer — the opposite of the restrained, subordinate treatment we want. */
+    #shopify-section-{{ section.id }} .home-slideshow-slide-cta2-row {
+      display: block;
+      margin-top: 18px;
+    }
+    #shopify-section-{{ section.id }} .home-slideshow-slide-cta2 {
+      display: inline-block;
+      padding: 9px 26px;
+      font-family: Lora, serif;
+      font-style: normal;
+      font-weight: 400;
+      font-size: 0.7857142857rem;       /* ~11px — well below the ~16px primary */
+      line-height: 1;
+      letter-spacing: 0.18em;
+      text-transform: uppercase;
+      text-decoration: none;
+      border: 1px solid currentColor;    /* hairline pill, inherits overlay text color */
+      border-radius: 999px;
+      /* Faint dark backing + soft shadow so the small pill text always clears contrast over a
+         variable hero photo (APCA), without becoming a loud solid block like the primary button. */
+      background: rgba(0,0,0,0.46);   /* APCA Lc>=75 worst-case over a light photo region; still translucent vs the opaque primary */
+      -webkit-backdrop-filter: blur(6px);
+              backdrop-filter: blur(6px);
+      text-shadow: 0 1px 6px rgba(0,0,0,0.45);
+      opacity: 0.95;
+      transition: opacity 0.25s ease, background-color 0.25s ease;
+    }
+    #shopify-section-{{ section.id }} .home-slideshow-slide-cta2:hover,
+    #shopify-section-{{ section.id }} .home-slideshow-slide-cta2:focus {
+      opacity: 1;
+      background: rgba(0,0,0,0.55);
+    }
+    @media (min-width: 770px) {
+      #shopify-section-{{ section.id }} .home-slideshow-slide-cta2-row { margin-top: 24px; }
+    }
+
+    @media (max-width: 768px) {
+      #shopify-section-{{ section.id }} .home-slideshow-slide { display: block; }
+      #shopify-section-{{ section.id }} .home-slideshow-slide-content {
+        position: absolute; inset: 0; display: flex; flex-direction: column;
+        align-items: center; justify-content: flex-end; text-align: center; padding: 0 18px 34px;
+      }
+      #shopify-section-{{ section.id }} .home-slideshow-slide-content-inner { width: 100%; max-width: 92%; }
+      #shopify-section-{{ section.id }} .home-slideshow-slide-heading { color: #fff !important; text-shadow: 0 1px 14px rgba(0,0,0,.5); }
+      #shopify-section-{{ section.id }} .home-slideshow-slide-subheading { color: #fff !important; text-shadow: 0 1px 10px rgba(0,0,0,.45); }
+      /* On mobile the photo sits directly behind the text — force the hairline pill light + give it a faint backing for legibility. */
+      #shopify-section-{{ section.id }} .home-slideshow-slide-cta2 { color: #fff !important; border-color: rgba(255,255,255,.7) !important; }
+      #shopify-section-{{ section.id }} .home-slideshow-slide-image::after {
+        background: linear-gradient(to top, rgba(0,0,0,.58) 0%, rgba(0,0,0,.24) 42%, rgba(0,0,0,0) 78%);
+      }
+    }
+  </style>
+</section>
+
+{% schema %}
+{
+  "name": "Slideshow",
+  "class": "shopify-section--slideshow",
+  "max_blocks": 6,
+  "settings": [
+    {
+      "type": "select",
+      "id": "layout",
+      "label": "Layout",
+      "options": [
+        {
+          "value": "content-width",
+          "label": "Content width"
+        },
+        {
+          "value": "full-width",
+          "label": "Full width"
+        }
+      ],
+      "default": "full-width"
+    },
+    {
+      "type": "color",
+      "id": "controls-color",
+      "label": "Controls",
+      "default": "#000"
+    },
+    {
+      "type": "checkbox",
+      "id": "autoplay",
+      "label": "Auto-rotate slides",
+      "default": false
+    },
+    {
+      "type": "range",
+      "id": "autoplay_delay",
+      "label": "Rotation interval",
+      "min": 2,
+      "max": 10,
+      "step": 1,
+      "unit": "sec",
+      "default": 3
+    },
+    {
+      "type": "checkbox",
+      "id": "autoplay-hover-pause",
+      "label": "Pause auto-rotate on hover",
+      "default": false
+    }
+  ],
+  "blocks": [
+    {
+      "type": "slide",
+      "name": "Slide",
+      "settings": [
+        {
+          "type": "image_picker",
+          "id": "image",
+          "label": "Image",
+          "info": "2880 x 1280px recommended"
+        },
+        {
+          "type": "text_alignment",
+          "id": "content-alignment",
+          "label": "Text alignment",
+          "default": "center"
+        },
+        {
+          "type": "color",
+          "id": "overlay-color",
+          "label": "Overlay text",
+          "default": "#262626"
+        },
+        {
+          "type": "text",
+          "id": "title",
+          "label": "Heading",
+          "default": "Slide"
+        },
+        {
+          "type": "text",
+          "id": "subheading",
+          "label": "Subheading",
+          "default": "Introduce customers to your shop with lifestyle imagery and product photography"
+        },
+        {
+          "type": "text",
+          "id": "cta",
+          "label": "Button",
+          "default": "Button"
+        },
+        {
+          "type": "url",
+          "id": "url",
+          "label": "Link"
+        },
+        {
+          "type": "header",
+          "content": "Secondary link (optional)"
+        },
+        {
+          "type": "text",
+          "id": "cta2",
+          "label": "Secondary button",
+          "info": "Restrained hairline link shown beneath the main button (e.g. Book an Appointment)."
+        },
+        {
+          "type": "url",
+          "id": "url2",
+          "label": "Secondary link"
+        }
+      ]
+    }
+  ],
+  "presets": [
+    {
+      "category": "Image",
+      "name": "Slideshow",
+      "blocks": [
+        {
+          "type": "slide"
+        },
+        {
+          "type": "slide"
+        },
+        {
+          "type": "slide"
+        }
+      ]
+    }
+  ],
+  "disabled_on": {
+    "groups": [
+      "*"
+    ]
+  }
+}
+
+{% endschema %}
\ No newline at end of file
diff --git a/shopify/hero-appt-cta-dev/make-dev-copy.py b/shopify/hero-appt-cta-dev/make-dev-copy.py
new file mode 100644
index 00000000..86c6dd1f
--- /dev/null
+++ b/shopify/hero-appt-cta-dev/make-dev-copy.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+# Make an UNPUBLISHED dev copy of theme 144070803507 by:
+#   1) creating an empty unpublished theme,
+#   2) copying every asset from the source theme into it.
+# NEVER publishes. Prints the new theme id.
+import os, json, time, urllib.request, urllib.error, urllib.parse, sys
+STORE="designer-laboratory-sandbox.myshopify.com"; API="2024-10"
+SRC=144070803507
+NAME="5.9.1 [Dev] hero-appt-CTA"
+SECRETS=os.path.expanduser("~/Projects/secrets-manager/.env")
+TOK=None
+for line in open(SECRETS, encoding="utf-8", errors="replace"):
+    if line.startswith("SHOPIFY_THEME_TOKEN="):
+        TOK=line.split("=",1)[1].strip().strip('"\''); break
+assert TOK, "no theme token"
+H={"X-Shopify-Access-Token":TOK,"Content-Type":"application/json"}
+
+def req(method,url,body=None):
+    data=json.dumps(body).encode() if body is not None else None
+    r=urllib.request.Request(url,data=data,headers=H,method=method)
+    for attempt in range(6):
+        try:
+            return json.load(urllib.request.urlopen(r))
+        except urllib.error.HTTPError as e:
+            if e.code==429:
+                time.sleep(2*(attempt+1)); continue
+            print("HTTP",e.code,method,url[:90],e.read().decode()[:300]); raise
+    raise SystemExit("retries exhausted")
+
+# 0) guard: don't recreate if a theme with this name already exists
+themes=req("GET",f"https://{STORE}/admin/api/{API}/themes.json")["themes"]
+existing=[t for t in themes if t["name"]==NAME and t["role"]!="main"]
+if existing:
+    print("ALREADY EXISTS:", existing[0]["id"]); sys.exit(0)
+
+# 1) create empty unpublished theme
+new=req("POST",f"https://{STORE}/admin/api/{API}/themes.json",
+        {"theme":{"name":NAME,"role":"unpublished"}})["theme"]
+nid=new["id"]; print("created theme", nid, new["name"], "role", new["role"])
+
+# 2) list source assets
+assets=req("GET",f"https://{STORE}/admin/api/{API}/themes/{SRC}/assets.json")["assets"]
+keys=[a["key"] for a in assets]
+print("source assets:", len(keys))
+
+copied=err=0
+for k in keys:
+    q=urllib.parse.urlencode({"asset[key]":k})
+    a=req("GET",f"https://{STORE}/admin/api/{API}/themes/{SRC}/assets.json?{q}")["asset"]
+    payload={"key":k}
+    if a.get("value") is not None:
+        payload["value"]=a["value"]
+    elif a.get("attachment") is not None:
+        payload["attachment"]=a["attachment"]
+    elif a.get("public_url"):
+        payload["src"]=a["public_url"]
+    else:
+        # config/* sometimes returns value; skip if truly empty
+        print("  skip (no body):",k); continue
+    try:
+        req("PUT",f"https://{STORE}/admin/api/{API}/themes/{nid}/assets.json",{"asset":payload})
+        copied+=1
+    except Exception as ex:
+        err+=1; print("  PUT FAIL",k, str(ex)[:120])
+    time.sleep(0.18)  # stay under Standard 2/s
+print(f"copied {copied}/{len(keys)} assets, {err} errors")
+print("NEW_THEME_ID", nid)
diff --git a/shopify/hero-appt-cta-dev/push-edits-v2.py b/shopify/hero-appt-cta-dev/push-edits-v2.py
new file mode 100644
index 00000000..38f36e88
--- /dev/null
+++ b/shopify/hero-appt-cta-dev/push-edits-v2.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+import os, json, time, urllib.request, urllib.error, urllib.parse, datetime, sys
+STORE="designer-laboratory-sandbox.myshopify.com"; API="2024-10"; DST=144076931123
+HERE="/tmp/dw-hero-cta"; BK=os.path.join(HERE,"backups"); os.makedirs(BK,exist_ok=True)
+SECRETS=os.path.expanduser("~/Projects/secrets-manager/.env")
+TOK=next(l.split("=",1)[1].strip().strip('"\'') for l in open(SECRETS,encoding="utf-8",errors="replace") if l.startswith("SHOPIFY_THEME_TOKEN="))
+H={"X-Shopify-Access-Token":TOK,"Content-Type":"application/json"}
+def req(m,u,b=None):
+    d=json.dumps(b).encode() if b is not None else None
+    for a in range(6):
+        try: return json.load(urllib.request.urlopen(urllib.request.Request(u,data=d,headers=H,method=m)))
+        except urllib.error.HTTPError as e:
+            if e.code==429: time.sleep(2*(a+1)); continue
+            print("HTTP",e.code,e.read().decode()[:300]); raise
+t=next(x for x in req("GET",f"https://{STORE}/admin/api/{API}/themes.json")["themes"] if x["id"]==DST)
+assert t["role"]!="main","REFUSE main"
+print("target",DST,t["name"],t["role"])
+stamp=datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
+def rd(k):
+    q=urllib.parse.urlencode({"asset[key]":k}); return req("GET",f"https://{STORE}/admin/api/{API}/themes/{DST}/assets.json?{q}")["asset"]["value"]
+def push(k,lp,markers):
+    cur=rd(k); open(os.path.join(BK,k.replace("/","_")+f".dev{DST}.{stamp}.bak"),"w").write(cur)
+    new=open(lp,encoding="utf-8").read()
+    for mk in markers:
+        if mk not in new: sys.exit(f"marker {mk!r} missing in local {k}")
+    req("PUT",f"https://{STORE}/admin/api/{API}/themes/{DST}/assets.json",{"asset":{"key":k,"value":new}})
+    time.sleep(2.0)  # avoid stale read-after-write
+    back=rd(k); ok=all(mk in back for mk in markers) and len(back)==len(new)
+    print(f"{k}: size r={len(back)} l={len(new)} markers={'ok' if all(mk in back for mk in markers) else 'MISSING'} -> {'OK' if ok else 'CHECK'}")
+push("snippets/home-slideshow-slide.liquid", os.path.join(HERE,"home-slideshow-slide.liquid"),
+     ["home-slideshow-slide-cta2-row","block.settings.cta2"])
+push("sections/home-slideshow.liquid", os.path.join(HERE,"home-slideshow.liquid"),
+     ["home-slideshow-slide-cta2-row","\"id\": \"cta2\"","border: 1px solid currentColor"])
+print("done")
diff --git a/shopify/hero-appt-cta-dev/push-edits.py b/shopify/hero-appt-cta-dev/push-edits.py
new file mode 100644
index 00000000..2ffc1980
--- /dev/null
+++ b/shopify/hero-appt-cta-dev/push-edits.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+# Push the 3 hero-CTA edits to the DEV theme 144076931123 ONLY. Never main.
+# 1) snippets/home-slideshow-slide.liquid  (renders secondary CTA)
+# 2) sections/home-slideshow.liquid        (schema cta2/url2 + restrained CSS)
+# 3) templates/index.json                  (cta2/url2 on first hero slide)
+# Backs up the dev theme's current copy of each asset, PUTs, GETs back, verifies markers.
+import os, json, time, urllib.request, urllib.error, urllib.parse, datetime, sys
+STORE="designer-laboratory-sandbox.myshopify.com"; API="2024-10"
+DST=144076931123
+HERE="/tmp/dw-hero-cta"
+BK=os.path.join(HERE,"backups"); os.makedirs(BK,exist_ok=True)
+SECRETS=os.path.expanduser("~/Projects/secrets-manager/.env")
+TOK=next(l.split("=",1)[1].strip().strip('"\'') for l in open(SECRETS,encoding="utf-8",errors="replace") if l.startswith("SHOPIFY_THEME_TOKEN="))
+H={"X-Shopify-Access-Token":TOK,"Content-Type":"application/json"}
+
+def req(m,u,b=None):
+    d=json.dumps(b).encode() if b is not None else None
+    for a in range(6):
+        try: return json.load(urllib.request.urlopen(urllib.request.Request(u,data=d,headers=H,method=m)))
+        except urllib.error.HTTPError as e:
+            if e.code==429: time.sleep(2*(a+1)); continue
+            print("HTTP",e.code,m,u[:80],e.read().decode()[:300]); raise
+
+# hard guard: confirm target is NOT main
+themes=req("GET",f"https://{STORE}/admin/api/{API}/themes.json")["themes"]
+t=next((x for x in themes if x["id"]==DST),None)
+assert t, "dev theme not found"
+assert t["role"]!="main", "REFUSING: target is the live/main theme"
+print(f'target {DST} "{t["name"]}" role={t["role"]} — live untouched')
+
+stamp=datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
+
+def get_remote(key):
+    q=urllib.parse.urlencode({"asset[key]":key})
+    return req("GET",f"https://{STORE}/admin/api/{API}/themes/{DST}/assets.json?{q}")["asset"]["value"]
+
+def backup(key,val):
+    fn=os.path.join(BK,key.replace("/","_")+f".dev{DST}.{stamp}.bak")
+    open(fn,"w").write(val); print("  backup:",fn,f"({len(val)} bytes)")
+
+def push_file(key, localpath, markers):
+    print(f"\n== {key} ==")
+    cur=get_remote(key); backup(key,cur)
+    new=open(localpath,encoding="utf-8").read()
+    for m in markers:
+        if m not in new: sys.exit(f"  ABORT: marker {m!r} missing in local file")
+    req("PUT",f"https://{STORE}/admin/api/{API}/themes/{DST}/assets.json",{"asset":{"key":key,"value":new}})
+    time.sleep(0.4)
+    back=get_remote(key)
+    ok=all(m in back for m in markers) and len(back)==len(new)
+    print(f"  PUT ok — verify markers={'all present' if all(m in back for m in markers) else 'MISSING'} size r={len(back)} l={len(new)} -> {'OK' if ok else 'MISMATCH'}")
+    return ok
+
+ok1=push_file("snippets/home-slideshow-slide.liquid",
+              os.path.join(HERE,"home-slideshow-slide.liquid"),
+              ["home-slideshow-slide-cta2","block.settings.cta2"])
+ok2=push_file("sections/home-slideshow.liquid",
+              os.path.join(HERE,"home-slideshow.liquid"),
+              ["home-slideshow-slide-cta2","\"id\": \"cta2\"","border: 1px solid currentColor"])
+
+# index.json: patch first hero slide programmatically from the freshly-fetched remote copy
+print("\n== templates/index.json ==")
+cur=get_remote("templates/index.json"); backup("templates/index.json",cur)
+d=json.loads(cur)
+sec=d["sections"]["home_slideshow_EXmkyD"]
+blk=sec["blocks"]["slide_8gwUz8"]
+blk["settings"]["cta2"]="Book an Appointment"
+blk["settings"]["url2"]="/pages/book-appointment"
+new=json.dumps(d,indent=2,ensure_ascii=False)
+req("PUT",f"https://{STORE}/admin/api/{API}/themes/{DST}/assets.json",{"asset":{"key":"templates/index.json","value":new}})
+time.sleep(0.4)
+back=get_remote("templates/index.json")
+bd=json.loads(back)
+bs=bd["sections"]["home_slideshow_EXmkyD"]["blocks"]["slide_8gwUz8"]["settings"]
+ok3=(bs.get("cta2")=="Book an Appointment" and bs.get("url2")=="/pages/book-appointment")
+print(f"  verify: cta2={bs.get('cta2')!r} url2={bs.get('url2')!r} -> {'OK' if ok3 else 'FAIL'}")
+
+print("\nALL OK" if (ok1 and ok2 and ok3) else "\nONE OR MORE FAILED")

← 9773acef auto-save: 2026-06-29T16:54:45 (8 files) — pending-approval/  ·  back to Designer Wallcoverings  ·  auto-save: 2026-06-29T17:55:02 (5 files) — pending-approval/ 646de077 →