← 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
A shopify/hero-appt-cta-dev/README.mdA shopify/hero-appt-cta-dev/home-slideshow-slide.liquidA shopify/hero-appt-cta-dev/home-slideshow.liquidA shopify/hero-appt-cta-dev/make-dev-copy.pyA shopify/hero-appt-cta-dev/push-edits-v2.pyA shopify/hero-appt-cta-dev/push-edits.py
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 →