← back to Designerwallcoverings

scripts/backfill-clarke-redirects.py

59 lines

#!/usr/bin/env python3
"""
Backfill 301 redirects for the renamed Clarke handles: old hashed handle -> new
dwkk-{number} handle. Reads every old->new pair from rename-clarke-handles-ledger.jsonl
and creates a Shopify URL redirect (/products/<old> -> /products/<new>) so the old
hashed URLs stop 404-ing. Idempotent (skips pairs that already have a redirect).
REQUIRES a token with write_online_store_navigation (the default one lacks it —
this will ACCESS_DENIED until the scope is added). $0 (Shopify Admin GraphQL).

Env: DRYRUN=1 to only report the count.
"""
import os,sys,json,urllib.request,time
SHOP="designer-laboratory-sandbox.myshopify.com"
DRYRUN=os.environ.get("DRYRUN")=="1"
HERE=os.path.dirname(os.path.abspath(__file__))
LEDGER=os.path.join(HERE,"rename-clarke-handles-ledger.jsonl")
TOKEN_VAR=os.environ.get("TOKEN_VAR","SHOPIFY_ADMIN_TOKEN")  # Steve 2026-06-18: use SHOPIFY_DW_ADMIN_TOKEN_CONTENT for redirects
def _token():
    for p in (os.path.join(HERE,"..",".env"), os.path.expanduser("~/Projects/secrets-manager/.env")):
        try:
            for ln in open(p):
                if ln.startswith(TOKEN_VAR+"="): return ln.split("=",1)[1].strip()
        except FileNotFoundError: pass
    sys.exit(f"no {TOKEN_VAR}")
TOKEN=_token(); URL=f"https://{SHOP}/admin/api/2024-10/graphql.json"
def gql(q,v=None):
    req=urllib.request.Request(URL,data=json.dumps({"query":q,"variables":v or {}}).encode(),
        headers={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"})
    return json.load(urllib.request.urlopen(req,timeout=40))

# unique old->new pairs from the ledger (last write wins; only successful renames)
pairs={}
for ln in open(LEDGER):
    try: r=json.loads(ln)
    except: continue
    old,new=r.get("old"),r.get("new")
    if old and new and old!=new and old.startswith("dwkk-g"):
        pairs[old]=new
print(f"old->new redirect pairs: {len(pairs)}")
if DRYRUN:
    for o,n in list(pairs.items())[:5]: print(f"  /products/{o} -> /products/{n}")
    sys.exit(0)

RC='mutation($r:UrlRedirectInput!){ urlRedirectCreate(urlRedirect:$r){ urlRedirect{id} userErrors{field message} } }'
made=skip=err=0
for i,(old,new) in enumerate(pairs.items(),1):
    try:
        r=gql(RC,{"r":{"path":f"/products/{old}","target":f"/products/{new}"}}).get("data",{}).get("urlRedirectCreate") or {}
        ue=r.get("userErrors") or []
        if any("already" in (e.get("message","").lower()) for e in ue): skip+=1
        elif ue: err+=1; (print("  err:",ue[:1]) if err<=3 else None)
        elif r.get("urlRedirect"): made+=1
    except Exception as e:
        err+=1
        if err<=3: print("  exc:",str(e)[:100])
    if i%200==0: print(f"  [{i}/{len(pairs)}] made={made} skip={skip} err={err}",flush=True)
    time.sleep(0.3)
print(f"DONE redirects made={made} already-existed={skip} errors={err} of {len(pairs)}")