[object Object]

← back to Secrets Manager

auto-save: 2026-07-21T16:38:33 (3 files) — deploy-collection-aeo.py find-theme-token.sh greenland-cleanup-backups/dw-collection-hero-BACKUP-20260721-manual.liquid

fc3b6fb2767bbb068e166c1d20b89df9d04f79fa · 2026-07-21 16:38:34 -0700 · Steve Abrams

Files touched

Diff

commit fc3b6fb2767bbb068e166c1d20b89df9d04f79fa
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 21 16:38:34 2026 -0700

    auto-save: 2026-07-21T16:38:33 (3 files) — deploy-collection-aeo.py find-theme-token.sh greenland-cleanup-backups/dw-collection-hero-BACKUP-20260721-manual.liquid
---
 deploy-collection-aeo.py                           | 92 ++++++++++++++++++++++
 find-theme-token.sh                                | 32 ++++++++
 ...w-collection-hero-BACKUP-20260721-manual.liquid | 90 +++++++++++++++++++++
 3 files changed, 214 insertions(+)

diff --git a/deploy-collection-aeo.py b/deploy-collection-aeo.py
new file mode 100644
index 0000000..5c2c7bf
--- /dev/null
+++ b/deploy-collection-aeo.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+"""
+deploy-collection-aeo.py — insert the AEO block into the LIVE DW theme's collection
+hero, lifting all 670 collection pages at once. Idempotent + backs up first.
+
+Inserts, into sections/dw-collection-hero.liquid (live theme 144396058675):
+  1. a direct-answer <p> right after the <h1> (the 16-pt AEO signal),
+  2. CollectionPage + BreadcrumbList JSON-LD (+ conditional FAQPage from a
+     custom.faq metafield) right before the section's <style>.
+The description already renders (lines 31-33 + sections/collection.liquid) so it's
+NOT duplicated. Shopify validates Liquid on PUT — a 200 means valid syntax.
+Backup written before any change; restore = re-PUT the backup file.
+Run:  ! python3 ~/Projects/secrets-manager/deploy-collection-aeo.py            # dry-run (default)
+      ! python3 ~/Projects/secrets-manager/deploy-collection-aeo.py --apply    # write live
+"""
+import re, json, sys, urllib.request, datetime
+
+APPLY = '--apply' in sys.argv
+env = open('/Users/macstudio3/Projects/secrets-manager/.env').read()
+tok = re.search(r'^SHOPIFY_THEME_TOKEN=(.+)$', env, re.M).group(1).strip().strip('"').strip("'")
+S = "designer-laboratory-sandbox.myshopify.com"; A = "2024-10"; TID = 144396058675
+KEY = "sections/dw-collection-hero.liquid"
+
+def get_asset():
+    r = urllib.request.Request(f"https://{S}/admin/api/{A}/themes/{TID}/assets.json?asset[key]={KEY}",
+                               headers={"X-Shopify-Access-Token": tok})
+    return json.load(urllib.request.urlopen(r, timeout=30))['asset']['value']
+
+def put_asset(value):
+    body = json.dumps({"asset": {"key": KEY, "value": value}}).encode()
+    r = urllib.request.Request(f"https://{S}/admin/api/{A}/themes/{TID}/assets.json", data=body,
+        method="PUT", headers={"X-Shopify-Access-Token": tok, "Content-Type": "application/json"})
+    return json.load(urllib.request.urlopen(r, timeout=30))
+
+ANSWER = (
+    '      {%- assign aeo_cat = collection.metafields.custom.category | default: \'wallcovering\' -%}\n'
+    '      <p class="dw-collection-hero__answer">Browse {{ collection.products_count }} luxury '
+    '{{ aeo_cat }}{% if collection.products_count != 1 %}s{% endif %} in the {{ collection.title }} '
+    'collection at Designer Wallcoverings.</p>\n'
+)
+SCHEMA = '''{%- assign aeo_faq = collection.metafields.custom.faq.value -%}
+<script type="application/ld+json">
+{"@context":"https://schema.org","@type":"CollectionPage","name":{{ collection.title | json }},"description":{{ collection.description | strip_html | truncatewords: 40 | json }},"url":{{ request.origin | append: collection.url | json }},"isPartOf":{"@type":"WebSite","name":"Designer Wallcoverings","url":{{ request.origin | json }}}}
+</script>
+<script type="application/ld+json">
+{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":{{ request.origin | json }}},{"@type":"ListItem","position":2,"name":"Collections","item":{{ request.origin | append: '/collections' | json }}},{"@type":"ListItem","position":3,"name":{{ collection.title | json }},"item":{{ request.origin | append: collection.url | json }}}]}
+</script>
+{%- if aeo_faq != blank and aeo_faq != empty -%}
+<div class="dw-collection-faq"><h2>Frequently Asked Questions</h2>
+{%- for item in aeo_faq -%}<h3>{{ item.q }}</h3><p>{{ item.a }}</p>{%- endfor -%}</div>
+<script type="application/ld+json">
+{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{%- for item in aeo_faq -%}{"@type":"Question","name":{{ item.q | json }},"acceptedAnswer":{"@type":"Answer","text":{{ item.a | strip_html | json }}}}{% unless forloop.last %},{% endunless %}{%- endfor -%}]}
+</script>
+{%- endif -%}
+'''
+
+v = get_asset()
+if 'dw-collection-hero__answer' in v:
+    print("AEO block already present — no change (idempotent)."); sys.exit(0)
+
+ts = "20260721-manual"  # stamp is fixed (no clock in this env); rename backup after if needed
+backup = f"/Users/macstudio3/Projects/secrets-manager/greenland-cleanup-backups/dw-collection-hero-BACKUP-{ts}.liquid"
+open(backup, "w").write(v)
+print(f"backup written: {backup}")
+
+lines = v.splitlines(keepends=True)
+out, done_answer, done_schema = [], False, False
+for ln in lines:
+    if (not done_answer) and 'dw-collection-hero__title' in ln and '<h1' in ln:
+        out.append(ln); out.append(ANSWER); done_answer = True; continue
+    if (not done_schema) and ln.lstrip().startswith('<style'):
+        out.append(SCHEMA); out.append(ln); done_schema = True; continue
+    out.append(ln)
+new = ''.join(out)
+
+if not (done_answer and done_schema):
+    print(f"ABORT: could not locate insertion points (answer={done_answer} schema={done_schema}) — no write made."); sys.exit(1)
+print(f"insertions located: answer-line ✓  schema ✓   (+{len(new)-len(v)} chars)")
+
+if not APPLY:
+    print("\nDRY-RUN — re-run with --apply to write the live theme. Preview of inserted answer line:")
+    print("  " + ANSWER.strip().splitlines()[-1][:110])
+    sys.exit(0)
+
+res = put_asset(new)
+ok = 'asset' in res
+print("PUT:", "OK (Shopify accepted the Liquid = valid syntax)" if ok else json.dumps(res)[:300])
+# confirm the write landed
+back = get_asset()
+print("verify: block present in live asset =", 'dw-collection-hero__answer' in back)
+print(f"\nDeployed to live theme {TID}. Rollback if needed: re-PUT {backup}")
+print("Check a collection page (allow for storefront cache): https://designerwallcoverings.com/collections/grasscloth-wallpapers")
diff --git a/find-theme-token.sh b/find-theme-token.sh
new file mode 100644
index 0000000..9836eed
--- /dev/null
+++ b/find-theme-token.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+# find-theme-token.sh — identify which already-stored DW Shopify token carries
+# read_themes/write_themes scope, by probing the themes.json endpoint (needs read_themes).
+# READ-ONLY: prints only the var NAME + last-4 + HTTP result. Never prints a full token.
+# Companion to find-content-token.sh. For the collection-AEO theme-template deploy
+# (DTD verdict A 2026-07-21). The "Theme Editor" app (dashboard id 312098) is the
+# expected holder — if its token is stored under any of these names it shows CONTENT OK.
+set -euo pipefail
+cd "$(dirname "$0")"
+STORE="designer-laboratory-sandbox.myshopify.com"
+API="2024-10"
+CANDIDATES=(SHOPIFY_THEME_TOKEN SHOPIFY_THEME_EDITOR_TOKEN SHOPIFY_ADMIN_TOKEN \
+            SHOPIFY_CONTENT_TOKEN SHOPIFY_DRAFT_TOKEN SHOPIFY_ORDERS_TOKEN)
+WINNER=""
+for n in "${CANDIDATES[@]}"; do
+  line=$(grep -m1 "^${n}=" .env 2>/dev/null || true)
+  [ -z "$line" ] && { printf '%-30s — not stored\n' "$n"; continue; }
+  tok="${line#*=}"; tok="${tok%\"}"; tok="${tok#\"}"; tok="${tok%\'}"; tok="${tok#\'}"
+  code=$(curl -s -o /dev/null -w '%{http_code}' \
+         "https://$STORE/admin/api/$API/themes.json" \
+         -H "X-Shopify-Access-Token: $tok")
+  if [ "$code" = "200" ]; then
+    printf '%-30s …%s  →  ✅ THEMES OK (has read_themes)\n' "$n" "${tok: -4}"
+    [ -z "$WINNER" ] && WINNER="$n"
+  elif [ "$code" = "403" ]; then
+    printf '%-30s …%s  →  403 (no themes scope)\n' "$n" "${tok: -4}"
+  else
+    printf '%-30s …%s  →  HTTP %s\n' "$n" "${tok: -4}" "$code"
+  fi
+done
+echo "---"
+[ -n "$WINNER" ] && echo "USABLE_THEME_TOKEN=$WINNER" || echo "USABLE_THEME_TOKEN=none — reveal the Theme Editor app (312098) token, or grant read_themes+write_themes"
diff --git a/greenland-cleanup-backups/dw-collection-hero-BACKUP-20260721-manual.liquid b/greenland-cleanup-backups/dw-collection-hero-BACKUP-20260721-manual.liquid
new file mode 100644
index 0000000..355f6b8
--- /dev/null
+++ b/greenland-cleanup-backups/dw-collection-hero-BACKUP-20260721-manual.liquid
@@ -0,0 +1,90 @@
+{%- comment -%}
+  DW Collection Hero — Steve standing rule (2026-06-23): every collection page
+  must have a background "bg" hero image. Renders collection.image as a full-bleed
+  cover background with the title + breadcrumbs + description on a SEMI-OPAQUE
+  CONTRASTING PANEL (dark scrim panel) so the text stays legible over any image.
+  Collections with no image fall back to a branded gradient so the page is never
+  broken; audit-collection-hero-images.py lists which collections still need one.
+
+  Also HIDES the Boost AI Search & Filter app's own collection banner (the
+  duplicate lower banner) + any legacy theme breadcrumbs, so only this hero shows.
+
+  Toggle by removing it from templates/collection.json "order".
+{%- endcomment -%}
+{%- liquid
+  assign dw_has_hero = false
+  if collection.image
+    assign dw_has_hero = true
+  endif
+-%}
+<div class="dw-collection-hero{% unless dw_has_hero %} dw-collection-hero--fallback{% endunless %}"
+     {% if dw_has_hero %}style="background-image:url({{ collection.image | img_url: '2048x' }});"{% endif %}
+     data-dw-collection-hero>
+  <div class="dw-collection-hero__inner page-width">
+    <div class="dw-collection-hero__panel">
+      <nav class="dw-collection-hero__crumbs" aria-label="Breadcrumb">
+        <a href="/">Home</a> <span aria-hidden="true">/</span>
+        <a href="/collections">Collections</a> <span aria-hidden="true">/</span>
+        <span aria-current="page">{{ collection.title }}</span>
+      </nav>
+      <h1 class="dw-collection-hero__title">{{ collection.title }}</h1>
+      {%- if section.settings.show_description and collection.description != blank -%}
+        <div class="dw-collection-hero__desc">{{ collection.description | strip_html | truncatewords: 32 }}</div>
+      {%- endif -%}
+    </div>
+  </div>
+</div>
+<style>
+  .dw-collection-hero{position:relative;min-height:clamp(80px,10.7vw,140px);display:flex;
+    align-items:center;background-size:cover;background-position:center;background-repeat:no-repeat;
+    margin:0 0 1.5rem;overflow:hidden;}
+  .dw-collection-hero--fallback{background:linear-gradient(135deg,#1c1c1c 0%,#3a3a3a 55%,#2a2320 100%);}
+  .dw-collection-hero__inner{position:relative;z-index:1;width:100%;padding:0.9rem 2.25rem;}
+  /* Semi-opaque CONTRASTING panel behind the text — dark scrim over the light
+     pattern so title/breadcrumbs/description read clearly without darkening the
+     whole image. Constrained width, left-aligned. */
+  .dw-collection-hero__panel{display:inline-block;max-width:min(640px,92%);
+    background:rgba(22,18,16,0.66);
+    -webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);
+    border:1px solid rgba(255,255,255,0.14);border-radius:6px;
+    padding:1.4rem 1.75rem;box-shadow:0 6px 30px rgba(0,0,0,.28);}
+  .dw-collection-hero__crumbs{font-size:.78rem;letter-spacing:.04em;text-transform:uppercase;
+    color:#fff;opacity:.92;margin:0 0 .45rem;}
+  .dw-collection-hero__crumbs a,.dw-collection-hero__crumbs span{color:#fff;text-decoration:none;}
+  .dw-collection-hero__crumbs a:hover{text-decoration:underline;}
+  .dw-collection-hero__title{color:#fff;font-size:clamp(1.8rem,3.4vw,3rem);margin:.1rem 0 0;
+    letter-spacing:.5px;line-height:1.05;}
+  .dw-collection-hero__desc{color:#fff;max-width:60ch;margin:.6rem 0 0;opacity:.96;
+    font-size:.95rem;line-height:1.55;}
+
+  /* Hide the duplicate LOWER banner drawn by the Boost AI Search & Filter app
+     (its own collection header/banner) so only this hero shows. */
+  .boost-sd__collection-header,
+  .boost-sd__page-header,
+  .boost-sd__collection-header-image,
+  .boost-sd__collection-header-content,
+  .boost-sd__collection-header-title,
+  .boost-sd__collection-header-description,
+  .boost-sd__collection-title,
+  .boost-sd__collection-description{display:none !important;}
+  /* hero now carries breadcrumbs: hide any legacy theme breadcrumbs on collection pages */
+  .template-collection .breadcrumbs,
+  .template-collection .breadcrumb{display:none;}
+</style>
+
+{% schema %}
+{
+  "name": "DW Collection Hero",
+  "settings": [
+    {
+      "type": "checkbox",
+      "id": "show_description",
+      "label": "Show collection description in hero",
+      "default": true
+    }
+  ],
+  "presets": [
+    { "name": "DW Collection Hero" }
+  ]
+}
+{% endschema %}

← 1afa936 auto-save: 2026-07-21T16:08:25 (1 files) — greenland-cleanup  ·  back to Secrets Manager  ·  auto-save: 2026-07-21T17:08:42 (5 files) — deploy-collection 1b7fdd1 →