seo_finish: soporte para el sitio ES (--site en|es)
El blog ES no tenía forma de llegar desde las herramientas: solo existía el wrapper ghst-en. Creado ghst-es (gemelo, namespace zona-exclusion, token efímero del pod, nada en disco) y parametrizado seo_finish. Ojo al canónico, que va al revés en cada sitio: EN es www, ES es el apex. seo_rules.SITE_HOST está fijado a EN y NO se parametriza ahí a propósito: ese fichero se vendoriza byte a byte dentro de ResearchOwl y la CI lo verifica. Se reasigna en tiempo de ejecución desde use_site(), que deja intacto el contrato compartido. Los ficheros de trabajo (imagen convertida, volcado de texto) y los backups llevan ahora prefijo de sitio, para que un mismo slug en ambos idiomas no se pise. Verificado contra los dos sitios: ES detecta las 8 violaciones del borrador nuevo (todas por dryrun, que ya no escribe SEO) y EN sigue dando 0 en el artículo belga publicado hoy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8c83fb81fc
commit
34263469e4
+38
-13
@@ -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 <patch.json>")
|
||||
print(f" seo_finish.py apply {a.slug} --site {a.site} --from-json <patch.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()
|
||||
|
||||
Reference in New Issue
Block a user