validador: aceptar --site, que llevaba desde siempre clavado al EN

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.
This commit is contained in:
ChemaVX
2026-07-28 07:51:51 +00:00
parent 2e552232f3
commit 7bc9b3e2aa
+25 -11
View File
@@ -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 <slug> # fetch one post (draft OR published) via ghst-en
python3 seo_validate.py --slug <slug> [--site es] # un post (borrador o publicado)
python3 seo_validate.py --id <post_id> # same, by id
python3 seo_validate.py --json <file> # 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)