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:
+25
-11
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
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
|
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.
|
missing alt, too-few internal links.
|
||||||
|
|
||||||
Modes:
|
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 --id <post_id> # same, by id
|
||||||
python3 seo_validate.py --json <file> # validate a post object from a JSON file
|
python3 seo_validate.py --json <file> # validate a post object from a JSON file
|
||||||
# (raw post dict, or {"posts":[{...}]})
|
# (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
|
"""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 topic-collision check. Returns [] (with a warning) if the fetch fails —
|
||||||
the validator still runs the per-post rules."""
|
the validator still runs the per-post rules."""
|
||||||
@@ -52,7 +60,7 @@ def fetch_corpus():
|
|||||||
try:
|
try:
|
||||||
# canonical_url hace falta para NO marcar como colisión un par que ya
|
# 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).
|
# 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}")
|
f"--fields id,title,slug,status,canonical_url > {path}")
|
||||||
r = subprocess.run(cmd, shell=True, stderr=subprocess.PIPE, text=True)
|
r = subprocess.run(cmd, shell=True, stderr=subprocess.PIPE, text=True)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
@@ -66,7 +74,7 @@ def fetch_corpus():
|
|||||||
os.unlink(path)
|
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.
|
"""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
|
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:
|
try:
|
||||||
flag = "--slug" if selector == "slug" else ""
|
flag = "--slug" if selector == "slug" else ""
|
||||||
target = value if selector == "slug" else value
|
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"
|
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)
|
r = subprocess.run(cmd, shell=True, stderr=subprocess.PIPE, text=True)
|
||||||
if r.returncode != 0:
|
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))
|
data = json.load(open(path))
|
||||||
return _unwrap(data)
|
return _unwrap(data)
|
||||||
finally:
|
finally:
|
||||||
@@ -156,18 +164,24 @@ def main():
|
|||||||
g.add_argument("--slug")
|
g.add_argument("--slug")
|
||||||
g.add_argument("--id")
|
g.add_argument("--id")
|
||||||
g.add_argument("--json", metavar="FILE")
|
g.add_argument("--json", metavar="FILE")
|
||||||
|
ap.add_argument("--site", choices=("en", "es"), default="en")
|
||||||
ap.add_argument("--no-collision", action="store_true",
|
ap.add_argument("--no-collision", action="store_true",
|
||||||
help="skip the topic-collision check (intentional follow-up piece)")
|
help="skip the topic-collision check (intentional follow-up piece)")
|
||||||
args = ap.parse_args()
|
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:
|
if args.json:
|
||||||
post = load_json(args.json)
|
post = load_json(args.json)
|
||||||
elif args.slug:
|
elif args.slug:
|
||||||
post = fetch_one("slug", args.slug)
|
post = fetch_one("slug", args.slug, cli)
|
||||||
else:
|
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)
|
n = validate(post, corpus)
|
||||||
sys.exit(1 if n else 0)
|
sys.exit(1 if n else 0)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user