Web para seguir desde el móvil cualquier torneo de badminton.es: tu próximo partido, qué se juega en cada pista, horario, cuadros y liguillas, clubes, medallero y estadísticas. Los datos llegan con unos 2 minutos de retraso como mucho respecto a badminton.es.
La web no tiene datos propios: lee en cada momento los de badminton.es, los reorganiza pensando en el jugador que está en el pabellón con el móvil, y calcula lo que badminton.es no enseña (clasificaciones de grupo, medallas, estadísticas, horas estimadas). Es una web no oficial: ante cualquier duda, manda la organización.
Idea original: ESC26.
móvil (navegador) ──► Cloudflare Worker ──► badminton.es
public/ src/index.js (Tournament Software)
pinta la web y sirve la web y
lee el HTML guarda copias unos
segundos
| Pieza | Dónde | Qué hace |
|---|---|---|
| La web | public/ |
Lo que se ve. Pide las páginas de badminton.es a través del Worker, las convierte en datos y dibuja las pantallas. HTML, CSS y JavaScript sin dependencias. |
| El Worker | src/index.js y wrangler.toml |
Un programa pequeño en Cloudflare. Entrega los archivos de la web y hace de intermediario con badminton.es. |
| La publicación | .github/workflows/deploy.yml |
Cada cambio que se sube a main pasa los tests y se publica solo en Cloudflare. |
Por qué hace falta el Worker: un navegador no puede leer páginas de otra web (badminton.es) desde la nuestra; el navegador lo bloquea. El Worker está en el mismo dominio que la web, así que la web le pide los datos a él y él los pide a badminton.es.
Por qué la web procesa el HTML y no el Worker: el plan gratuito de Cloudflare deja muy poco tiempo de cálculo por petición. Así el Worker solo reenvía páginas y el móvil hace el trabajo.
src/index.js)Todo lo que no empieza por /api/ son los archivos de la web, que se sirven tal cual (la versión compactada de public/ que prepara scripts/build.mjs en dist/). Las rutas /api/, que solo responden a la propia web (ver «Protección del código»), son estas:
| Ruta | Qué pide a badminton.es | Se guarda |
|---|---|---|
/api/config |
Nada: devuelve la lista de torneos de wrangler.toml |
— |
/api/tournament/<id> |
Página del torneo: nombre, días, lugar y nota del organizador | 10 min |
/api/clubs/<id> |
Lista de clubes | 10 min |
/api/draws/<id> |
Lista de cuadros | 5 min |
/api/league/<liga> |
Liga: nombre, temporada y todas las categorías | 1 día |
/api/standings/<liga>/<cuadro> |
Liga: clasificación de un grupo | 5 min en jornada, 1 día si no |
/api/fixtures/<liga>/<cuadro> |
Liga: todas las eliminatorias de un grupo o de un play-off | 5 min en jornada, 1 día si no |
/api/tie/<liga>/<eliminatoria> |
Liga: los partidos de una eliminatoria entre dos clubes | 5 min si es de hoy o de ayer, 1 día si no |
/api/clubplayers/<id>/<club> |
Jugadores de un club en el torneo; antes de los cuadros es lo que dice de qué club es cada inscrito | 10 min |
/api/day/<id>/<AAAAMMDD> |
Todos los partidos de ese día | 30 s (1 h si el día ya pasó) |
/api/draw/<id>/<n> |
Un cuadro de eliminación | 45 s |
/api/players/<id> |
Inscritos, reducidos a una lista [id, nombre, apellidos] |
10 min |
/api/player/<id>/<n> |
Perfil, licencia y nombre completo de un jugador; para eso entra también en su ranking | 1 día |
/api/h2h/<id>/<socios> |
Historial entre los dos lados de un partido, de todos los torneos (la página «H2H» de badminton.es). <socios> son los números de socio del partido, T1P1-T1P2-T2P1-T2P2 (0 si no hay segundo jugador), que salen de su enlace H2H |
6 h |
/api/career/<id>/<perfil> |
La «Visión general» del perfil de un jugador en badminton.es: partidos jugados, ganados y perdidos de toda su carrera y del año, en individual, dobles y mixto. <perfil> es el id de perfil que se guarda con «Soy yo» |
1 h |
/api/live/<id> |
Solo en los torneos con botón «LiveScore»: el tanteo punto a punto de cada pista. No sale de badminton.es sino del servidor de LiveScore de Tournament Software (src/livescore.js); se entrega ya como JSON pequeño. La web lo pega a cada partido en juego |
10 s |
/api/batch?p=<ruta>,<ruta>,... |
Varias de las anteriores (hasta 12, sin la ficha de jugador) en una sola petición; cada ruta sin /api/, por ejemplo day/<id>/20261004 |
lo de cada una |
Reglas del Worker:
wrangler.toml y solo esas páginas. Cualquier otra cosa devuelve 404.X-Data-Stale. Si no tiene ninguna, responde 502 (“badminton.es no responde”).X-Data-Time: cuándo se leyó de verdad de badminton.es. Con ella la web enseña “Datos hace 40 s”./api/batch junta páginas porque cada petición al Worker cuenta para el límite diario (ver Límites). Responde con una línea JSON que dice, de cada página, si ha llegado, cuándo se leyó y cuántos caracteres ocupa, y detrás los contenidos seguidos. Así el Worker no tiene que meter el HTML dentro de un JSON, que le costaría tiempo de cálculo.public/)index.html es el esqueleto: cabecera, contenido y barra de pestañas. css/app.css tiene los estilos, con los colores como variables al principio y modo oscuro automático. La lógica está en cuatro archivos de js/:
parse.js: del HTML de badminton.es a datosLee las páginas buscando cada pieza por su clase CSS, igual que la vería una persona:
CyL-91 es CyL).data.js: carga, refresco y modeloLa web revisa cada 30 s si toca pedir algo, y solo mientras está en pantalla. Todo lo que toca se pide junto, en una sola petición a /api/batch.
logic.js: los cálculosClasificaciones de liguilla, medallas, números de cada jugador y estadísticas del torneo. Las reglas están en Reglas que calcula la web.
app.js: las pantallasElige la pantalla según lo que va detrás de # en la dirección: #inicio, #horario, #cuadros, #clubes, #datos, #perfil, #tv (pantalla para el pabellón), y las fichas #j=<jugador>, #c=<club> y #d=<cuadro>. Cada vez que cambia algo vuelve a pintar la pantalla entera, conservando dónde estabas en la página, el control que tenía el foco y lo que estuvieras escribiendo en el buscador.
liga.js)La Liga Nacional de Clubes tiene su propio apartado, ?liga, al que se llega desde la tarjeta «Liga Nacional de Clubes 2026-2027» de arriba del selector. Ahí salen todas las categorías de la liga, leídas de su página de badminton.es (/api/league/<liga>, una vez al día). Por ahora solo se puede abrir la División de Honor (?liga=division-de-honor); las demás (Primera Oro, Plata y Bronce, Segunda Oro) empiezan en enero de 2027 y salen como «Próximamente» hasta que se activen.
#e=<id>): los 7 partidos (DX, DF, DM, IF1, IF2, IM1, IM2), con jugadores, juegos y retiradas.#q=<id>): su puesto, sus números y todas sus eliminatorias.Peticiones: cada vez se pide junto, en una sola petición al Worker, la clasificación y las eliminatorias de cada cuadro (6 páginas para la División de Honor). El Worker guarda cada página para todos los que miran: 5 min si trae alguna eliminatoria de hoy o de ayer, y 1 día si no. La web las vuelve a pedir cada 5 min el fin de semana en que se juega (hoy hay eliminatoria, o ayer y falta algún resultado) y una vez al día el resto de días. La categoría (qué grupos y play-off tiene) se mira una vez al día. Los partidos de una eliminatoria solo se piden al abrirla: cada 5 min si es de hoy o de ayer, una vez al día si aún queda lejos; una vez jugada, se guardan en el móvil y no se vuelven a pedir. Así, aunque la mire mucha gente, a badminton.es le llegan como mucho unas 70 peticiones por hora en jornada y, el resto de días, una por página y día. (El Worker guarda las páginas en memoria y Cloudflare la vacía de vez en cuando, así que alguna página se puede pedir algo más de una vez al día.)
Tutorial (tour.js): la primera vez que se entra en la web, en la página principal (la de todos los torneos) o directamente en un torneo, sale encima un carrusel de 10 tarjetas, cada una con un esquema de la web hecho con HTML y CSS: qué es la web, de dónde salen los datos (de badminton.es: si la organización no los actualiza allí, aquí tampoco cambian, y la web no se hace responsable), «Soy yo», la tarjeta de Inicio que cambia de color cuando te toca, corazón y estrella, «Los míos» en Horario, cuadros, clubes y datos, la ficha de jugador, torneos y liga, y cómo tenerla como una app. Esta última explica cómo añadirla a la pantalla de inicio en Android (Chrome), iPhone con Safari e iPhone con Chrome, con el navegador del móvil elegido de entrada; en Chrome de Android, si el navegador lo permite, tiene además un botón «Instalar ahora». Si la web ya está abierta como app, esa tarjeta no sale. Se pasa deslizando, con «Siguiente» o con las flechas; «Saltar» o Escape lo cierran. Sale una sola vez por móvil: se apunta como visto en cuanto aparece. No sale en la liga ni en la pantalla del pabellón, y se puede volver a ver desde la página principal o desde Mi perfil. Para volver a enseñárselo a todos (por ejemplo, con una función nueva importante), se sube TOUR_V.
Inicio
Al pasar a roja o naranja hace una pequeña animación y avisa a los lectores de pantalla.
Tres marcas de jugador, cada una con su color en los partidos (borde izquierdo de la tarjeta, el nombre resaltado, el cuadro y la fila de la clasificación):
Un jugador es seguido o tutelado, no las dos cosas: al marcar el corazón se quita la estrella y al revés. Si un partido tiene varios jugadores marcados, manda el más importante (tú, luego tutelado, luego seguido). “Finalizado” y los sets ganados van en gris para no confundirse con el verde de tus partidos.
Horario
#tv en la dirección): para que la organización la ponga en una tele o un proyector.
Cuadros: agrupados por categoría y modalidad. Cada cuadro lleva el color de quien juega en él: verde si juegas tú, morado si juega un tutelado y naranja si juega un seguido (manda el más importante). Antes de que salgan los cuadros, igual con las pruebas de inscritos.
Clubes: tabla con jugadores, ganados–perdidos y medallas. La ficha de cada club tiene sus jugadores, sus próximos partidos y sus resultados.
Datos: cifras del torneo, líderes, récords, medallero por clubes, clubes por victorias y resumen por categoría, con filtro por categoría. En cada lista de líderes salen los 5 primeros; si has dicho «Soy yo» y no estás entre ellos, debajo sale tu puesto (de cuántos), y si estás, tu fila va marcada con «Tú».
Ficha de jugador: club, pruebas, medallas, números, botones a sus cuadros, todos sus partidos, y los botones Seguir, Soy yo, Compartir y Añadir al calendario.
Contra su próximo rival (en la ficha, y una línea en tu tarjeta de Inicio): el balance y los últimos partidos entre los dos, en cualquier torneo, sacados del historial «H2H» de badminton.es.
Añadir al calendario genera en el propio móvil un archivo .ics (js/ics.js) con los partidos que le quedan al jugador:
Mi perfil: tu nombre, tu licencia, el enlace a tu perfil de badminton.es, “No soy yo” y los torneos de esta web en los que juegas. Dentro de un torneo es la última pestaña; en la página principal y en la liga, que no tienen barra abajo, se abre con el botón redondo de arriba a la derecha (tus iniciales, o un muñeco si aún no has dicho quién eres), que lleva a ?perfil: el mismo perfil, sin las cosas de un torneo concreto. Si aún no has dicho quién eres, Mi perfil tiene un buscador con los inscritos de todos los torneos de la web (se piden la primera vez que se abre, y luego se guardan en el móvil como los de «Tus torneos»): sale cada nombre con en cuántos torneos aparece, y «Soy yo» guarda tu perfil a partir del más reciente.
Selector de torneos: sale al abrir la web sin ?t=. Primero los torneos en los que juegas (si tienes perfil), en un desplegable que sale plegado con el siguiente a la vista (el móvil recuerda si lo dejaste abierto), luego todos los torneos en un solo calendario por meses, cada mes en un desplegable. Arriba, un desplegable discreto «Torneos pasados» con los meses anteriores (el más reciente primero); después el mes en curso, el único abierto al entrar, que lleva arriba otro «Torneos pasados» con los ya terminados de ese mes; y luego los meses que vienen. Lo que abras o cierres no se recuerda entre visitas. Dentro de cada mes, por semanas de lunes a domingo con su número (la semana ISO, la que se usa en el calendario de bádminton) y sus fechas; la semana en curso lleva «Esta semana». Cada semana va entera al mes en que cae su sábado, para que un fin de semana entre dos meses no salga partido.
Así, aunque la organización asigne pista a todo el horario de antemano, solo cuenta el que toca. Si un torneo no asigna pistas en directo, esta parte se queda vacía.
~ y marcada como “estimada”, porque es una estimación nuestra, no un horario oficial:
Con todo igualado (por ejemplo, antes de empezar), van primero los cabezas de serie. El reglamento oficial puede desempatar de otra forma en casos raros; la web lo avisa en la tabla.
Todo va en el localStorage del navegador, con el prefijo tb::
tb:career:<perfil>), que se vuelve a pedir pasada 1 hora;tb:tour, con su número de versión).Además, sw.js (un service worker) guarda en la caché del navegador los archivos de la web (HTML, CSS, JS, fuentes e iconos), para que abra aunque no haya cobertura. Pide primero la versión nueva y, si la red no responde en 3 s, usa la guardada; así cada cambio publicado llega solo. Los datos de /api/ no pasan por él.
No hay cookies, cuentas ni base de datos de usuarios, y nada de esto sale del móvil. Para reconocerte solo se piden a badminton.es páginas públicas de jugadores.
CLOUDFLARE_API_TOKEN con el token;CLOUDFLARE_ACCOUNT_ID con el id.https://torneo-bdmnt.javiergar1315.workers.dev.A partir de ahí, cada cambio que se suba a main se publica solo en uno o dos minutos. Esta web no funciona en GitHub Pages: Pages solo sirve archivos y no puede ejecutar el Worker, que es de donde salen los datos.
El formulario para pedir un torneo (en Novedades y al final de la lista de torneos) lo recibe el Worker (src/solicitud.js) y avisa por Telegram y por correo. Necesita cuatro secretos en Cloudflare: en el panel, Workers & Pages → torneo-bdmnt → Settings → Variables and Secrets → Add, tipo Secret (se mantienen entre publicaciones y no van al repositorio):
TELEGRAM_TOKEN: el token del bot. En Telegram, escribe a @BotFather, manda /newbot y sigue los pasos.TELEGRAM_CHAT: tu número de chat. Escribe cualquier cosa a tu bot y abre https://api.telegram.org/bot<TOKEN>/getUpdates: es el "id" que sale dentro de "chat".RESEND_KEY: una clave de resend.com (cuenta gratis, API Keys → Create API Key).NOTIFY_EMAIL: el correo con el que te diste de alta en Resend (sin dominio propio, Resend solo puede escribir a ese).Con uno de los dos canales basta; si no hay ninguno, quien envía el formulario ve que no se ha podido enviar. En local (dev/server.py) no se avisa a nadie: la solicitud sale en la consola.
La web vive también en https://badmintonburgos.es/torneos. La web del club (repositorio badmintonburgos/badminton-burgos, en Vercel) pasa por detrás todo lo que llega a /torneos a este Worker. El visitante solo ve la dirección del club, y las visitas cuentan como visitas a su web.
badmintonburgos.es/torneos... → Vercel (web del club) → Worker (torneo-bdmnt...workers.dev) → badminton.es
En la web del club hay dos cambios:
vercel.json, dos reglas de reenvío (/torneos y /torneos/:path*) delante de la que manda todo a index.html;middleware.js, /torneos queda fuera del middleware de SEO, que si no devolvería la web de React con un 404.Lo que hace esta web para vivir ahí:
trailingSlash: false), así que se abre en /torneos, sin barra. Para que sus enlaces relativos (css/, js/, api/…) apunten dentro de la carpeta, el Worker pone <base href="/torneos/"> en las páginas que llegan a través del club. Si no lo pusiera, una línea al principio de cada página la calcula./torneos/sw.js pero cubre /torneos. El Worker lo permite con la cabecera Service-Worker-Allowed.api/ junto a la web, nunca a /api/ desde la raíz./torneos. Todo lo que usa esta web está permitido en ella.Redirección desde workers.dev. En wrangler.toml, PUBLIC_URL = "https://badmintonburgos.es/torneos" y REDIRECT = "si": quien abra una página en la dirección de workers.dev pasa a la del club, con el mismo torneo, jugador o pantalla (?t=... y #...).
REDIRECT = "si") solo cuando https://badmintonburgos.es/torneos/api/ping diga "forwardedHost": "badmintonburgos.es" o "vercel": true. Así sabe el Worker que la visita viene del club; si no, daría vueltas sin fin./api/ sigue respondiendo en workers.dev, para que quien tenga abierta la web antigua no se quede sin datos.sw.js pasan antes por el Worker (run_worker_first). El CSS, el JS y las imágenes se sirven directamente y no cuentan para el límite de peticiones.Qué cambia para quien ya la usa: lo guardado en el móvil (“Soy yo”, favoritos, tutelados) va ligado a la dirección, así que en la nueva hay que marcarlo otra vez. Y quien la tenga en la pantalla de inicio debe volver a añadirla desde la nueva dirección.
La barra de arriba del club. Lo que llega a través del club lleva además, arriba del todo, la barra de la web del club (logo, menú y Zona privada), para que /torneos se vea como una página más de su web. La pone el Worker (src/clubbar.js, estilos .cb en app.css) y es una copia de src/components/Navbar.jsx del repositorio del club: si cambian los enlaces del menú del club, hay que cambiarlos también en src/clubbar.js. En la dirección de workers.dev no sale. En el móvil, entre el logo y el menú lleva además un botón «Ir web Bádminton Burgos» (la mayoría no abre el menú); en el ordenador no, porque ya está el menú entero. Para verla en local: http://localhost:8787/?club.
Visitas. A lo que llega a través del club se le añaden también los scripts de Vercel Analytics y Speed Insights (/_vercel/insights/script.js y /_vercel/speed-insights/script.js, sin cookies), así que las visitas a /torneos y lo rápido que carga salen en los paneles de Analytics y Speed Insights de la web del club, en Vercel.
El repositorio es privado y todo el código es «todos los derechos reservados» (LICENSE y la sección «Propiedad intelectual» del aviso legal). Además:
scripts/build.mjs (lo lanza wrangler.toml, en [build]) prepara dist/ a partir de public/. Todo el JavaScript va en un solo js/app.js sin comentarios, sin espacios y con los nombres internos acortados. El CSS y sw.js van compactados y el HTML sin comentarios. Cada archivo lleva delante el aviso de copyright. Se sigue editando public/, que es lo que prueban los tests y lo que sirve dev/server.py. dist/ no se sube al repositorio.src/index.js, que es quien habla con badminton.es, corre en Cloudflare y nunca se manda al navegador./api/ (menos /api/ping) contesta 403 a lo que pidan desde el navegador páginas de otros dominios (Sec-Fetch-Site: cross-site, o un Origin que no es el de la web ni el del club). Sin cabeceras CORS, otras webs tampoco pueden leer las respuestas.Lo que no se puede evitar es que el navegador reciba el código para ejecutarlo. Quien insista puede guardarse la versión compactada, pero no el código original con sus nombres y comentarios.
En wrangler.toml, una línea por torneo con un nombre corto y su identificador:
TOURNAMENTS = """
ttr-astorga = 666EE6B4-96EA-41EE-9AD7-9822D1FFEFD2
sub15-leon = 9A1C22F0-...
"""
id= del enlace del torneo, badminton.es/sport/tournament?id=666EE6B4-.... Todo lo demás (nombre, fechas, lugar, categorías, cuadros, clubes) se lee de badminton.es.https://torneo-bdmnt.javiergar1315.workers.dev/?t=sub15-leon. El botón “Compartir torneo” de Inicio lo genera solo.?t= sale el selector de torneos; si solo hay uno, se abre directamente.?t=666EE6B4-...) siguen funcionando.Al subir el cambio a main, el torneo nuevo funciona en uno o dos minutos.
Las categorías de la liga van en LEAGUES, una por línea: nombre-corto = <id de la liga>/<número de la categoría>, con el id= y el event= del enlace de la categoría en badminton.es/sport/events.aspx?id=<id de la liga>.
pip install requests
python dev/server.py
# abre http://localhost:8787 (y http://localhost:8787/?t=<nombre-corto> para cada torneo)
dev/server.py hace lo mismo que el Worker, con una caché de 60 s en dev/cache/. Sin argumentos usa los torneos de wrangler.toml y los relee en cada petición, así que un torneo añadido funciona sin reiniciar. También se le pueden pasar otros: python dev/server.py prueba=<id>.
Para usar el Worker de verdad en local: npm install y npx wrangler dev.
npm install
npm test
Prueban parse.js contra páginas reales de badminton.es guardadas en test/fixtures/: un torneo, sus clubes y cuadros, un día con horario, un día ya jugado (con una retirada), un día en cola por pista, dos cuadros de eliminación y la «Visión general» de un perfil de jugador. Así, si badminton.es cambia su web o un cambio en parse.js rompe algo, se ve aquí y no en el pabellón.
python dev/server.py para que queden en dev/cache/, edita la lista de dev/fixtures.py y ejecuta python dev/fixtures.py.Los tests usan páginas guardadas, así que no ven si badminton.es cambia su web mañana. Para eso, cada día a las 6:00 (hora UTC) la Action Comprobar badminton.es (.github/workflows/check.yml) ejecuta dev/check.js:
parse.js y comprueba que sale lo que debe.Si algo falla, la Action sale en rojo y GitHub manda un correo a quien modificó por última vez la programación del archivo. El resumen de qué ha fallado está en la página de la Action. Se puede lanzar a mano desde Actions → Comprobar badminton.es → Run workflow, o en local con npm run check (o node dev/check.js http://localhost:8787 contra dev/server.py).
GitHub desactiva las Actions programadas si el repositorio pasa 60 días sin cambios; avisa antes por correo, y se reactivan desde la pestaña Actions.
| Archivo | Para qué |
|---|---|
public/index.html |
Esqueleto de la web: cabecera, contenido y pestañas |
public/css/app.css |
Estilos; los colores son variables al principio, con modo oscuro |
public/fonts/ |
Fuentes Inter y Outfit, servidas desde la propia web para no pasar la IP de los visitantes a Google |
public/sw.js |
Guarda los archivos de la web en el móvil para abrir sin cobertura |
public/manifest.webmanifest |
Nombre, icono y modo app al añadir la web a la pantalla de inicio; se abre en el torneo desde el que se añadió |
public/legal.html |
Aviso legal, privacidad y cookies; enlazado desde el pie de todas las pantallas |
public/js/parse.js |
Convierte el HTML de badminton.es en datos |
public/js/data.js |
Carga, refresco, caché del móvil, modelo, “en pista”, horas estimadas y perfil |
public/js/logic.js |
Clasificaciones, medallas y estadísticas |
public/js/ics.js |
Archivo de calendario con los partidos de un jugador |
public/js/app.js |
Pantallas, navegación, botones y refresco automático |
public/js/tour.js |
El tutorial de la primera vez |
public/js/liga.js |
La liga: carga, refresco y pantallas de grupos, play-off, eliminatorias y equipos |
scripts/build.mjs |
Prepara en dist/ la web compactada que se publica |
src/index.js |
El Worker: sirve la web y hace de intermediario con badminton.es |
src/clubbar.js |
La barra de arriba de la web del club, que se añade cuando la web se abre en badmintonburgos.es/torneos |
wrangler.toml |
Configuración del Worker, lista de torneos y categorías de la liga |
.github/workflows/deploy.yml |
Pasa los tests y publica en Cloudflare con cada cambio en main |
.github/workflows/check.yml |
Cada día comprueba que la web sigue sabiendo leer badminton.es |
dev/check.js |
Esa comprobación: lee un torneo real a través de la web publicada |
dev/server.py |
Servidor para probar en local |
dev/fixtures.py |
Crea las páginas anonimizadas de los tests a partir de dev/cache/ |
test/ |
Tests de parse.js y del calendario (npm test), y las páginas de ejemplo |
PRODUCT.md |
Para quién es la web y sus principios |
DESIGN.md |
Sistema de diseño: colores, tipografías y componentes |
package.json |
Para tener wrangler en local y pasar los tests (jsdom) |
parse.js puede dejar de encontrar las piezas. La comprobación diaria lo avisa por correo y la web, en la mayoría de casos, avisa de que no ha podido leer los datos. Se arregla tocando parse.js./api/ sí. Un móvil con la web abierta hace como mucho una petición por minuto, porque todo lo que toca refrescar va junto, y menos cuando ya han terminado los partidos del día:
workers.dev: la Cache API de Cloudflare solo está garantizada con un dominio propio, y Workers Cache haría que los archivos de la web también contasen para el límite diario. Con un dominio propio en Cloudflare, caches.default lo resolvería sin coste.