diff --git a/seo_case.py b/seo_case.py
new file mode 100644
index 0000000..70c4c46
--- /dev/null
+++ b/seo_case.py
@@ -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 `
//`. 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())
diff --git a/seo_watch.py b/seo_watch.py
index 603166b..b4ef72a 100644
--- a/seo_watch.py
+++ b/seo_watch.py
@@ -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
diff --git a/titulos-es-propuestas.md b/titulos-es-propuestas.md
new file mode 100644
index 0000000..0f562fb
--- /dev/null
+++ b/titulos-es-propuestas.md
@@ -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
+
+```