#!/usr/bin/env python3 """ Phase 2 apply — trim 9 long meta_descriptions, keep og/twitter mirrored. Reads new descriptions from phase2_desc.json (slug -> new meta_description). For each post sets meta_description = og_description = twitter_description to the SAME new value via a minimal ghst --from-json patch (+ updated_at for Ghost's collision check). Touches nothing else. READ-ONLY against meta_title/body/etc. """ import json, os, subprocess, sys, tempfile from seo_audit import fetch_posts NEW = json.load(open(os.path.join(os.path.dirname(__file__), "phase2_desc.json"))) def apply_one(post, val): body = {"posts": [{ "meta_description": val, "og_description": val, "twitter_description": val, "updated_at": post["updated_at"], }]} fd, jp = tempfile.mkstemp(suffix=".json"); os.close(fd) fd, op = tempfile.mkstemp(suffix=".json"); os.close(fd) try: json.dump(body, open(jp, "w")) cmd = f"ghst-en post update {post['id']} --from-json {jp} --json > {op}" r = subprocess.run(cmd, shell=True, stderr=subprocess.PIPE, text=True) if r.returncode != 0: return r.stderr.strip() return None finally: os.unlink(jp); os.unlink(op) def main(): posts = {p["slug"]: p for p in fetch_posts()} ok, fail = [], [] for slug, val in NEW.items(): p = posts.get(slug) if not p: fail.append((slug, "slug not found in live posts")); print(f" FAIL {slug}: not found"); continue err = apply_one(p, val) if err: fail.append((slug, err)); print(f" FAIL {slug}: {err[:90]}") else: ok.append(slug); print(f" OK {slug} (meta/og/twitter desc = {len(val)} chars)") print(f"\nApplied: {len(ok)} | Failed: {len(fail)}") for s, e in fail: print(f" FAILED {s}: {e}") if __name__ == "__main__": main()