From 7bc9b3e2aac6da7cde1941f5c9df98abd1367ee7 Mon Sep 17 00:00:00 2001 From: ChemaVX Date: Tue, 28 Jul 2026 07:51:51 +0000 Subject: [PATCH] validador: aceptar --site, que llevaba desde siempre clavado al EN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seo_validate solo sabía preguntar a ghst-en, así que un slug español daba 404 y el paso 5 del remate —validar hasta que quede limpio— NUNCA pudo completarse en el blog ES, pese a que el checklist afirma que todas las herramientas aceptan --site. Y arrastraba el mismo fallo de SITE_HOST que ya apareció en el auditor: validando el Varginha del ES con el host del EN cuenta 0 enlaces internos donde hay 1, y pediría añadir los que ya tiene. Ahora llama a R.usar_sitio. Peor de cara al futuro, y por eso queda escrito en el módulo: hoy no hay ningún slug repetido entre los dos blogs, pero el día que lo hubiera el validador habría validado el post equivocado sin decir una palabra. --- seo_validate.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/seo_validate.py b/seo_validate.py index b0f1b5b..dc6e94f 100644 --- a/seo_validate.py +++ b/seo_validate.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Tool C — pre-publish SEO validator (theexclusionzone.com, Ghost). +Tool C — pre-publish SEO validator para los dos blogs (Ghost). A single-post GATE around the SHARED rule engine in seo_rules.py. It does NOT re-implement any rule — it calls check_post(), the exact same engine Tool A @@ -12,7 +12,7 @@ the issues we just cleaned up site-wide: long meta_description, empty OG/Twitter missing alt, too-few internal links. Modes: - python3 seo_validate.py --slug # fetch one post (draft OR published) via ghst-en + python3 seo_validate.py --slug [--site es] # un post (borrador o publicado) python3 seo_validate.py --id # same, by id python3 seo_validate.py --json # validate a post object from a JSON file # (raw post dict, or {"posts":[{...}]}) @@ -43,7 +43,15 @@ GROUPS = [ ] -def fetch_corpus(): +# ⚠️ Era EN-only y el checklist del remate afirmaba que todas las herramientas +# aceptan --site: el paso 5 (validar hasta que quede limpio) NUNCA pudo +# completarse en el ES, porque preguntar por un slug español a ghst-en da 404. +# Peor de cara al futuro: si un slug llegara a existir en los dos blogs, habría +# validado el post EQUIVOCADO sin decir nada. +SITIOS = {"en": "ghst-en", "es": "ghst-es"} + + +def fetch_corpus(cli="ghst-en"): """All published+scheduled posts (id, title, slug, status) via ghst-en, for the topic-collision check. Returns [] (with a warning) if the fetch fails — the validator still runs the per-post rules.""" @@ -52,7 +60,7 @@ def fetch_corpus(): try: # canonical_url hace falta para NO marcar como colisión un par que ya # está consolidado a propósito (ver topic_collision en seo_rules.py). - cmd = (f"ghst-en --json post list --limit all " + cmd = (f"{cli} --json post list --limit all " f"--fields id,title,slug,status,canonical_url > {path}") r = subprocess.run(cmd, shell=True, stderr=subprocess.PIPE, text=True) if r.returncode != 0: @@ -66,7 +74,7 @@ def fetch_corpus(): os.unlink(path) -def fetch_one(selector, value): +def fetch_one(selector, value, cli="ghst-en"): """Fetch a single post (draft or published) via ghst-en → post dict. selector is 'slug' or 'id'. Redirect to a temp file (a post with html+lexical @@ -76,12 +84,12 @@ def fetch_one(selector, value): try: flag = "--slug" if selector == "slug" else "" target = value if selector == "slug" else value - cmd = (f"ghst-en post get {flag} {target} --formats html,lexical --json > {path}" + cmd = (f"{cli} post get {flag} {target} --formats html,lexical --json > {path}" if selector == "slug" - else f"ghst-en post get {target} --formats html,lexical --json > {path}") + else f"{cli} post get {target} --formats html,lexical --json > {path}") r = subprocess.run(cmd, shell=True, stderr=subprocess.PIPE, text=True) if r.returncode != 0: - sys.exit(f"ghst-en fetch failed for {selector}={value}:\n{r.stderr}") + sys.exit(f"{cli} fetch failed for {selector}={value}:\n{r.stderr}") data = json.load(open(path)) return _unwrap(data) finally: @@ -156,18 +164,24 @@ def main(): g.add_argument("--slug") g.add_argument("--id") g.add_argument("--json", metavar="FILE") + ap.add_argument("--site", choices=("en", "es"), default="en") ap.add_argument("--no-collision", action="store_true", help="skip the topic-collision check (intentional follow-up piece)") args = ap.parse_args() + # El motor decide qué href es interno con R.SITE_HOST: sin esto, validar un + # post del ES contaría cero enlaces internos y pediría añadir los que ya tiene. + R.usar_sitio(args.site) + cli = SITIOS[args.site] + if args.json: post = load_json(args.json) elif args.slug: - post = fetch_one("slug", args.slug) + post = fetch_one("slug", args.slug, cli) else: - post = fetch_one("id", args.id) + post = fetch_one("id", args.id, cli) - corpus = [] if args.no_collision else fetch_corpus() + corpus = [] if args.no_collision else fetch_corpus(cli) n = validate(post, corpus) sys.exit(1 if n else 0)