diff --git a/seo_finish.py b/seo_finish.py index 272ab13..a274570 100755 --- a/seo_finish.py +++ b/seo_finish.py @@ -47,16 +47,37 @@ import seo_exceptions as X ALT_MAX = 191 # límite duro de Ghost para feature_image_alt WORK = os.path.expanduser("~/.cache/seo-finish") +# Los dos sitios. Ojo al canónico: EN es www, ES es el APEX — al revés. +SITES = { + "en": {"cli": "ghst-en", "host": "theexclusionzone.com", + "url": "https://www.theexclusionzone.com/"}, + "es": {"cli": "ghst-es", "host": "zonadeexclusion.com", + "url": "https://zonadeexclusion.com/"}, +} + + +def use_site(site): + """Selecciona el sitio y adapta el motor de reglas. + + seo_rules.SITE_HOST está fijado a EN y NO se puede parametrizar ahí: ese + fichero se vendoriza byte a byte dentro de ResearchOwl y la CI lo verifica. + Se reasigna aquí en tiempo de ejecución (el módulo lo lee en cada llamada), + que deja intacto el contrato compartido. + """ + cfg = SITES[site] + R.SITE_HOST = cfg["host"] + return cfg + def sh(cmd, timeout=240): return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) -def fetch(slug): +def fetch(slug, cli): fd, path = tempfile.mkstemp(suffix=".json", prefix="seo_finish_") os.close(fd) try: - sh(f"ghst-en post list --limit 100 --formats html --json > {path}") + sh(f"{cli} post list --limit 100 --formats html --json > {path}") with open(path) as f: data = json.load(f) posts = data.get("posts", data) if isinstance(data, dict) else data @@ -65,7 +86,7 @@ def fetch(slug): for p in posts: if p.get("slug") == slug: return p - sys.exit(f"ERROR: no encuentro el post '{slug}'") + sys.exit(f"ERROR: no encuentro el post '{slug}' en {cli}") def violations_of(post): @@ -99,11 +120,12 @@ def grab_image(url, slug): def cmd_prep(a): - p = fetch(a.slug) + cfg = use_site(a.site) + p = fetch(a.slug, cfg["cli"]) text = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", p.get("html") or "")).strip() - img = grab_image(p.get("feature_image"), a.slug) + img = grab_image(p.get("feature_image"), f"{a.site}-{a.slug[:40]}") - print(f"═══ {a.slug}") + print(f"═══ [{a.site.upper()}] {a.slug}") print(f" estado: {p.get('status')} | publica: {str(p.get('published_at'))[:10]} | {len(text.split())} palabras") print(f" id: {p['id']}") print("\n─── SEO actual ───") @@ -129,7 +151,7 @@ def cmd_prep(a): print(f" url: {p.get('feature_image')}") print(f" local (mírala para escribir el alt): {img or 'NO DISPONIBLE'}") - body = os.path.join(WORK, f"{a.slug}.txt") + body = os.path.join(WORK, f"{a.site}-{a.slug[:40]}.txt") os.makedirs(WORK, exist_ok=True) with open(body, "w") as f: f.write(text) @@ -137,10 +159,11 @@ def cmd_prep(a): print(f" volcado en: {body}") print(f" primeras líneas: {text[:400]}…") print(f"\nSiguiente paso: escribe el patch JSON y aplícalo con") - print(f" seo_finish.py apply {a.slug} --from-json ") + print(f" seo_finish.py apply {a.slug} --site {a.site} --from-json ") def cmd_apply(a): + cfg = use_site(a.site) with open(a.from_json) as f: patch = json.load(f) @@ -157,25 +180,25 @@ def cmd_apply(a): print(f"✗ {e}") sys.exit("Abortado: corrige los límites antes de aplicar.") - p = fetch(a.slug) + p = fetch(a.slug, cfg["cli"]) # Backup de los campos que vamos a tocar, con el mismo patrón que link-batch-backup. import datetime bdir = os.path.expanduser("~/link-batch-backup") os.makedirs(bdir, exist_ok=True) ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") - bpath = os.path.join(bdir, f"{a.slug}-seofinish-pristine-{ts}.json") + bpath = os.path.join(bdir, f"{a.site}-{a.slug[:40]}-seofinish-pristine-{ts}.json") with open(bpath, "w") as f: json.dump({"id": p["id"], "slug": a.slug, **{k: p.get(k) for k in patch}}, f, ensure_ascii=False, indent=1) print(f"backup: {os.path.basename(bpath)}") - r = sh(f'ghst-en post update {p["id"]} --from-json "{a.from_json}"') + r = sh(f'{cfg["cli"]} post update {p["id"]} --from-json "{a.from_json}"') if "Slug:" not in r.stdout: print(r.stdout, r.stderr) - sys.exit("✗ ghst-en no confirmó la actualización") + sys.exit(f"✗ {cfg['cli']} no confirmó la actualización") print("✓ aplicado") - after = fetch(a.slug) + after = fetch(a.slug, cfg["cli"]) viol = violations_of(after) if viol: print(f"\n⚠ el post SIGUE con {len(viol)} violación(es):") @@ -191,9 +214,11 @@ def main(): sub = ap.add_subparsers(dest="cmd", required=True) p1 = sub.add_parser("prep", help="paquete de revisión (no modifica nada)") p1.add_argument("slug") + p1.add_argument("--site", choices=("en", "es"), default="en") p1.set_defaults(func=cmd_prep) p2 = sub.add_parser("apply", help="aplica un patch y re-valida") p2.add_argument("slug") + p2.add_argument("--site", choices=("en", "es"), default="en") p2.add_argument("--from-json", required=True) p2.set_defaults(func=cmd_apply) a = ap.parse_args()