2026-09-19 · cf-crawl: Browser Run desde Workers (scrape + crawl)

Repo: ~/nef/cf-crawl/cf-crawl-project · Worker: https://cf-crawl-project.nef.workers.dev

Qué se construyó

Worker TS con binding browser que expone endpoints sin API token:

  • /scrape?url=&selectors=h1,h2 — extracción por CSS selectors
  • /links?url=&sameDomain=true — descubrimiento de URLs
  • /markdown?url= — página completa a markdown
  • /read?url= — modo lector: HTML limpio, sin scripts/rastreo, paga soft-paywalls
  • /crawl?url=&maxPages=&pattern=&include=markdown — BFS: links() + markdown() (el /crawl nativo de Browser Run es solo REST API, no existe como quickAction)

Trapis verificados (no repetir debugging)

  1. env.BROWSER.quickAction() devuelve un Response, no JSON ya parseado. Hacer await res.json() y revisar {success, result} / {success:false, errors}. stringify directo de un Response da {} (falso positivo que ya mordimos).
  2. scrape pide elements: [{selector}] (array), no selector suelto.
  3. Requiere compatibility_date >= 2026-03-24; en dev local solo funciona con remote.
  4. Rate limit del plan Free agresivo: 3s de pacing entre páginas + backoff de 12s.
  5. El token OAuth viejo (env CLOUDFLARE_API_TOKEN) no tenía permisos → unset CLOUDFLARE_API_TOKEN && npx wrangler login (OAuth incluye browser:write).

Sitios de noticias probados

  • bbc.com/mundo: funciona bien. Crawl de artículos con ?pattern=/mundo/articles/ da cuerpos limpios; /read renderiza 1600+ palabras, 0 scripts.
  • jornada.com.mx: home scrapeable (links), pero /ultimasnoticias bloquea (Cloudflare bot protection contra Browser Run).
  • elpais.com: SÍ pasa Browser Run. /read de un artículo sacó ~1260 palabras. El /links de la sección devuelve 0 (render lazy), pero /read de una URL de artículo concreta funciona.

Nota honesta sobre “sin paywall”

Browser Run SIEMPRE se identifica como bot (no configurable). /read limpia el DOM: quita <script>, <iframe>, <form>, <nav>, <footer>, frontmatter YAML, y inyecta CSS que oculta overlays .paywall/.modal/.overlay/.subscribe y fuerza max-height:none → recupera paywalls blandos (texto ya presente en DOM, tapado). NO rompe paywalls duros (donde el servidor no manda el texto). Es un lector limpio, no un burlador. Para EL PAÍS de pago real, no hay cuerpo que rascar.

Uso del puente harvest_cf.py (news-silo)

.venv/bin/python harvest_cf.py https://www.bbc.com/mundo --pages 5 \
    --pattern '/mundo/articles/' --out data/harvest-bbc.json
.venv/bin/python silo.py -i data/harvest-bbc.json -o data/silo-bbc.json

El crawl solo abre páginas que casen con —pattern y excluye la home. Output ya es {records,texts} y silo.py consume data[“texts”].

Endpoints nuevos en el worker

  • /read?url= → HTML de lectura limpia (modo lector) sin rastreo. Botón 📖 en index.html.
  • /crawl?...&include=markdown&pattern=REGEX → cuerpos completos para el silo.

Siguiente paso natural

Sembrar harvest_cf.py con varios medios que toleran Browser Run (BBC, EL PAÍS, Milenio) → silo_medios.py. Medir cuota: el plan Free da ~10 min browser/mes, cada crawl de 5 páginas consume buen trecho. Ver dashboard Workers → Browser Run.


2026-09-19 (cierre) · LLEGÓ EL CORREO DE CUOTA → PIVOTE A /rss GRATUITO

Llegó aviso de Cloudflare por consumo de Browser Run. Decisión: no seguir gastando cuota de browser. Se apaga Browser Run por defecto y se construye la vía gratuita.

Qué cambió en el worker

  • Nuevo /rss?url= — parsea RSS/Atom con fetch normal. 0 min de browser, gratis en el plan Workers Free siempre. Parámetros: ?url=<feed>.
  • Gate BROWSER_ENABLED (var en wrangler.jsonc, default off). Con off, los endpoints de Browser Run devuelven 503 con mensaje claro; /rss sigue vivo.
  • Re-activar cuota cuando toque: wrangler deploy --var BROWSER_ENABLED:on (o editar wrangler.jsonc → "BROWSER_ENABLED":"on").

Puente harvest_cf.py — modo RSS (gratuito)

.venv/bin/python harvest_cf.py --rss bbc-mundo elpais elfinanciero \
    --out data/harvest-rss.json
.venv/bin/python silo.py -i data/harvest-rss.json -o data/silo-rss.json --min-cluster-size 3

Feeds verificados que SÍ pasan por /rss desde el edge de CF:

  • bbc-mundo → feeds.bbci.co.uk/mundo/rss.xml (34)
  • elpais → feeds.elpais.com/mrss-s/pages/ep/site/elpais.com/portada (60)
  • elfinanciero → elfinanciero.com.mx/rss (60) 154 notas, 9 silos, coste browser = 0. Resultado en data/silo-rss.json. No pasan /rss (bloquean el fetch incluso desde CF): jornada (403), milenio (403), aristegui (530/defunto), eluniversal (404). Para esos, más adelante: Workers AI embeddings gratuitos o un feed proxy, no Browser Run.

Regla para no repetir el susto

Browser Run = cuota escasa. Usarlo SOLO para /read puntual o cosechar 1 artículo sin RSS. Para escalar el silo, siempre /rss + Workers AI. La clave del silo no es el scraping, es el patrón de feed estable.

No estaba configurado en ~/.config/opencode/opencode.jsonc (ni en ningún opencode.json de ~/nef). Nada que desactivar. La búsqueda la cubren los MCP de Cloudflare ya activos (documentation + observability). Si algún día añades brave, pon enabled:false ahí.