Surtidito → Alt RSS (retrieval → RSS para el blog)
Fecha: 2026-09-12 · Decision: sí, Surtidito se simplifica a un RSS 2.0 que el blog consume. Driver: el producto se siente sobredimensionado; la fragilidad está en el contorno de entrega, no en las fuentes.
Hallazgo de la investigación (sondeo en vivo, 2026-09-12)
74 endpoints probados → ~70 devuelven XML válido hoy:
| Bloque | Vivas | Rota |
|---|---|---|
| Tech RSS (45) | 44 | Zhihu (localhost, fuera del corte) |
| Hermanas RSS (20) | 17 | WaPo ×2 (timeout), Desinformémonos (HTML, no XML) |
Medios MX /feed/ (7) | 6 | Río Doce 500 (temporal) |
| YouTube RSS oficial | 100% | — (sin cuota; Invidious solo respaldo) |
Conclusión: los “problemas” eran X (RSSHub + TWITTER_AUTH_TOKEN) y la capa de entrega (Vercel crons, Supabase, newsletter), no el corpus RSS.
Decisiones
- RSS 2.0 real emitido por
feed.pyjunto afeed.json/feed.html:reporte/feed.rss.xml(feed unificado) yreporte/feed-tech.rss.xml(tech).- Corte para consumo externo: recencia 3 días, cuota 5 items/medio, tope 40 → cuerpo estable de ~30-40/día para el blog.
categoryinferida cuando el item no la trae:VIDEO(YT),NACIONAL(X de medios MX independientes).
- X se mantiene en medida que no lo bloqueen: RSSHub local (Docker) primero, fallback al
/feed/del sitio del medio (ya implementado enobtener_timeline). El sondeo confirma X funciona hoy (7/7 medios + 12/12 hermanas). - Cron local
scripts/feed_rss.sh: regenera el feed + XML; defineRSS_PUSH_URL(R2/Pages) para publicarlo. - Poda del contorno (segundo paso, NO tocado aquí): asgi.py web app, crons Vercel, Supabase artefactos, newsletter vía Resend — evaluar en sesión aparte si mueren o quedan como producto (radio/salto/boletín).
Archivos tocados (commit propuesto: “feed: alt-rss RSS 2.0”)
feed.py:_render_rss/_items_alt_rss/_link_rss/_categoria_rss/_fecha_parse; emisión engenerar()ygenerar_tech().scripts/feed_rss.sh: nuevo, cron local.
Demo (mismo día, agregada)
Prueba de que la home solo consume el artefacto RSS, con el scraper por detrás:
GET /demo— home con el esqueleto existente (_pagina+_nav, “Demo” en nav) que no junta ni scrapea: leereporte/feed.rss.xmly pinta las filas del feed; JS polling cada 25 s a/demo/feed.rss.xmlpara refrescarse conformescripts/feed_rss.shregenere por detrás.GET /demo/feed.rss.xml— artefacto crudo (application/rss+xml,no-store), lo mismo que consumirá el blog._rss_rows/demo_alt_rss/demo_rss_xmlenasgi.py.
Consolidación (mismo día, después)
“Ya solo existe una página.” / ahora es el feed único, IRC-design, texto plano, que consume todo lo extraído:
feed.py generar_todo(--todo) junta y dedupa por URL: feed (292) + feed-tech (819) + archivo diseño/ML (277) + referencias (231) + silo X (88) + papers CN (15) + notas de las 17 ediciones (144) = 1575 items, más reciente arriba, sin fechas al fondo. Emitereporte/feed-todo.json+reporte/feed-todo.rss.xml(full, sin cuotas).asgi.py /— la única página:<surtido>meta + una línea por item (≤140c, tipo tuit), click =/salto, carga progresiva (80 server + 120/montón con IntersectionObserver), refresco 45 s si cambialastBuildDate.GET /feed.rss.xml— raw consolidado (710 KB, full history);/demo*y/hn→ 301 a/.scripts/feed_rss.shregenera corte fresco + consolidado._fecha_parse/_ordenarnormalizan a UTC (mezcla naive/aware cross-fuentes).
Tamaño del corte servido por el demo: 40 items (recencia 3 d, cuota 5/medio), categorías VIDEO/NACIONAL/INTERNACIONAL/TECH.
Acordeón: leer la nota (mismo día, después)
“Ya tenemos el silo, ahora quiero leerlas, despleguemos acordeón.” La página pasa de solo-texto a lista-acordeón: click en una línea la expande y lee la noticia inline, sin salir de la página.
leer.py lectura_json(url)— JSON de lectura rápida cacheado. Prioridad: previa ya traducida (leer_cache/{h}.json, texto ES) → extracción original cacheada ({h}.lectura.json) → fetch+trafilatura en vivo (cacheada). Devuelve titulo, resumen extractivo (sin LLM), parrafos (formato lectura), es (¿texto en español?) con heurística_parece_es(marcadores EN).GET /leer/lectura?u=— endpoint que consume el acordeón (cacheno-store).- Página
/: cada item =div.fila(▸, línea tuitera) +div.paneloculto. Click = expandir y pedir/leer/lectura; panel con título, ”· resumen ·”, párrafos en serif/lectura rápida (columna ~62ch, fondo ligeramente más claro), footer “abrir origen ↗” (salto) y, sies=False, “versión en español ↗” (/leer). Delegación porclosest('.fila'), accesible por teclado. _origen_permitidosuavizado: permite si al menos una dirección DNS resuelve a IP pública (registros mixtos/CDN daban 400 falsos); bloquear solo si todas son reservadas.- Datos reales: extracción live de aje.news OK (live blog, EN → pill español).
Leer en IRC (mismo día, después)
“la de lectura, que tenga un diseño que vaya con la principal: irc-style, muy adhoc con solo leer (un solo bloque), descarga persiste y el mensaje de ‘apoya’”
leer.py render_html reescrito: se quitó el estilo Wikiwand (serif claro,
barra sticky, tabs Resumen/Cifras/Artículo) y las capas de cifras/nombres.
Ahora /leer (versión traducida) es:
- Fondo negro, mono,
<surtido> leer · versión traducida · un solo bloque[ORIGINAL EN → ES] · N párrafos · M min+ fuente (link salto).
- Un solo bloque de lectura (borde izquierdo + fondo
#0d0d0d): resumen como entradilla (· resumen ·) + artículo con subtítulos en ámbar. - Barra de progreso fina (acento
#5ee0a0). - Mensaje apoya persistido: “
<surtido>no olvides apoyar al medio — Surtidito es un puente, no el destino… (abrir original →)“. - Descarga persistida:
↓ descargar versión traducida (.md)+ salida anónima + original. _con_analiticasigue inyectando Matomo al final.
Limpieza — solo feed + leer + scraper (mismo día)
“limpia y deja lo esencial” → elegido: solo feed+leer+scraper, se elimina radio, cursos, lecturas/fuentes (páginas), y el boletín por email.
asgi.pyreescrito: 1517 → 539 líneas. Quedan solo:/(feed acordeón),/feed.rss.xml,/leer,/leer/lectura,/leer/descargar,/salto,/robots.txt,/llms.txt,/health,/demo*/hn*→ 301 a/. Caído:/fuentes,/lecturas,/cursos/*,/radio*,/feedlegacy,/subscribe,/api/proponer,/aprobar/*,/webhook/resend,/api/crony todo su helper (nav, CSS viejo, resend, turnstile, ediciones, mailing). Errores de /salvo y /leer ahora en IRC (_irc_error).- Borrados:
cursos.py,fuentes.py,miniaturas.py,scripts/radio_parrilla.py,scripts/tanda_radio.sh,scripts/radio_cron.sh,scripts/radio_push.sh,scripts/pronunciacion.py. - Cron: eliminada la línea
tanda_radio.sh; quedaprensa_noche.py(03:00), la fuente del feed. - El núcleo vivo se conserva:
prensa_noche.py(nocturno) +local_news.sh(lecturas→archivo→referencias) +feed_rss.sh→ feed-todo.rss.xml. - Motor del boletín (supabase/mailing/proponer/nlp/pulso/reporte/sentimiento/
traducciones) se queda: son infra compartida con
/leer(traducción LLM vía proponer) — sacarlos requiere cirugía en lazy-imports. Queda para otra sesión si se quiere purgar.
Versión alternativa — consola ámbar (AmberConsole) (mismo día)
- Pivote: el usuario citó https://github.com/ggerganov/hnterm (ImTui C++ que lee la HN API) y luego https://github.com/DutchDiederik/AmberConsole. Decisión final: no portar C++; retomar estructura/teclado de hnterm y pintarla con AmberConsole (CSS sólo, 0 deps, BSD-3). Back sigue siendo nuestro silo (we drink our own koolaid).
- AmberConsole venderedo local:
static/amber-console.css+fonts/(VT323 + Silkscreen, OFL-1.1). Montado enapp.mount("/static", StaticFiles(...)). /ahora es<html data-ac-tech="crt" data-ac-emitter="p3">= fósforo ámbar P3 dentro de.ac-screen ac-crt ac-afterglow(scanlines, retrace, persistencia).- Comportamiento hnterm conservado: j/k/↑/↓ mover · Enter leer · o origen · Tab cicla modo (categoría del feed) · g/G · esc/q cierra · r refresh · s status.
- Lector:
Enter→/leer/lectura?u=→ noticia completa, SIN resumen; botón▶ ABRIR EN EL MEDIO ORIGINAL(.ac-btn--filled ac-btn--lg) es lo más visible, siempre vía/salto(noreferrer); link “versión en español” sutil cuandoes=false. - Modos (commit
65a1be3): ALL → MX → TECH → INTL → OTROS (tabs pequeñas, 12px). La clave es agrupar por categoría: cuidado con que “INTERNACIONAL” contiene “NACIONAL” → usar\bNACIONAL\b(word boundary). - Lector = overlay modal fijo (fuera de
.ac-screen, z-index 60, box centradomin(780px,96vw), sans-serif system para el cuerpo,max-width:66ch, alineación centro en caja). Navegación dentro de la lectura:n/p(o j/k y flechas) cambian de noticia sin cerrar;esc/qcierra. Botones ‹ anterior / siguiente › en el pie junto al botón de origen. - Reseña orgánica (rss-ish): cuando la extracción es pobre (
parrafos<700 chars) se muestra primeroresumencomo· RESEÑA ORGÁNICA ·+ el texto extraído que haya; si no hay nada, aviso + botón al original. El<description>del feed solo guarda el medio (no resumen) → no hay summary en el RSS silo. - Commits:
65a1be3(modos + lector modal),e3d1715(front ámbar),da16e84(feed consolidado + lector),7a8f13f(limpieza).reporte/leer_cache/está en .gitignore (caché). - Mistake que vale recordar: Edit gigante de ~250 líneas corrompió el JSON de la
herramienta → la lección es reescribir el archivo completo con Write + Read
previo, y verificar JS embebido con
node --checkantes de reiniciar.
Re-evaluar cuando:
- X deje de responder (bloqueos) → el fallback a sitio ya está; re-considerar quitar X del cuerpo.
- El blog consuma Y entrada: ajustar
limite/por_medio/max_diasde_items_alt_rss.