Files
Daniel Avila dae5faa009 migrate: Vercel → Cloudflare Pages + Workers (/mes → ~/bin/zsh-5/mes) (#571)
* migrate: Vercel → Cloudflare Pages + Workers para reducir costos ($110 → ~$0-5/mes)

- Cambia adaptador Astro de @astrojs/vercel a @astrojs/cloudflare (mode: 'directory')
- Agrega dashboard/wrangler.toml con nodejs_compat para compatibilidad total
- Crea cloudflare-workers/crons/ Worker que reemplaza los cron jobs de Vercel
  (claude-code-check cada 30min, health-check cada 1h en vez de cada 15min)
- Actualiza CI/CD: Vercel CLI → wrangler pages deploy (requiere CLOUDFLARE_API_TOKEN y CLOUDFLARE_ACCOUNT_ID en GitHub Secrets)
- Elimina crons duplicados del proyecto legacy aitmpl (root vercel.json)
- Quick win: agrega Cache-Control headers a components.json (86400s), plugins.json,
  trending-data.json y claude-jobs.json para cortar el costo de bandwidth de $53

Próximos pasos manuales (ver PR description):
1. Crear proyecto en Cloudflare Pages
2. Agregar secrets de Cloudflare en GitHub
3. Migrar env vars de Vercel a Cloudflare Dashboard
4. Deployar cloudflare-workers/crons con wrangler
5. Apuntar dominios a Cloudflare Pages

* fix: nodejs_compat_v2 + react-dom/server.node alias para Cloudflare Workers

- Cambia compatibility_flags de nodejs_compat a nodejs_compat_v2
- Agrega alias vite react-dom/server → react-dom/server.node para evitar
  MessageChannel error en module init time en Cloudflare Workers runtime
- Agrega ssr.noExternal para bundlear react-dom y aplicar el alias

* fix: revert compatibility_flags a nodejs_compat (estándar)

nodejs_compat_v2 no es el flag primario. El fix real del MessageChannel
error fue el alias vite react-dom/server → react-dom/server.node,
no el cambio de flag.

* fix: inject Cloudflare Pages runtime env into process.env via middleware

Add Astro middleware that copies context.locals.runtime.env (Cloudflare
Pages secrets) into process.env before each request, so API routes can
use process.env.X as a unified pattern without changing the call sites.

Also add || process.env.X fallbacks to all API routes/libs that were
using import.meta.env exclusively (build-time only in Vite).

Verified on staging: track-download-supabase, health-check,
claude-code-check, collections, github/token all return correct responses.
2026-05-08 20:24:28 -04:00

51 lines
1.7 KiB
JavaScript

/**
* aitmpl-crons — Cloudflare Worker
*
* Replaces Vercel cron jobs by calling the dashboard API endpoints
* on a schedule. Cron: */30 * * * * (claude-code-check) and 0 * * * * (health-check).
*
* Secrets (wrangler secret put):
* DASHBOARD_URL — e.g. https://www.aitmpl.com
* TRIGGER_SECRET — shared secret sent as Authorization header
*/
export default {
async scheduled(event, env, ctx) {
const base = env.DASHBOARD_URL || 'https://www.aitmpl.com';
const headers = {
'Authorization': `Bearer ${env.TRIGGER_SECRET || ''}`,
'User-Agent': 'aitmpl-crons/1.0',
};
const cron = event.cron;
if (cron === '*/30 * * * *') {
// Every 30 minutes: check for new Claude Code releases
const res = await fetch(`${base}/api/claude-code-check`, { headers });
console.log(`claude-code-check: ${res.status}`);
} else if (cron === '0 * * * *') {
// Every hour: health check (was every 15 min on Vercel)
const res = await fetch(`${base}/api/health-check`, { headers });
console.log(`health-check: ${res.status}`);
}
},
// Manual trigger for testing: GET /trigger?cron=*/30+*+*+*+*
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname !== '/trigger') {
return new Response('aitmpl-crons worker', { status: 200 });
}
const auth = request.headers.get('Authorization');
if (!env.TRIGGER_SECRET || auth !== `Bearer ${env.TRIGGER_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
const cron = url.searchParams.get('cron') || '*/30 * * * *';
await this.scheduled({ cron }, env, {});
return new Response(`Triggered: ${cron}`, { status: 200 });
},
};