← back to Secrets Manager

deploy-collection-aeo.py

93 lines

#!/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")