títulos del ES en mayúscula de oración: 91 campos y la regla que lo vigila
Los encabezados llevaban limpios desde el 21 de julio, pero la regla solo miraba <h2>/<h3>/<h4>: 28 de los 31 títulos publicados del ES seguían en Title Case inglés, con sus meta_title, og_title y twitter_title detrás. Es justo lo que el lector español lee en Google. seo_case.py reescribe, y no reutiliza el detector tal cual porque no vale para esto: `_title_case_words` decide con una sola prueba —«¿la he visto en minúscula alguna vez?»— y eso deja fuera los verbos que solo viven en titulares («Ocultan», «Conmocionó», «Penetró») y las palabras de dos letras, que ni entran en el vocabulario. La primera versión devolvía títulos a medio arreglar y rompía «Talavera la Real» y «vuelo Iberia». Ahora se decide con dos pruebas —nunca en minúscula Y en mayúscula a media frase— más una lista de palabras gramaticales, y las frases propias se enmascaran conservando las posiciones para reescribir por desplazamiento sobre el original. Decisión de Jose: «Ejército», «Gobierno» y demás instituciones conservan la mayúscula; «Archivos» baja salvo en «Los Archivos PURSUE», que se protege por frase porque ahí se distingue por contexto, no por palabra suelta. check_title_case cierra el agujero en seo_watch. Simulado el ciclo entero antes de tocar nada: 78 hallazgos → 16 tras el pase automático → 0 con la decisión aplicada. Aplicado y verificado releyendo Ghost campo a campo (91 de 91), con backup pristine por post en ~/link-batch-backup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+335
@@ -0,0 +1,335 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Mayúscula de oración en TÍTULOS y METAS del blog ES.
|
||||
|
||||
Por qué existe. La regla de Title Case inglés ya estaba escrita y funcionando
|
||||
desde el 2026-07-21 —aquella limpieza quitó 219 encabezados— pero
|
||||
`check_heading_case` solo mira `<h2>/<h3>/<h4>`. El título del post y el
|
||||
`meta_title` nunca entraron en su ámbito, y son justo lo que el lector español
|
||||
lee en Google. Barrido del 2026-07-29 sobre los 31 publicados del ES:
|
||||
|
||||
títulos en Title Case inglés 28 de 31
|
||||
meta_title 16 de 31
|
||||
og_title desincronizados de su meta_title 3
|
||||
|
||||
No se reescribe con un modelo ni con reglas de mayúsculas propias: se
|
||||
MINUSCULIZAN EXACTAMENTE las palabras que marca `_title_case_words`, que es la
|
||||
misma función, el mismo vocabulario del corpus y el mismo umbral que ya decide
|
||||
esto en los encabezados. El resto del texto no se toca ni un byte, y el slug
|
||||
NUNCA se toca (cambiarlo obligaría a un redirect 301 y no hay motivo).
|
||||
|
||||
Lo que la máquina NO decide. Hay palabras que llevan mayúscula cuando nombran a
|
||||
la institución («el Ejército del Aire») y minúscula cuando son comunes («el
|
||||
ejército recuperó los restos»). El corpus las escribe de las dos formas, así
|
||||
que el vocabulario las marca y la propuesta las bajaría sin saber cuál es cuál.
|
||||
Esas se apartan en DUDOSAS para que las mire una persona.
|
||||
|
||||
Uso:
|
||||
python3 seo_case.py # en seco: informe de propuestas
|
||||
python3 seo_case.py --md propuestas.md # además, deja el diff en fichero
|
||||
python3 seo_case.py --apply --si-de-verdad
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import seo_rules as R
|
||||
from seo_audit import fetch_posts
|
||||
from seo_watch import CASE_KEEP, CASE_PHRASES, lowercase_vocab
|
||||
|
||||
CLI = "ghst-es"
|
||||
BACKUP_DIR = os.path.expanduser("~/link-batch-backup")
|
||||
|
||||
# Nombres de institución: conservan la mayúscula («el Ejército recuperó los
|
||||
# restos», «lo que el Gobierno acaba de confirmar»). Salieron del barrido como
|
||||
# dudosas —el corpus las escribe de las dos formas— y las resolvió Jose el
|
||||
# 2026-07-29: institución, mayúscula.
|
||||
#
|
||||
# «Archivos» se cayó de esta lista en esa misma decisión: en «222 archivos
|
||||
# OVNI» o «162 archivos secretos» es un nombre común y va en minúscula. El
|
||||
# único que es nombre propio, «Los Archivos PURSUE», se protege por frase en
|
||||
# CASE_PHRASES, que es donde se distingue por contexto y no por palabra suelta.
|
||||
INSTITUCIONES = {"Ejército", "Gobierno", "Estado", "Fuerzas", "Armada",
|
||||
"Iglesia", "Congreso", "Capitolio", "Pentágono"}
|
||||
|
||||
# Sin resolver: palabras que el barrido marque como ambiguas en el futuro. Un
|
||||
# campo que contenga una de estas NO se aplica a medias, va a revisión humana.
|
||||
DUDOSAS = set()
|
||||
|
||||
CAMPOS = ("title", "meta_title", "og_title", "twitter_title")
|
||||
|
||||
# Palabras gramaticales: van SIEMPRE en minúscula a media frase. Hacen falta
|
||||
# explícitamente porque las dos pruebas del corpus no las alcanzan —
|
||||
# `lowercase_vocab` solo recoge palabras de 3 letras o más, así que «el», «la»,
|
||||
# «lo», «un» o «se» jamás entran, y en el cuerpo del blog aparecen capitalizadas
|
||||
# a media frase lo bastante como para pasar por nombre propio. El resultado era
|
||||
# «Betty y Barney Hill 1961: El primer caso…», mitad arreglado.
|
||||
# Los artículos que SON parte de un topónimo («Los Ángeles», «Los Villares»,
|
||||
# «El Yunque») no se ven afectados: van en CASE_PHRASES y se enmascaran antes.
|
||||
FUNCION = {
|
||||
"el", "la", "lo", "los", "las", "un", "una", "unos", "unas", "al", "del",
|
||||
"de", "en", "y", "e", "o", "u", "a", "se", "no", "ni", "si", "su", "sus",
|
||||
"que", "con", "por", "para", "sin", "sobre", "tras", "ante", "entre",
|
||||
"es", "son", "fue", "ya", "más", "muy", "como", "cuando", "donde",
|
||||
"dos", "tres", "sus", "le", "les", "me", "te", "nos",
|
||||
}
|
||||
|
||||
_PAL = re.compile(r"[0-9A-Za-zÁÉÍÓÚÜÑáéíóúüñ][0-9A-Za-zÁÉÍÓÚÜÑáéíóúüñ'’\-\.]*")
|
||||
|
||||
|
||||
def nombres_propios(posts, minimo=1):
|
||||
"""Palabras que el corpus escribe EN MAYÚSCULA A MEDIA FRASE.
|
||||
|
||||
Es la prueba que le faltaba a `lowercase_vocab`, y la razón está medida.
|
||||
`lowercase_vocab` responde «¿la he visto alguna vez en minúscula?», y para
|
||||
DETECTAR sobra: si nunca la has visto en minúscula, callas y no molestas.
|
||||
Para REESCRIBIR no sirve, porque hay verbos vivos que solo existen en los
|
||||
titulares —«Ocultan», «Conmocionó», «Penetró», «Desvelan», «Se Toparon»—
|
||||
y el cuerpo del blog no los usa nunca en minúscula. La primera versión de
|
||||
esta herramienta los dejaba en mayúscula y devolvía títulos a medio
|
||||
arreglar, que es peor que no tocarlos.
|
||||
|
||||
Así que la decisión se toma con las DOS pruebas: una palabra conserva la
|
||||
mayúscula solo si el corpus nunca la escribe en minúscula Y además la
|
||||
escribe en mayúscula a media frase. «Roswell» pasa las dos; «Ocultan» no
|
||||
pasa la segunda y baja.
|
||||
"""
|
||||
c = {}
|
||||
for p in posts:
|
||||
txt = re.sub(r"<[^>]+>", " ", p.get("html") or "")
|
||||
txt = re.sub(r"\s+", " ", txt)
|
||||
primera = True
|
||||
for m in _PAL.finditer(txt):
|
||||
tok = m.group(0)
|
||||
antes = txt[:m.start()]
|
||||
abre = primera or bool(re.search(r"[.?!¿¡:;»\"]\s*$", antes))
|
||||
primera = False
|
||||
if not abre and tok[:1].isupper() and tok[1:].islower():
|
||||
c[tok] = c.get(tok, 0) + 1
|
||||
return {w for w, n in c.items() if n >= minimo}
|
||||
|
||||
|
||||
def _enmascara(texto):
|
||||
"""Sustituye las frases propias por relleno de la MISMA longitud.
|
||||
|
||||
Tiene que conservar las posiciones: la reescritura se hace por desplazamiento
|
||||
sobre el texto original, y `re.sub(frase, ' ')` —lo que hace el detector—
|
||||
acorta la cadena y desalinea todo. Sin esto, «La Batalla de Los Ángeles»
|
||||
perdería la mayúscula de «Los», que es parte del topónimo.
|
||||
"""
|
||||
for ph in CASE_PHRASES:
|
||||
texto = re.sub(re.escape(ph), lambda m: "\0" * len(m.group(0)),
|
||||
texto, flags=re.IGNORECASE)
|
||||
return texto
|
||||
|
||||
|
||||
def propone(texto, vocab, propios):
|
||||
"""(nuevo, bajadas, dudosas). Solo minusculiza; nunca reordena ni recorta."""
|
||||
if not texto:
|
||||
return texto, [], []
|
||||
mascara = _enmascara(texto)
|
||||
chars = list(texto)
|
||||
bajadas, dudosas = [], []
|
||||
for m in _PAL.finditer(mascara):
|
||||
tok = m.group(0)
|
||||
# «Primera palabra» se mide sobre el ORIGINAL, no sobre la máscara. Si
|
||||
# el título abre con una frase propia («Immaculate Constellation: El
|
||||
# programa…»), la máscara la deja en ceros y la palabra siguiente
|
||||
# parecía la primera del texto, así que se salvaba: quedaban «El
|
||||
# programa UAP secreto» y «Archivo secreto OVNI» sin tocar.
|
||||
antes = texto[:m.start()]
|
||||
# En español, tras dos puntos NO va mayúscula: a diferencia del
|
||||
# detector, aquí los dos puntos sí abren minúscula.
|
||||
abre = (not re.search(r"[0-9A-Za-zÁÉÍÓÚÜÑáéíóúüñ]", antes)
|
||||
or bool(re.search(r"[.?!]\s*$", antes)))
|
||||
if abre or "\0" in tok:
|
||||
continue
|
||||
if not (tok[:1].isupper() and tok[1:].islower()):
|
||||
continue # acrónimos (OVNI, EE.UU.) y ya-minúsculas
|
||||
if tok in CASE_KEEP or tok in INSTITUCIONES:
|
||||
continue
|
||||
if tok.lower() not in FUNCION and tok.lower() not in vocab and tok in propios:
|
||||
continue # nombre propio probado por las dos pruebas
|
||||
if tok in DUDOSAS:
|
||||
dudosas.append(tok)
|
||||
continue
|
||||
chars[m.start()] = tok[:1].lower()
|
||||
bajadas.append(tok)
|
||||
return "".join(chars), bajadas, dudosas
|
||||
|
||||
|
||||
def revisa(posts):
|
||||
"""Propuestas por post publicado.
|
||||
|
||||
Lo PROGRAMADO se deja fuera a propósito: sale del remate con las metas ya
|
||||
puestas y aún se puede editar sin prisa; meterlo aquí mezcla una limpieza
|
||||
de lo publicado con la cola de agosto.
|
||||
"""
|
||||
vocab = lowercase_vocab(posts)
|
||||
propios = nombres_propios(posts)
|
||||
fuera = []
|
||||
for p in posts:
|
||||
if p.get("status") != "published":
|
||||
continue
|
||||
cambios, revisar = {}, {}
|
||||
for campo in CAMPOS:
|
||||
actual = p.get(campo) or ""
|
||||
nuevo, _, dud = propone(actual, vocab, propios)
|
||||
if nuevo == actual:
|
||||
continue
|
||||
if dud:
|
||||
# Un campo con una palabra dudosa NO se aplica a medias: dejaría
|
||||
# «162 Archivos secretos», que está peor que como estaba. Va
|
||||
# entero a revisión humana, con la propuesta parcial a la vista.
|
||||
revisar[campo] = (nuevo, sorted(set(dud)))
|
||||
else:
|
||||
cambios[campo] = nuevo
|
||||
# El og/twitter que hoy espeja al meta tiene que seguir espejándolo:
|
||||
# si no, la limpieza deja el título social viejo en Title Case.
|
||||
meta_nuevo = cambios.get("meta_title")
|
||||
if meta_nuevo:
|
||||
for campo in ("og_title", "twitter_title"):
|
||||
if (p.get(campo) or "").strip() == (p.get("meta_title") or "").strip():
|
||||
cambios[campo] = meta_nuevo
|
||||
revisar.pop(campo, None)
|
||||
if cambios or revisar:
|
||||
fuera.append((p, cambios, revisar))
|
||||
return fuera
|
||||
|
||||
|
||||
def comprueba_limites(cambios):
|
||||
"""Los límites duros de Ghost/SERP. Minusculizar no cambia la longitud,
|
||||
así que esto solo puede saltar si el campo YA venía largo: se avisa y no
|
||||
se cuenta como culpa de esta herramienta."""
|
||||
avisos = []
|
||||
mt = cambios.get("meta_title")
|
||||
if mt and len(mt) > R.META_TITLE_MAX:
|
||||
avisos.append(f"meta_title {len(mt)} car. > {R.META_TITLE_MAX} (ya venía largo)")
|
||||
return avisos
|
||||
|
||||
|
||||
def informe(fuera, md=None):
|
||||
L = []
|
||||
tocados = [f for f in fuera if f[1]]
|
||||
n_campos = sum(len(c) for _, c, _ in fuera)
|
||||
n_rev = sum(len(r) for _, _, r in fuera)
|
||||
L.append(f"{len(tocados)} post(s) con títulos/metas en Title Case inglés "
|
||||
f"→ {n_campos} campo(s) automáticos, {n_rev} a decisión humana\n")
|
||||
for p, cambios, revisar in fuera:
|
||||
L.append(f"── {p['slug']}")
|
||||
for campo, nuevo in cambios.items():
|
||||
L.append(f" {campo}")
|
||||
L.append(f" antes: {p.get(campo)}")
|
||||
L.append(f" ahora: {nuevo}")
|
||||
for a in comprueba_limites(cambios):
|
||||
L.append(f" ⚠ {a}")
|
||||
for campo, (nuevo, dud) in revisar.items():
|
||||
L.append(f" {campo} ⚠ DECIDE UNA PERSONA ({', '.join(dud)}: "
|
||||
f"¿institución o nombre común?)")
|
||||
L.append(f" antes: {p.get(campo)}")
|
||||
L.append(f" borr.: {nuevo}")
|
||||
L.append("")
|
||||
texto = "\n".join(L)
|
||||
print(texto)
|
||||
if md:
|
||||
hoy = datetime.date.today().isoformat()
|
||||
with open(md, "w") as fh:
|
||||
fh.write(
|
||||
"# Títulos y metas del ES en mayúscula de oración\n\n"
|
||||
f"Propuesta de `seo_case.py`, generada EN SECO el {hoy}.\n\n"
|
||||
"Solo se minusculizan las palabras que el propio corpus escribe en\n"
|
||||
"minúscula en otros sitios (mismo vocabulario que usa la regla de\n"
|
||||
"encabezados). El resto del texto y el slug no se tocan.\n\n"
|
||||
"```\n" + texto + "\n```\n")
|
||||
print(f"[propuestas escritas en {md}]")
|
||||
return n_campos
|
||||
|
||||
|
||||
def aplica(p, cambios):
|
||||
"""Escribe en Ghost por el mismo camino que seo_finish: backup pristine,
|
||||
`ghst-es post update --from-json` y re-lectura para verificar."""
|
||||
os.makedirs(BACKUP_DIR, exist_ok=True)
|
||||
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
bpath = os.path.join(BACKUP_DIR, f"es-{p['slug'][:40]}-seocase-pristine-{ts}.json")
|
||||
with open(bpath, "w") as fh:
|
||||
json.dump({"id": p["id"], "slug": p["slug"],
|
||||
**{k: p.get(k) for k in cambios}}, fh, ensure_ascii=False, indent=1)
|
||||
|
||||
fd, path = tempfile.mkstemp(suffix=".json", prefix="seo_case_")
|
||||
os.close(fd)
|
||||
try:
|
||||
with open(path, "w") as fh:
|
||||
json.dump(cambios, fh, ensure_ascii=False)
|
||||
r = subprocess.run(f'{CLI} post update {p["id"]} --from-json "{path}"',
|
||||
shell=True, capture_output=True, text=True, timeout=240)
|
||||
finally:
|
||||
os.unlink(path)
|
||||
if "Slug:" not in r.stdout:
|
||||
print(r.stdout, r.stderr)
|
||||
return False, os.path.basename(bpath)
|
||||
return True, os.path.basename(bpath)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--json", help="posts guardados, en vez de pedirlos a Ghost")
|
||||
ap.add_argument("--md", help="deja el diff completo en este fichero")
|
||||
ap.add_argument("--apply", action="store_true")
|
||||
ap.add_argument("--si-de-verdad", action="store_true",
|
||||
help="obligatorio junto a --apply: son títulos publicados")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.json:
|
||||
data = json.load(open(args.json))
|
||||
posts = data["posts"] if isinstance(data, dict) and "posts" in data else data
|
||||
else:
|
||||
posts = fetch_posts(CLI)
|
||||
|
||||
fuera = revisa(posts)
|
||||
informe(fuera, args.md)
|
||||
|
||||
if not args.apply:
|
||||
print("(en seco: no se ha tocado nada — repite con --apply --si-de-verdad)")
|
||||
return 0
|
||||
if not args.si_de_verdad:
|
||||
print("✗ --apply necesita --si-de-verdad.")
|
||||
return 1
|
||||
|
||||
ok = fallos = 0
|
||||
esperado = {}
|
||||
for p, cambios, _ in fuera:
|
||||
if not cambios:
|
||||
continue
|
||||
bien, backup = aplica(p, cambios)
|
||||
print(f" {'✓' if bien else '✗'} {p['slug']} (backup {backup})")
|
||||
ok += bien
|
||||
fallos += not bien
|
||||
if bien:
|
||||
esperado[p["slug"]] = cambios
|
||||
print(f"\n{ok} post(s) actualizados, {fallos} fallo(s)")
|
||||
|
||||
# Verificación contra la realidad, no contra el código de salida: se
|
||||
# re-lee Ghost y se compara campo a campo. Un «Slug:» en la salida del CLI
|
||||
# solo prueba que la llamada volvió, no que el texto esté guardado.
|
||||
print("\nreleyendo de Ghost para verificar…")
|
||||
ahora = {q["slug"]: q for q in fetch_posts(CLI)}
|
||||
malos = [(s, c) for s, camb in esperado.items() for c, v in camb.items()
|
||||
if (ahora.get(s, {}).get(c) or "") != v]
|
||||
if malos:
|
||||
print(f"⚠ {len(malos)} campo(s) NO quedaron como se pidió:")
|
||||
for s, c in malos[:8]:
|
||||
print(f" {s[:36]} · {c}")
|
||||
return 1
|
||||
print(f"✓ verificado: {sum(len(c) for c in esperado.values())} campos "
|
||||
f"guardados tal cual")
|
||||
return 1 if fallos else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+44
-6
@@ -262,6 +262,9 @@ CASE_PHRASES = [
|
||||
"Maria Blair", "Kenneth Arnold", "Memorándum Halt", "Moment of Contact",
|
||||
"Los Ángeles", "Los Villares", "Puerto Rico", "El Yunque",
|
||||
"Francisco Julio Padrón", # «Julio» aquí es nombre de pila, no el mes
|
||||
"Talavera la Real", # el pueblo de Badajoz: «Real» es del topónimo
|
||||
"Archivos PURSUE", # nombre propio de la desclasificación; el resto
|
||||
# de «archivos» del corpus son nombre común
|
||||
]
|
||||
CASE_KEEP = {
|
||||
"Pentágono", "Roswell", "Rendlesham", "Varginha", "Manises", "Nimitz",
|
||||
@@ -277,7 +280,7 @@ CASE_KEEP = {
|
||||
"Lake", "Huron", "Villares", "Yunque", "Ángeles", "Memorándum", "Moment",
|
||||
"Contact", "Dr.", "Sr.", "San", "Santa", "Francisco", "David", "Kurt",
|
||||
"Travis", "Donald", "Theresa", "Jesse", "Walter", "Maria", "Marco", "Eli",
|
||||
"Kenneth", "Ártico", "Julio",
|
||||
"Kenneth", "Ártico", "Julio", "Iberia", # la aerolínea del caso Manises
|
||||
}
|
||||
_CASE_WORD = re.compile(r"[0-9A-Za-zÁÉÍÓÚÜÑáéíóúüñ][0-9A-Za-zÁÉÍÓÚÜÑáéíóúüñ'’\-\.]*")
|
||||
_CASE_ABBR = re.compile(r"(?:^|\s)(?:[A-ZÁÉÍÓÚÑ]{1,3}|Dr|Sr|Sra|vs|etc)\.\s*$")
|
||||
@@ -370,6 +373,34 @@ def check_heading_case(posts, site):
|
||||
return out
|
||||
|
||||
|
||||
def check_title_case(posts, site):
|
||||
"""Lo mismo, pero en el TÍTULO y el meta_title — que es lo que se lee en Google.
|
||||
|
||||
El agujero se vio el 2026-07-29: los encabezados llevaban limpios desde el
|
||||
21 de julio y, sin embargo, 28 de los 31 títulos publicados del ES seguían
|
||||
en Title Case inglés, con 16 meta_title detrás. La regla estaba escrita y
|
||||
funcionando; simplemente nunca había mirado ahí.
|
||||
|
||||
Umbral de 2, igual que en los encabezados y por el mismo motivo. Se miran
|
||||
también `og_title` y `twitter_title` porque son los que viajan a X y a
|
||||
Facebook, y suelen ser copia del meta: si se limpia uno y no los otros, el
|
||||
título viejo sigue circulando por ahí.
|
||||
"""
|
||||
if site != "es":
|
||||
return []
|
||||
vocab = lowercase_vocab(posts)
|
||||
out = []
|
||||
for p in posts:
|
||||
if p.get("status") != "published":
|
||||
continue
|
||||
for campo in ("title", "meta_title", "og_title", "twitter_title"):
|
||||
texto = (p.get(campo) or "").strip()
|
||||
malas = _title_case_words(texto, vocab)
|
||||
if len(malas) >= 2:
|
||||
out.append((p["slug"], f"{campo}: {texto[:44]}", malas[:4]))
|
||||
return out
|
||||
|
||||
|
||||
def check_rules(posts, site):
|
||||
"""Violaciones accionables (excluye waivers e INFO), agrupadas por regla."""
|
||||
out = {}
|
||||
@@ -385,7 +416,7 @@ def check_rules(posts, site):
|
||||
|
||||
# ---------- salida ----------
|
||||
|
||||
def build_report(roto, nocanon, prematuro, envejecido, n_sitemap, sm_malos, reglas, mayus):
|
||||
def build_report(roto, nocanon, prematuro, envejecido, n_sitemap, sm_malos, reglas, mayus, titulos):
|
||||
L = []
|
||||
if roto:
|
||||
L.append(f"🔴 {len(roto)} enlace(s) interno(s) ROTO(S):")
|
||||
@@ -408,6 +439,9 @@ def build_report(roto, nocanon, prematuro, envejecido, n_sitemap, sm_malos, regl
|
||||
if mayus:
|
||||
L.append(f"🔤 {len(mayus)} encabezado(s) en Title Case inglés (el ES va en mayúscula de oración):")
|
||||
L += [f" {s[:24]}: «{t[:34]}» ({', '.join(w)})" for s, t, w in mayus[:6]]
|
||||
if titulos:
|
||||
L.append(f"🔠 {len(titulos)} título/meta en Title Case inglés (es lo que se lee en Google):")
|
||||
L += [f" {s[:20]}: {t}" for s, t, _ in titulos[:6]]
|
||||
if reglas:
|
||||
tot = sum(len(v) for v in reglas.values())
|
||||
L.append(f"📋 {tot} violación(es) accionable(s) del auditor:")
|
||||
@@ -417,7 +451,7 @@ def build_report(roto, nocanon, prematuro, envejecido, n_sitemap, sm_malos, regl
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
def fingerprint(roto, nocanon, prematuro, envejecido, sm_malos, reglas, mayus):
|
||||
def fingerprint(roto, nocanon, prematuro, envejecido, sm_malos, reglas, mayus, titulos):
|
||||
return json.dumps({
|
||||
"roto": sorted(f"{s}|{u}" for s, u in roto),
|
||||
"nocanon": sorted(f"{s}|{u}" for s, u in nocanon),
|
||||
@@ -425,6 +459,7 @@ def fingerprint(roto, nocanon, prematuro, envejecido, sm_malos, reglas, mayus):
|
||||
"envejecido": sorted(f"{s}|{t}|{m}" for s, _, t, m in envejecido),
|
||||
"sitemap": sorted(u for u, _ in sm_malos),
|
||||
"mayus": sorted(f"{s}|{t}" for s, t, _ in mayus),
|
||||
"titulos": sorted(f"{s}|{t}" for s, t, _ in titulos),
|
||||
"reglas": {k: sorted(v) for k, v in reglas.items()},
|
||||
}, sort_keys=True)
|
||||
|
||||
@@ -450,13 +485,16 @@ def run_site(site):
|
||||
n_sm, sm_malos = check_sitemap()
|
||||
reglas = check_rules(posts, site)
|
||||
mayus = check_heading_case(posts, site)
|
||||
titulos = check_title_case(posts, site)
|
||||
|
||||
body = build_report(roto, nocanon, prematuro, envejecido, n_sm, sm_malos, reglas, mayus)
|
||||
body = build_report(roto, nocanon, prematuro, envejecido, n_sm, sm_malos, reglas, mayus,
|
||||
titulos)
|
||||
pub = sum(1 for p in posts if p.get("status") == "published")
|
||||
header = (f"── [{site.upper()}] {cfg['host']}\n"
|
||||
f" {pub} publicados, {len(posts)} totales, sitemap {n_sm} URLs")
|
||||
fp = fingerprint(roto, nocanon, prematuro, envejecido, sm_malos, reglas, mayus)
|
||||
hallazgos = bool(roto or nocanon or prematuro or envejecido or sm_malos or mayus)
|
||||
fp = fingerprint(roto, nocanon, prematuro, envejecido, sm_malos, reglas, mayus, titulos)
|
||||
hallazgos = bool(roto or nocanon or prematuro or envejecido or sm_malos or mayus
|
||||
or titulos)
|
||||
return header + "\n" + body, fp, hallazgos
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
# Títulos y metas del ES en mayúscula de oración
|
||||
|
||||
Propuesta de `seo_case.py`, generada EN SECO el 2026-07-29.
|
||||
|
||||
Solo se minusculizan las palabras que el propio corpus escribe en
|
||||
minúscula en otros sitios (mismo vocabulario que usa la regla de
|
||||
encabezados). El resto del texto y el slug no se tocan.
|
||||
|
||||
```
|
||||
25 post(s) con títulos/metas en Title Case inglés → 72 campo(s) automáticos, 19 a decisión humana
|
||||
|
||||
── trans-en-provence-1981-ovni-gepan
|
||||
title
|
||||
antes: La Noche del Misterio en Trans-en-Provence: El Caso OVNI Mejor Documentado de la Historia
|
||||
ahora: La noche del misterio en Trans-en-Provence: el caso OVNI mejor documentado de la historia
|
||||
|
||||
── chiles-whitted-1948-piloto-eastern-airlines-ovni
|
||||
title
|
||||
antes: El Encuentro de Chiles-Whitted: Cuando Dos Pilotos Presenciaron lo Inexplicable a 5.000 Pies de Altura
|
||||
ahora: El encuentro de Chiles-Whitted: cuando dos pilotos presenciaron lo inexplicable a 5.000 pies de altura
|
||||
og_title
|
||||
antes: Chiles-Whitted 1948: Pilotos ante lo Inexplicable
|
||||
ahora: Chiles-Whitted 1948: pilotos ante lo inexplicable
|
||||
|
||||
── batalla-los-angeles-1942-ovni-antiaereo
|
||||
title
|
||||
antes: La Batalla de Los Ángeles 1942: Pánico Antiaéreo Bajo Reflectores y Misterio Sin Resolver
|
||||
ahora: La batalla de Los Ángeles 1942: pánico antiaéreo bajo reflectores y misterio sin resolver
|
||||
og_title
|
||||
antes: La Batalla de Los Ángeles 1942: Pánico y Misterio
|
||||
ahora: La batalla de Los Ángeles 1942: pánico y misterio
|
||||
|
||||
── vaticano-extraterrestres-iglesia-catolica
|
||||
title
|
||||
antes: El Vaticano y los Extraterrestres: Una Iglesia Abierta al Misterio del Cosmos
|
||||
ahora: El Vaticano y los extraterrestres: una Iglesia abierta al misterio del cosmos
|
||||
og_title
|
||||
antes: El Vaticano y los Extraterrestres
|
||||
ahora: El Vaticano y los extraterrestres
|
||||
|
||||
── pascagoula-1973-abduccion-hickson-parker
|
||||
title
|
||||
antes: Pascagoula 1973: La Noche que Cambió a Dos Pescadores para Siempre
|
||||
ahora: Pascagoula 1973: la noche que cambió a dos pescadores para siempre
|
||||
meta_title
|
||||
antes: Pascagoula 1973: La Abducción Alien Mejor Documentada
|
||||
ahora: Pascagoula 1973: la abducción alien mejor documentada
|
||||
og_title
|
||||
antes: Pascagoula 1973: La Abducción Alien Mejor Documentada
|
||||
ahora: Pascagoula 1973: la abducción alien mejor documentada
|
||||
twitter_title
|
||||
antes: Pascagoula 1973: La Abducción Alien Mejor Documentada
|
||||
ahora: Pascagoula 1973: la abducción alien mejor documentada
|
||||
|
||||
── los-villares-1996-el-encuentro-extraterrestre-que-marco-la-ufologia-espanola
|
||||
title
|
||||
antes: Los Villares 1996: El Encuentro Extraterrestre que Marcó la Ufología Española
|
||||
ahora: Los Villares 1996: el encuentro extraterrestre que marcó la ufología española
|
||||
meta_title
|
||||
antes: Los Villares 1996: El Encuentro OVNI que Marcó España
|
||||
ahora: Los Villares 1996: el encuentro OVNI que marcó España
|
||||
og_title
|
||||
antes: Los Villares 1996: El Encuentro OVNI que Marcó España
|
||||
ahora: Los Villares 1996: el encuentro OVNI que marcó España
|
||||
twitter_title
|
||||
antes: Los Villares 1996: El Encuentro OVNI que Marcó España
|
||||
ahora: Los Villares 1996: el encuentro OVNI que marcó España
|
||||
|
||||
── gary-mckinnon-el-hacker-que-vio-algo-en-los-servidores-de-la-nasa
|
||||
title
|
||||
antes: Gary McKinnon: El Hacker que Vio Algo en los Servidores de la NASA
|
||||
ahora: Gary McKinnon: el hacker que vio algo en los servidores de la NASA
|
||||
meta_title
|
||||
antes: Gary McKinnon: El Hacker que Penetró el Pentágono
|
||||
ahora: Gary McKinnon: el hacker que penetró el Pentágono
|
||||
og_title
|
||||
antes: Gary McKinnon: El Hacker que Penetró el Pentágono
|
||||
ahora: Gary McKinnon: el hacker que penetró el Pentágono
|
||||
twitter_title
|
||||
antes: Gary McKinnon: El Hacker que Penetró el Pentágono
|
||||
ahora: Gary McKinnon: el hacker que penetró el Pentágono
|
||||
|
||||
── gimbal-gofast-uap-uss-theodore-roosevelt-2015
|
||||
title
|
||||
antes: Gimbal y GoFast: Lo que los Vídeos del USS Theodore Roosevelt Siguen sin Explicar
|
||||
ahora: Gimbal y GoFast: lo que los vídeos del USS Theodore Roosevelt siguen sin explicar
|
||||
|
||||
── grusch-capitolio-9-junio-2026-divulgacion-uap
|
||||
title
|
||||
antes: Grusch en el Capitolio: El Día que la Divulgación UAP Pasó del Testimonio a la Exigencia
|
||||
ahora: Grusch en el Capitolio: el día que la divulgación UAP pasó del testimonio a la exigencia
|
||||
|
||||
── aaro-vs-grusch-pentagono-testimonio-contradiccion
|
||||
title
|
||||
antes: AARO vs. Grusch: Cuando el Pentágono y un Testigo bajo Juramento se Contradicen
|
||||
ahora: AARO vs. Grusch: cuando el Pentágono y un testigo bajo juramento se contradicen
|
||||
|
||||
── archivos-pursue-uap-bases-nucleares-desclasificado
|
||||
title ⚠ DECIDE UNA PERSONA (Archivos, Gobierno: ¿institución o nombre común?)
|
||||
antes: Los Archivos PURSUE y las Bases Nucleares: Lo que el Gobierno Acaba de Confirmar
|
||||
borr.: Los Archivos PURSUE y las bases nucleares: lo que el Gobierno acaba de confirmar
|
||||
|
||||
── 222-archivos-ovni-desclasificados-pentagono-2026
|
||||
title ⚠ DECIDE UNA PERSONA (Archivos: ¿institución o nombre común?)
|
||||
antes: 222 Archivos OVNI Desclasificados: Lo que Revelan, lo que Ocultan y lo que Sigue sin Respuesta
|
||||
borr.: 222 Archivos OVNI desclasificados: lo que revelan, lo que ocultan y lo que sigue sin respuesta
|
||||
meta_title ⚠ DECIDE UNA PERSONA (Archivos: ¿institución o nombre común?)
|
||||
antes: 222 Archivos OVNI: Lo que Revelan y lo que Ocultan
|
||||
borr.: 222 Archivos OVNI: lo que revelan y lo que ocultan
|
||||
og_title ⚠ DECIDE UNA PERSONA (Archivos: ¿institución o nombre común?)
|
||||
antes: 222 Archivos OVNI: Lo que Revelan y lo que Ocultan
|
||||
borr.: 222 Archivos OVNI: lo que revelan y lo que ocultan
|
||||
twitter_title ⚠ DECIDE UNA PERSONA (Archivos: ¿institución o nombre común?)
|
||||
antes: 222 Archivos OVNI: Lo que Revelan y lo que Ocultan
|
||||
borr.: 222 Archivos OVNI: lo que revelan y lo que ocultan
|
||||
|
||||
── kecksburg-1965-objeto-ejercito-pennsylvania
|
||||
meta_title
|
||||
antes: Kecksburg 1965: El Roswell de Pennsylvania
|
||||
ahora: Kecksburg 1965: el Roswell de Pennsylvania
|
||||
og_title
|
||||
antes: Kecksburg 1965: El Roswell de Pennsylvania
|
||||
ahora: Kecksburg 1965: el Roswell de Pennsylvania
|
||||
twitter_title
|
||||
antes: Kecksburg 1965: El Roswell de Pennsylvania
|
||||
ahora: Kecksburg 1965: el Roswell de Pennsylvania
|
||||
title ⚠ DECIDE UNA PERSONA (Ejército: ¿institución o nombre común?)
|
||||
antes: Kecksburg 1965: El Objeto que el Ejército Recuperó y Nadie Explicó
|
||||
borr.: Kecksburg 1965: el objeto que el Ejército recuperó y nadie explicó
|
||||
|
||||
── immaculate-constellation-programa-secreto-pentagon-ovni
|
||||
title
|
||||
antes: Immaculate Constellation: El Programa Secreto del Pentágono que Archiva Todos los OVNIs
|
||||
ahora: Immaculate Constellation: el programa secreto del Pentágono que archiva todos los OVNIs
|
||||
meta_title
|
||||
antes: Immaculate Constellation: Archivo Secreto OVNI del Pentágono
|
||||
ahora: Immaculate Constellation: archivo secreto OVNI del Pentágono
|
||||
og_title
|
||||
antes: Immaculate Constellation: Archivo Secreto OVNI del Pentágono
|
||||
ahora: Immaculate Constellation: archivo secreto OVNI del Pentágono
|
||||
twitter_title
|
||||
antes: Immaculate Constellation: Archivo Secreto OVNI del Pentágono
|
||||
ahora: Immaculate Constellation: archivo secreto OVNI del Pentágono
|
||||
|
||||
── cefaa-chile-unidad-oficial-ovni
|
||||
title
|
||||
antes: CEFAA: Chile, el Único País del Mundo con una Unidad Oficial Investigando OVNIs
|
||||
ahora: CEFAA: Chile, el único país del mundo con una unidad oficial investigando OVNIs
|
||||
meta_title
|
||||
antes: CEFAA: Chile y su Unidad Oficial de Investigación OVNI
|
||||
ahora: CEFAA: Chile y su unidad oficial de investigación OVNI
|
||||
og_title
|
||||
antes: CEFAA: Chile y su Unidad Oficial de Investigación OVNI
|
||||
ahora: CEFAA: Chile y su unidad oficial de investigación OVNI
|
||||
twitter_title
|
||||
antes: CEFAA: Chile y su Unidad Oficial de Investigación OVNI
|
||||
ahora: CEFAA: Chile y su unidad oficial de investigación OVNI
|
||||
|
||||
── segunda-desclasificacion-ovni-mayo-2026
|
||||
title
|
||||
antes: Segunda Desclasificación OVNI Mayo 2026: Apolo 12, Bases Nucleares y 51 Vídeos Inéditos
|
||||
ahora: Segunda desclasificación OVNI mayo 2026: Apolo 12, bases nucleares y 51 vídeos inéditos
|
||||
meta_title
|
||||
antes: Segunda Desclasificación OVNI: Apolo 12 y Bases Nucleares
|
||||
ahora: Segunda desclasificación OVNI: Apolo 12 y bases nucleares
|
||||
og_title
|
||||
antes: Segunda Desclasificación OVNI: Apolo 12 y Bases Nucleares
|
||||
ahora: Segunda desclasificación OVNI: Apolo 12 y bases nucleares
|
||||
twitter_title
|
||||
antes: Segunda Desclasificación OVNI: Apolo 12 y Bases Nucleares
|
||||
ahora: Segunda desclasificación OVNI: Apolo 12 y bases nucleares
|
||||
|
||||
── travis-walton-1975-abduccion-ovni-arizona
|
||||
title
|
||||
antes: Fuego en el Cielo: La Abducción de Travis Walton que Conmocionó a Arizona en 1975
|
||||
ahora: Fuego en el cielo: la abducción de Travis Walton que conmocionó a Arizona en 1975
|
||||
meta_title
|
||||
antes: Travis Walton 1975: La Abducción con Seis Testigos
|
||||
ahora: Travis Walton 1975: la abducción con seis testigos
|
||||
og_title
|
||||
antes: Travis Walton 1975: La Abducción con Seis Testigos
|
||||
ahora: Travis Walton 1975: la abducción con seis testigos
|
||||
twitter_title
|
||||
antes: Travis Walton 1975: La Abducción con Seis Testigos
|
||||
ahora: Travis Walton 1975: la abducción con seis testigos
|
||||
|
||||
── betty-y-barney-hill-1961-el-primer-caso-de-abduccion-extraterrestre-documentado
|
||||
title
|
||||
antes: Betty y Barney Hill 1961: El Primer Caso de Abducción Extraterrestre Documentado
|
||||
ahora: Betty y Barney Hill 1961: el primer caso de abducción extraterrestre documentado
|
||||
meta_title
|
||||
antes: Betty y Barney Hill 1961: La Primera Abducción Documentada
|
||||
ahora: Betty y Barney Hill 1961: la primera abducción documentada
|
||||
og_title
|
||||
antes: Betty y Barney Hill 1961: La Primera Abducción Documentada
|
||||
ahora: Betty y Barney Hill 1961: la primera abducción documentada
|
||||
twitter_title
|
||||
antes: Betty y Barney Hill 1961: La Primera Abducción Documentada
|
||||
ahora: Betty y Barney Hill 1961: la primera abducción documentada
|
||||
|
||||
── las-revelaciones-del-pentagono-sobre-uap-un-hito-historico-en-la-busqueda-de-la-verdad-sobre-fenomenos-anomalos-no-identificados
|
||||
title
|
||||
antes: Las Revelaciones del Pentágono sobre UAP: Un Hito Histórico en la Búsqueda de la Verdad sobre Fenómenos Anómalos No Identificados
|
||||
ahora: Las revelaciones del Pentágono sobre UAP: un hito histórico en la búsqueda de la verdad sobre fenómenos anómalos no identificados
|
||||
meta_title
|
||||
antes: Immaculate Constellation: El Programa UAP Secreto
|
||||
ahora: Immaculate Constellation: el programa UAP secreto
|
||||
og_title
|
||||
antes: Immaculate Constellation: El Programa UAP Secreto
|
||||
ahora: Immaculate Constellation: el programa UAP secreto
|
||||
twitter_title
|
||||
antes: Immaculate Constellation: El Programa UAP Secreto
|
||||
ahora: Immaculate Constellation: el programa UAP secreto
|
||||
|
||||
── phoenix-lights-1997-la-noche-cuando-arizona-vio-lo-inexplicable
|
||||
title
|
||||
antes: Phoenix Lights 1997: La Noche Cuando Arizona Vio lo Inexplicable
|
||||
ahora: Phoenix Lights 1997: la noche cuando Arizona vio lo inexplicable
|
||||
meta_title
|
||||
antes: Phoenix Lights 1997: Dos Eventos que Arizona No Olvidó
|
||||
ahora: Phoenix Lights 1997: dos eventos que Arizona no olvidó
|
||||
og_title
|
||||
antes: Phoenix Lights 1997: Dos Eventos que Arizona No Olvidó
|
||||
ahora: Phoenix Lights 1997: dos eventos que Arizona no olvidó
|
||||
twitter_title
|
||||
antes: Phoenix Lights 1997: Dos Eventos que Arizona No Olvidó
|
||||
ahora: Phoenix Lights 1997: dos eventos que Arizona no olvidó
|
||||
|
||||
── varginha-1996-el-caso-ovni-mas-intrigante-de-brasil
|
||||
title
|
||||
antes: Varginha 1996: El Caso OVNI Más Intrigante de Brasil
|
||||
ahora: Varginha 1996: el caso OVNI más intrigante de Brasil
|
||||
meta_title ⚠ DECIDE UNA PERSONA (Ejército: ¿institución o nombre común?)
|
||||
antes: Caso Varginha 1996: El OVNI que el Ejército Silenció
|
||||
borr.: Caso Varginha 1996: el OVNI que el Ejército silenció
|
||||
og_title ⚠ DECIDE UNA PERSONA (Ejército: ¿institución o nombre común?)
|
||||
antes: Caso Varginha 1996: El OVNI que el Ejército Silenció
|
||||
borr.: Caso Varginha 1996: el OVNI que el Ejército silenció
|
||||
twitter_title ⚠ DECIDE UNA PERSONA (Ejército: ¿institución o nombre común?)
|
||||
antes: Caso Varginha 1996: El OVNI que el Ejército Silenció
|
||||
borr.: Caso Varginha 1996: el OVNI que el Ejército silenció
|
||||
|
||||
── el-incidente-nimitz-2004-cuando-pilotos-de-caza-f-18-se-toparon-con-lo-inexplicable
|
||||
title
|
||||
antes: El Incidente Nimitz 2004: Cuando Pilotos de Caza F-18 Se Toparon con lo Inexplicable
|
||||
ahora: El incidente Nimitz 2004: cuando pilotos de caza F-18 se toparon con lo inexplicable
|
||||
meta_title
|
||||
antes: Nimitz 2004: El Vídeo UAP que el Pentágono Confirmó
|
||||
ahora: Nimitz 2004: el vídeo UAP que el Pentágono confirmó
|
||||
og_title
|
||||
antes: Nimitz 2004: El Vídeo UAP que el Pentágono Confirmó
|
||||
ahora: Nimitz 2004: el vídeo UAP que el Pentágono confirmó
|
||||
twitter_title
|
||||
antes: Nimitz 2004: El Vídeo UAP que el Pentágono Confirmó
|
||||
ahora: Nimitz 2004: el vídeo UAP que el Pentágono confirmó
|
||||
|
||||
── eeuu-desclasifica-162-archivos-secretos-sobre-ovnis-y-vida-extraterrestre-el-pentagono-abre-sus-bovedas
|
||||
title ⚠ DECIDE UNA PERSONA (Archivos: ¿institución o nombre común?)
|
||||
antes: EEUU Desclasifica 162 Archivos Secretos sobre Ovnis y Vida Extraterrestre: El Pentágono Abre sus Bóvedas
|
||||
borr.: EEUU desclasifica 162 Archivos secretos sobre ovnis y vida extraterrestre: el Pentágono abre sus bóvedas
|
||||
meta_title ⚠ DECIDE UNA PERSONA (Archivos: ¿institución o nombre común?)
|
||||
antes: 162 Archivos OVNI Desclasificados: Lo Que Dice el Pentágono
|
||||
borr.: 162 Archivos OVNI desclasificados: lo que dice el Pentágono
|
||||
og_title ⚠ DECIDE UNA PERSONA (Archivos: ¿institución o nombre común?)
|
||||
antes: 162 Archivos OVNI Desclasificados: Lo Que Dice el Pentágono
|
||||
borr.: 162 Archivos OVNI desclasificados: lo que dice el Pentágono
|
||||
twitter_title ⚠ DECIDE UNA PERSONA (Archivos: ¿institución o nombre común?)
|
||||
antes: 162 Archivos OVNI Desclasificados: Lo Que Dice el Pentágono
|
||||
borr.: 162 Archivos OVNI desclasificados: lo que dice el Pentágono
|
||||
|
||||
── roswell-1947-el-misterio-que-cambio-la-historia-de-la-ufologia
|
||||
title
|
||||
antes: Roswell 1947: El Misterio Que Cambió la Historia de la Ufología
|
||||
ahora: Roswell 1947: el misterio que cambió la historia de la ufología
|
||||
meta_title ⚠ DECIDE UNA PERSONA (Ejército: ¿institución o nombre común?)
|
||||
antes: Roswell 1947: El Misterio que el Ejército Nunca Explicó
|
||||
borr.: Roswell 1947: el misterio que el Ejército nunca explicó
|
||||
og_title ⚠ DECIDE UNA PERSONA (Ejército: ¿institución o nombre común?)
|
||||
antes: Roswell 1947: El Misterio que el Ejército Nunca Explicó
|
||||
borr.: Roswell 1947: el misterio que el Ejército nunca explicó
|
||||
twitter_title ⚠ DECIDE UNA PERSONA (Ejército: ¿institución o nombre común?)
|
||||
antes: Roswell 1947: El Misterio que el Ejército Nunca Explicó
|
||||
borr.: Roswell 1947: el misterio que el Ejército nunca explicó
|
||||
|
||||
── la-explosion-de-luz-de-canarias-1976-testigos-militares-espanoles-desvelan-un-secreto-de-la-guerra-fria
|
||||
title
|
||||
antes: La Explosión de Luz de Canarias 1976: Testigos Militares Españoles Desvelan un Secreto de la Guerra Fría
|
||||
ahora: La explosión de luz de Canarias 1976: testigos militares españoles desvelan un secreto de la Guerra Fría
|
||||
meta_title ⚠ DECIDE UNA PERSONA (Ejército: ¿institución o nombre común?)
|
||||
antes: Canarias 1976: El OVNI que Vio el Ejército Español
|
||||
borr.: Canarias 1976: el OVNI que vio el Ejército español
|
||||
og_title ⚠ DECIDE UNA PERSONA (Ejército: ¿institución o nombre común?)
|
||||
antes: Canarias 1976: El OVNI que Vio el Ejército Español
|
||||
borr.: Canarias 1976: el OVNI que vio el Ejército español
|
||||
twitter_title ⚠ DECIDE UNA PERSONA (Ejército: ¿institución o nombre común?)
|
||||
antes: Canarias 1976: El OVNI que Vio el Ejército Español
|
||||
borr.: Canarias 1976: el OVNI que vio el Ejército español
|
||||
|
||||
── el-testimonio-de-david-grusch-el-denunciante-que-cambio-la-conversacion-sobre-ovnis-en-el-congreso-de-ee-uu
|
||||
title
|
||||
antes: El Testimonio de David Grusch: El Denunciante que Cambió la Conversación sobre OVNIs en el Congreso de EE.UU.
|
||||
ahora: El testimonio de David Grusch: el denunciante que cambió la conversación sobre OVNIs en el Congreso de EE.UU.
|
||||
meta_title
|
||||
antes: David Grusch en el Congreso: Lo Que los Medios No Contaron
|
||||
ahora: David Grusch en el Congreso: lo que los medios no contaron
|
||||
og_title
|
||||
antes: David Grusch en el Congreso: Lo Que los Medios No Contaron
|
||||
ahora: David Grusch en el Congreso: lo que los medios no contaron
|
||||
twitter_title
|
||||
antes: David Grusch en el Congreso: Lo Que los Medios No Contaron
|
||||
ahora: David Grusch en el Congreso: lo que los medios no contaron
|
||||
|
||||
── la-noche-que-el-cielo-se-nego-a-revelar-sus-secretos-el-caso-manises-de-1979
|
||||
title
|
||||
antes: La Noche que el Cielo se Negó a Revelar sus Secretos: El Caso Manises de 1979
|
||||
ahora: La noche que el cielo se negó a revelar sus secretos: el caso Manises de 1979
|
||||
meta_title
|
||||
antes: Manises 1979: El OVNI que Obligó a Aterrizar un Vuelo Iberia
|
||||
ahora: Manises 1979: el OVNI que obligó a aterrizar un vuelo Iberia
|
||||
og_title
|
||||
antes: Manises 1979: El OVNI que Obligó a Aterrizar un Vuelo Iberia
|
||||
ahora: Manises 1979: el OVNI que obligó a aterrizar un vuelo Iberia
|
||||
twitter_title
|
||||
antes: Manises 1979: El OVNI que Obligó a Aterrizar un Vuelo Iberia
|
||||
ahora: Manises 1979: el OVNI que obligó a aterrizar un vuelo Iberia
|
||||
|
||||
── el-incidente-ovni-de-rendlesham-forest-1980-el-caso-mas-documentado-de-la-historia-militar-britanica
|
||||
title
|
||||
antes: El Incidente OVNI de Rendlesham Forest 1980: El Caso Más Documentado de la Historia Militar Británica
|
||||
ahora: El incidente OVNI de Rendlesham Forest 1980: el caso más documentado de la historia militar británica
|
||||
meta_title
|
||||
antes: Rendlesham Forest 1980: El OVNI que Vieron los Militares
|
||||
ahora: Rendlesham Forest 1980: el OVNI que vieron los militares
|
||||
og_title
|
||||
antes: Rendlesham Forest 1980: El OVNI que Vieron los Militares
|
||||
ahora: Rendlesham Forest 1980: el OVNI que vieron los militares
|
||||
twitter_title
|
||||
antes: Rendlesham Forest 1980: El OVNI que Vieron los Militares
|
||||
ahora: Rendlesham Forest 1980: el OVNI que vieron los militares
|
||||
|
||||
```
|
||||
Reference in New Issue
Block a user