{"repo": "aisecurity365/syntro", "files": {".github/scripts/seo-autofix.cjs": {"e2d70542fec0c07e200c5850e3fd53b12cb073db": {"t": 1785747020.6861675, "n": 10306, "h": "a1ebf34a094070ca", "c": "#!/usr/bin/env node\n'use strict';\n/**\n * seo-autofix — analiza las páginas .astro modificadas, detecta fallos SEO/GEO,\n * los corrige con IA (DeepSeek) mediante reemplazos DIRIGIDOS (nunca reescribe el\n * archivo entero) y, SOLO si mejora algo, hace commit + push y envía un email.\n * Sin mejoras → ni commit ni email.\n */\nconst fs = require('fs');\nconst { execSync } = require('child_process');\n\nconst DEEPSEEK_KEY = (process.env.DEEPSEEK_API_KEY || '').trim();\nconst RESEND_KEY = (process.env.RESEND_API_KEY || '').trim();\nconst EMAIL_TO = (process.env.EMAIL_TO || 'julen.sistemas@gmail.com').trim();\n\nconst FILES = (process.env.CHANGED_FILES || '')\n .split('\\n').map(s => s.trim()).filter(Boolean)\n .filter(f => f.endsWith('.astro') && !f.includes('/api/') && !f.includes('/_'));\n\nconst stripTags = s => s.replace(/<[^>]+>/g, '').replace(/\\s+/g, ' ').trim();\nconst esc = s => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n// ── Extraer title/description del source (const o prop) ──\nfunction getStr(src, key) {\n let m = src.match(new RegExp('const\\\\s+' + key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `const ${key} = \"${m[1]}\"`, repl: v => `const ${key} = \"${v}\"` };\n m = src.match(new RegExp(key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `${key}=\"${m[1]}\"`, repl: v => `${key}=\"${v}\"` };\n return null;\n}\n\nfunction analyze(src, file) {\n const isEn = file.includes('/en/');\n const t = getStr(src, 'title');\n const d = getStr(src, 'description');\n const issues = [];\n if (t && (t.value.length < 40 || t.value.length > 70)) issues.push('title');\n if (d && (d.value.length < 120 || d.value.length > 165)) issues.push('desc');\n\n // attrs: un heading de artículo va sin atributos (

Texto

). Los que llevan\n // class/style son etiquetas de tarjeta, cabeceras de sección de landing o títulos de\n // vídeo: convertirlos a pregunta destroza la UI y no aporta señal (jul 2026).\n const hs = [...src.matchAll(/]*)>([\\s\\S]*?)<\\/h\\1>/gi)]\n .map(m => ({ inner: m[3], text: stripTags(m[3]), plain: !/[<]/.test(m[3]), bare: !m[2].trim() }))\n .filter(h => h.text);\n // Headings genéricos de cierre/estructura: en pregunta quedan forzados.\n const GENERIC = /^(conclusi[oó]n|recursos adicionales|requisitos previos|introducci[oó]n|resumen|preguntas frecuentes|faq)$/i;\n let badHeadings = [];\n if (!isEn && hs.length > 2) {\n const q = hs.filter(h => h.text.startsWith('¿')).length;\n if (q / hs.length < 0.6) {\n badHeadings = hs\n .filter(h => !h.text.startsWith('¿') && h.plain && h.bare\n && !GENERIC.test(h.text.trim())\n && h.text.trim().split(/\\s+/).length >= 3)\n .slice(0, 3); // máx 3 por página: el objetivo es afinar, no reescribir el índice\n if (badHeadings.length) issues.push('headings');\n }\n }\n return { isEn, t, d, hs, badHeadings, issues };\n}\n\nasync function deepseek(messages) {\n const res = await fetch('https://api.deepseek.com/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_KEY}` },\n body: JSON.stringify({ model: 'deepseek-v4-flash', temperature: 0.4, messages }),\n });\n if (!res.ok) throw new Error('DeepSeek HTTP ' + res.status);\n const d = await res.json();\n let txt = d.choices?.[0]?.message?.content || '';\n txt = txt.replace(/```json|```/g, '').trim();\n const s = txt.indexOf('{'), e = txt.lastIndexOf('}');\n return JSON.parse(txt.slice(s, e + 1));\n}\n\nasync function fixFile(file) {\n if (!fs.existsSync(file)) return null;\n let src = fs.readFileSync(file, 'utf8');\n const a = analyze(src, file);\n if (!a.issues.length) return null;\n\n const lang = a.isEn ? 'English' : 'español';\n const prompt = `Eres un editor SEO senior (idioma: ${lang}). Corrige SOLO lo que se pide. Devuelve EXCLUSIVAMENTE un JSON válido, sin texto extra.\n\nPRIORIDAD (importante): las buenas prácticas **SEO clásicas** mandan (son las que dominan el mercado). Optimiza para clic real en Google (CTR): keyword principal al principio, título claro, atractivo y honesto (sin clickbait). Las buenas prácticas **GEO** (formato pregunta, lenguaje natural para IA) son SECUNDARIAS y **NUNCA** deben empeorar la keyword ni la claridad SEO. Ante duda, gana el SEO.\n\nReglas:\n- \"title\": reescríbelo si mide <40 o >70 caracteres. Keyword principal al inicio + marca \"AI Security\" si ya estaba. 50-65 ideal, orientado a CTR. Si ya está bien, pon null.\n- \"description\": reescríbela si mide <120 o >165 caracteres. 140-160 ideal, con la keyword y un gancho/CTA que suba el CTR. Si está bien, pon null.\n- \"headings\": SOLO español. Convierte a pregunta (¿…?) **solo si sigue siendo natural y conserva la keyword**; si forzar la pregunta perjudica el SEO, NO lo incluyas (deja el titular como está). Devuelve [] si no se piden.\n- No inventes datos. No cambies el idioma.\n\nDatos actuales:\ntitle (${a.t ? a.t.value.length : 0} car.): ${a.t ? a.t.value : '—'}\ndescription (${a.d ? a.d.value.length : 0} car.): ${a.d ? a.d.value : '—'}\nheadings_a_convertir: ${JSON.stringify(a.badHeadings.map(h => h.text))}\n\nFormato de salida:\n{\"title\": null|\"...\", \"description\": null|\"...\", \"headings\": [{\"from\":\"texto original\",\"to\":\"¿pregunta?\"}]}`;\n\n let out;\n try {\n out = await deepseek([{ role: 'user', content: prompt }]);\n } catch (e) { console.log(` ⚠️ DeepSeek falló en ${file}: ${e.message}`); return null; }\n\n const changes = [];\n // Title\n if (a.issues.includes('title') && out.title && typeof out.title === 'string') {\n const nt = out.title.trim();\n if (nt.length >= 40 && nt.length <= 70 && a.t && src.includes(a.t.find)) {\n src = src.replace(a.t.find, a.t.repl(nt.replace(/\"/g, \"'\")));\n changes.push({ k: 'Title', from: a.t.value, to: nt });\n }\n }\n // Description\n if (a.issues.includes('desc') && out.description && typeof out.description === 'string') {\n const nd = out.description.trim();\n if (nd.length >= 120 && nd.length <= 165 && a.d && src.includes(a.d.find)) {\n src = src.replace(a.d.find, a.d.repl(nd.replace(/\"/g, \"'\")));\n changes.push({ k: 'Description', from: a.d.value, to: nd });\n }\n }\n // Headings (solo plano, reemplazo dirigido del texto interno)\n if (Array.isArray(out.headings)) {\n for (const h of out.headings) {\n if (!h || !h.from || !h.to || !h.to.startsWith('¿')) continue;\n const re = new RegExp('(]*>)\\\\s*' + esc(h.from) + '\\\\s*()');\n if (re.test(src)) {\n src = src.replace(re, `$1${h.to}$3`);\n changes.push({ k: 'Heading', from: h.from, to: h.to });\n }\n }\n }\n\n if (!changes.length) return null;\n // Re-validar: que no aparezca ningún problema NUEVO. Antes los cambios de Heading\n // saltaban esta comprobación por completo y se commiteaban aunque no mejorase nada;\n // era la vía por la que entraban las reescrituras masivas de titulares (jul 2026).\n const after = analyze(src, file);\n if (after.issues.some(i => !a.issues.includes(i))) return null;\n\n fs.writeFileSync(file, src, 'utf8');\n return { file, changes, before: a.issues.length, after: after.issues.length };\n}\n\nfunction emailHtml(results) {\n const rows = results.map(r => `\n
\n
${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}
\n ${r.changes.map(c => `\n
\n
${c.k}
\n
${c.from}
\n
${c.to}
\n
`).join('')}\n
`).join('');\n return `
\n

🤖 Auto-mejora SEO/GEO aplicada

\n

El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a main.

\n ${rows}\n

AI Security · SEO/GEO bot

\n
`;\n}\n\nasync function sendEmail(results) {\n if (!RESEND_KEY) { console.log(' ⚠️ RESEND_API_KEY no configurada — sin email'); return; }\n const res = await fetch('https://api.resend.com/emails', {\n method: 'POST',\n headers: { 'Authorization': `Bearer ${RESEND_KEY}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n from: 'AI Security SEO Bot ',\n to: [EMAIL_TO],\n subject: `🤖 SEO/GEO: ${results.length} página(s) mejoradas automáticamente`,\n html: emailHtml(results),\n }),\n });\n console.log(` 📧 Email: HTTP ${res.status}`);\n}\n\nasync function main() {\n if (!FILES.length) { console.log('Sin páginas .astro que analizar.'); return; }\n if (!DEEPSEEK_KEY) { console.log('⚠️ DEEPSEEK_API_KEY no configurada — no se puede auto-mejorar.'); return; }\n console.log(`Analizando ${FILES.length} página(s):\\n${FILES.join('\\n')}\\n`);\n\n const results = [];\n for (const f of FILES) {\n try {\n const r = await fixFile(f);\n if (r) { console.log(` ✅ Mejorada: ${f} (${r.changes.length} cambio/s)`); results.push(r); }\n else { console.log(` ➖ Sin cambios: ${f}`); }\n } catch (e) { console.log(` ⚠️ Error en ${f}: ${e.message}`); }\n }\n\n if (!results.length) { console.log('\\nNada que mejorar. No se envía email ni se commitea.'); return; }\n\n // Commit + push\n try {\n execSync('git config user.name \"AI Security SEO Bot\"');\n execSync('git config user.email \"info@aisecurity.es\"');\n execSync('git add ' + results.map(r => `\"${r.file}\"`).join(' '));\n execSync('git commit -m \"chore(seo-bot): auto-mejora SEO/GEO de ' + results.length + ' pagina(s) [skip ci]\"');\n execSync('git push origin HEAD:main');\n console.log('\\n🚀 Cambios commiteados y pusheados a main.');\n } catch (e) {\n console.log('⚠️ Error en git: ' + e.message);\n }\n\n await sendEmail(results);\n}\n\nmain().catch(e => { console.error(e); process.exit(1); });\n"}, "b543554061c324239d26ab1bdfdb03d02a1018db": {"t": 1785769864.4328377, "n": 10306, "h": "a1ebf34a094070ca", "c": "#!/usr/bin/env node\n'use strict';\n/**\n * seo-autofix — analiza las páginas .astro modificadas, detecta fallos SEO/GEO,\n * los corrige con IA (DeepSeek) mediante reemplazos DIRIGIDOS (nunca reescribe el\n * archivo entero) y, SOLO si mejora algo, hace commit + push y envía un email.\n * Sin mejoras → ni commit ni email.\n */\nconst fs = require('fs');\nconst { execSync } = require('child_process');\n\nconst DEEPSEEK_KEY = (process.env.DEEPSEEK_API_KEY || '').trim();\nconst RESEND_KEY = (process.env.RESEND_API_KEY || '').trim();\nconst EMAIL_TO = (process.env.EMAIL_TO || 'julen.sistemas@gmail.com').trim();\n\nconst FILES = (process.env.CHANGED_FILES || '')\n .split('\\n').map(s => s.trim()).filter(Boolean)\n .filter(f => f.endsWith('.astro') && !f.includes('/api/') && !f.includes('/_'));\n\nconst stripTags = s => s.replace(/<[^>]+>/g, '').replace(/\\s+/g, ' ').trim();\nconst esc = s => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n// ── Extraer title/description del source (const o prop) ──\nfunction getStr(src, key) {\n let m = src.match(new RegExp('const\\\\s+' + key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `const ${key} = \"${m[1]}\"`, repl: v => `const ${key} = \"${v}\"` };\n m = src.match(new RegExp(key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `${key}=\"${m[1]}\"`, repl: v => `${key}=\"${v}\"` };\n return null;\n}\n\nfunction analyze(src, file) {\n const isEn = file.includes('/en/');\n const t = getStr(src, 'title');\n const d = getStr(src, 'description');\n const issues = [];\n if (t && (t.value.length < 40 || t.value.length > 70)) issues.push('title');\n if (d && (d.value.length < 120 || d.value.length > 165)) issues.push('desc');\n\n // attrs: un heading de artículo va sin atributos (

Texto

). Los que llevan\n // class/style son etiquetas de tarjeta, cabeceras de sección de landing o títulos de\n // vídeo: convertirlos a pregunta destroza la UI y no aporta señal (jul 2026).\n const hs = [...src.matchAll(/]*)>([\\s\\S]*?)<\\/h\\1>/gi)]\n .map(m => ({ inner: m[3], text: stripTags(m[3]), plain: !/[<]/.test(m[3]), bare: !m[2].trim() }))\n .filter(h => h.text);\n // Headings genéricos de cierre/estructura: en pregunta quedan forzados.\n const GENERIC = /^(conclusi[oó]n|recursos adicionales|requisitos previos|introducci[oó]n|resumen|preguntas frecuentes|faq)$/i;\n let badHeadings = [];\n if (!isEn && hs.length > 2) {\n const q = hs.filter(h => h.text.startsWith('¿')).length;\n if (q / hs.length < 0.6) {\n badHeadings = hs\n .filter(h => !h.text.startsWith('¿') && h.plain && h.bare\n && !GENERIC.test(h.text.trim())\n && h.text.trim().split(/\\s+/).length >= 3)\n .slice(0, 3); // máx 3 por página: el objetivo es afinar, no reescribir el índice\n if (badHeadings.length) issues.push('headings');\n }\n }\n return { isEn, t, d, hs, badHeadings, issues };\n}\n\nasync function deepseek(messages) {\n const res = await fetch('https://api.deepseek.com/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_KEY}` },\n body: JSON.stringify({ model: 'deepseek-v4-flash', temperature: 0.4, messages }),\n });\n if (!res.ok) throw new Error('DeepSeek HTTP ' + res.status);\n const d = await res.json();\n let txt = d.choices?.[0]?.message?.content || '';\n txt = txt.replace(/```json|```/g, '').trim();\n const s = txt.indexOf('{'), e = txt.lastIndexOf('}');\n return JSON.parse(txt.slice(s, e + 1));\n}\n\nasync function fixFile(file) {\n if (!fs.existsSync(file)) return null;\n let src = fs.readFileSync(file, 'utf8');\n const a = analyze(src, file);\n if (!a.issues.length) return null;\n\n const lang = a.isEn ? 'English' : 'español';\n const prompt = `Eres un editor SEO senior (idioma: ${lang}). Corrige SOLO lo que se pide. Devuelve EXCLUSIVAMENTE un JSON válido, sin texto extra.\n\nPRIORIDAD (importante): las buenas prácticas **SEO clásicas** mandan (son las que dominan el mercado). Optimiza para clic real en Google (CTR): keyword principal al principio, título claro, atractivo y honesto (sin clickbait). Las buenas prácticas **GEO** (formato pregunta, lenguaje natural para IA) son SECUNDARIAS y **NUNCA** deben empeorar la keyword ni la claridad SEO. Ante duda, gana el SEO.\n\nReglas:\n- \"title\": reescríbelo si mide <40 o >70 caracteres. Keyword principal al inicio + marca \"AI Security\" si ya estaba. 50-65 ideal, orientado a CTR. Si ya está bien, pon null.\n- \"description\": reescríbela si mide <120 o >165 caracteres. 140-160 ideal, con la keyword y un gancho/CTA que suba el CTR. Si está bien, pon null.\n- \"headings\": SOLO español. Convierte a pregunta (¿…?) **solo si sigue siendo natural y conserva la keyword**; si forzar la pregunta perjudica el SEO, NO lo incluyas (deja el titular como está). Devuelve [] si no se piden.\n- No inventes datos. No cambies el idioma.\n\nDatos actuales:\ntitle (${a.t ? a.t.value.length : 0} car.): ${a.t ? a.t.value : '—'}\ndescription (${a.d ? a.d.value.length : 0} car.): ${a.d ? a.d.value : '—'}\nheadings_a_convertir: ${JSON.stringify(a.badHeadings.map(h => h.text))}\n\nFormato de salida:\n{\"title\": null|\"...\", \"description\": null|\"...\", \"headings\": [{\"from\":\"texto original\",\"to\":\"¿pregunta?\"}]}`;\n\n let out;\n try {\n out = await deepseek([{ role: 'user', content: prompt }]);\n } catch (e) { console.log(` ⚠️ DeepSeek falló en ${file}: ${e.message}`); return null; }\n\n const changes = [];\n // Title\n if (a.issues.includes('title') && out.title && typeof out.title === 'string') {\n const nt = out.title.trim();\n if (nt.length >= 40 && nt.length <= 70 && a.t && src.includes(a.t.find)) {\n src = src.replace(a.t.find, a.t.repl(nt.replace(/\"/g, \"'\")));\n changes.push({ k: 'Title', from: a.t.value, to: nt });\n }\n }\n // Description\n if (a.issues.includes('desc') && out.description && typeof out.description === 'string') {\n const nd = out.description.trim();\n if (nd.length >= 120 && nd.length <= 165 && a.d && src.includes(a.d.find)) {\n src = src.replace(a.d.find, a.d.repl(nd.replace(/\"/g, \"'\")));\n changes.push({ k: 'Description', from: a.d.value, to: nd });\n }\n }\n // Headings (solo plano, reemplazo dirigido del texto interno)\n if (Array.isArray(out.headings)) {\n for (const h of out.headings) {\n if (!h || !h.from || !h.to || !h.to.startsWith('¿')) continue;\n const re = new RegExp('(]*>)\\\\s*' + esc(h.from) + '\\\\s*()');\n if (re.test(src)) {\n src = src.replace(re, `$1${h.to}$3`);\n changes.push({ k: 'Heading', from: h.from, to: h.to });\n }\n }\n }\n\n if (!changes.length) return null;\n // Re-validar: que no aparezca ningún problema NUEVO. Antes los cambios de Heading\n // saltaban esta comprobación por completo y se commiteaban aunque no mejorase nada;\n // era la vía por la que entraban las reescrituras masivas de titulares (jul 2026).\n const after = analyze(src, file);\n if (after.issues.some(i => !a.issues.includes(i))) return null;\n\n fs.writeFileSync(file, src, 'utf8');\n return { file, changes, before: a.issues.length, after: after.issues.length };\n}\n\nfunction emailHtml(results) {\n const rows = results.map(r => `\n
\n
${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}
\n ${r.changes.map(c => `\n
\n
${c.k}
\n
${c.from}
\n
${c.to}
\n
`).join('')}\n
`).join('');\n return `
\n

🤖 Auto-mejora SEO/GEO aplicada

\n

El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a main.

\n ${rows}\n

AI Security · SEO/GEO bot

\n
`;\n}\n\nasync function sendEmail(results) {\n if (!RESEND_KEY) { console.log(' ⚠️ RESEND_API_KEY no configurada — sin email'); return; }\n const res = await fetch('https://api.resend.com/emails', {\n method: 'POST',\n headers: { 'Authorization': `Bearer ${RESEND_KEY}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n from: 'AI Security SEO Bot ',\n to: [EMAIL_TO],\n subject: `🤖 SEO/GEO: ${results.length} página(s) mejoradas automáticamente`,\n html: emailHtml(results),\n }),\n });\n console.log(` 📧 Email: HTTP ${res.status}`);\n}\n\nasync function main() {\n if (!FILES.length) { console.log('Sin páginas .astro que analizar.'); return; }\n if (!DEEPSEEK_KEY) { console.log('⚠️ DEEPSEEK_API_KEY no configurada — no se puede auto-mejorar.'); return; }\n console.log(`Analizando ${FILES.length} página(s):\\n${FILES.join('\\n')}\\n`);\n\n const results = [];\n for (const f of FILES) {\n try {\n const r = await fixFile(f);\n if (r) { console.log(` ✅ Mejorada: ${f} (${r.changes.length} cambio/s)`); results.push(r); }\n else { console.log(` ➖ Sin cambios: ${f}`); }\n } catch (e) { console.log(` ⚠️ Error en ${f}: ${e.message}`); }\n }\n\n if (!results.length) { console.log('\\nNada que mejorar. No se envía email ni se commitea.'); return; }\n\n // Commit + push\n try {\n execSync('git config user.name \"AI Security SEO Bot\"');\n execSync('git config user.email \"info@aisecurity.es\"');\n execSync('git add ' + results.map(r => `\"${r.file}\"`).join(' '));\n execSync('git commit -m \"chore(seo-bot): auto-mejora SEO/GEO de ' + results.length + ' pagina(s) [skip ci]\"');\n execSync('git push origin HEAD:main');\n console.log('\\n🚀 Cambios commiteados y pusheados a main.');\n } catch (e) {\n console.log('⚠️ Error en git: ' + e.message);\n }\n\n await sendEmail(results);\n}\n\nmain().catch(e => { console.error(e); process.exit(1); });\n"}, "b71137bd97ba025c546e9a364c71cdd1775aeae8": {"t": 1785769865.7987335, "n": 9456, "h": "9dd7b8e26beee575", "c": "#!/usr/bin/env node\n'use strict';\n/**\n * seo-autofix — analiza las páginas .astro modificadas, detecta fallos SEO/GEO,\n * los corrige con IA (DeepSeek) mediante reemplazos DIRIGIDOS (nunca reescribe el\n * archivo entero) y, SOLO si mejora algo, hace commit + push y envía un email.\n * Sin mejoras → ni commit ni email.\n */\nconst fs = require('fs');\nconst { execSync } = require('child_process');\n\nconst DEEPSEEK_KEY = (process.env.DEEPSEEK_API_KEY || '').trim();\nconst RESEND_KEY = (process.env.RESEND_API_KEY || '').trim();\nconst EMAIL_TO = (process.env.EMAIL_TO || 'julen.sistemas@gmail.com').trim();\n\nconst FILES = (process.env.CHANGED_FILES || '')\n .split('\\n').map(s => s.trim()).filter(Boolean)\n .filter(f => f.endsWith('.astro') && !f.includes('/api/') && !f.includes('/_'));\n\nconst stripTags = s => s.replace(/<[^>]+>/g, '').replace(/\\s+/g, ' ').trim();\nconst esc = s => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n// ── Extraer title/description del source (const o prop) ──\nfunction getStr(src, key) {\n let m = src.match(new RegExp('const\\\\s+' + key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `const ${key} = \"${m[1]}\"`, repl: v => `const ${key} = \"${v}\"` };\n m = src.match(new RegExp(key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `${key}=\"${m[1]}\"`, repl: v => `${key}=\"${v}\"` };\n return null;\n}\n\nfunction analyze(src, file) {\n const isEn = file.includes('/en/');\n const t = getStr(src, 'title');\n const d = getStr(src, 'description');\n const issues = [];\n if (t && (t.value.length < 40 || t.value.length > 70)) issues.push('title');\n if (d && (d.value.length < 120 || d.value.length > 165)) issues.push('desc');\n\n const hs = [...src.matchAll(/]*>([\\s\\S]*?)<\\/h\\1>/gi)]\n .map(m => ({ inner: m[2], text: stripTags(m[2]), plain: !/[<]/.test(m[2]) }))\n .filter(h => h.text);\n let badHeadings = [];\n if (!isEn && hs.length > 2) {\n const q = hs.filter(h => h.text.startsWith('¿')).length;\n if (q / hs.length < 0.6) {\n badHeadings = hs.filter(h => !h.text.startsWith('¿') && h.plain).slice(0, 8);\n if (badHeadings.length) issues.push('headings');\n }\n }\n return { isEn, t, d, hs, badHeadings, issues };\n}\n\nasync function deepseek(messages) {\n const res = await fetch('https://api.deepseek.com/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_KEY}` },\n body: JSON.stringify({ model: 'deepseek-v4-flash', temperature: 0.4, messages }),\n });\n if (!res.ok) throw new Error('DeepSeek HTTP ' + res.status);\n const d = await res.json();\n let txt = d.choices?.[0]?.message?.content || '';\n txt = txt.replace(/```json|```/g, '').trim();\n const s = txt.indexOf('{'), e = txt.lastIndexOf('}');\n return JSON.parse(txt.slice(s, e + 1));\n}\n\nasync function fixFile(file) {\n if (!fs.existsSync(file)) return null;\n let src = fs.readFileSync(file, 'utf8');\n const a = analyze(src, file);\n if (!a.issues.length) return null;\n\n const lang = a.isEn ? 'English' : 'español';\n const prompt = `Eres un editor SEO senior (idioma: ${lang}). Corrige SOLO lo que se pide. Devuelve EXCLUSIVAMENTE un JSON válido, sin texto extra.\n\nPRIORIDAD (importante): las buenas prácticas **SEO clásicas** mandan (son las que dominan el mercado). Optimiza para clic real en Google (CTR): keyword principal al principio, título claro, atractivo y honesto (sin clickbait). Las buenas prácticas **GEO** (formato pregunta, lenguaje natural para IA) son SECUNDARIAS y **NUNCA** deben empeorar la keyword ni la claridad SEO. Ante duda, gana el SEO.\n\nReglas:\n- \"title\": reescríbelo si mide <40 o >70 caracteres. Keyword principal al inicio + marca \"AI Security\" si ya estaba. 50-65 ideal, orientado a CTR. Si ya está bien, pon null.\n- \"description\": reescríbela si mide <120 o >165 caracteres. 140-160 ideal, con la keyword y un gancho/CTA que suba el CTR. Si está bien, pon null.\n- \"headings\": SOLO español. Convierte a pregunta (¿…?) **solo si sigue siendo natural y conserva la keyword**; si forzar la pregunta perjudica el SEO, NO lo incluyas (deja el titular como está). Devuelve [] si no se piden.\n- No inventes datos. No cambies el idioma.\n\nDatos actuales:\ntitle (${a.t ? a.t.value.length : 0} car.): ${a.t ? a.t.value : '—'}\ndescription (${a.d ? a.d.value.length : 0} car.): ${a.d ? a.d.value : '—'}\nheadings_a_convertir: ${JSON.stringify(a.badHeadings.map(h => h.text))}\n\nFormato de salida:\n{\"title\": null|\"...\", \"description\": null|\"...\", \"headings\": [{\"from\":\"texto original\",\"to\":\"¿pregunta?\"}]}`;\n\n let out;\n try {\n out = await deepseek([{ role: 'user', content: prompt }]);\n } catch (e) { console.log(` ⚠️ DeepSeek falló en ${file}: ${e.message}`); return null; }\n\n const changes = [];\n // Title\n if (a.issues.includes('title') && out.title && typeof out.title === 'string') {\n const nt = out.title.trim();\n if (nt.length >= 40 && nt.length <= 70 && a.t && src.includes(a.t.find)) {\n src = src.replace(a.t.find, a.t.repl(nt.replace(/\"/g, \"'\")));\n changes.push({ k: 'Title', from: a.t.value, to: nt });\n }\n }\n // Description\n if (a.issues.includes('desc') && out.description && typeof out.description === 'string') {\n const nd = out.description.trim();\n if (nd.length >= 120 && nd.length <= 165 && a.d && src.includes(a.d.find)) {\n src = src.replace(a.d.find, a.d.repl(nd.replace(/\"/g, \"'\")));\n changes.push({ k: 'Description', from: a.d.value, to: nd });\n }\n }\n // Headings (solo plano, reemplazo dirigido del texto interno)\n if (Array.isArray(out.headings)) {\n for (const h of out.headings) {\n if (!h || !h.from || !h.to || !h.to.startsWith('¿')) continue;\n const re = new RegExp('(]*>)\\\\s*' + esc(h.from) + '\\\\s*()');\n if (re.test(src)) {\n src = src.replace(re, `$1${h.to}$3`);\n changes.push({ k: 'Heading', from: h.from, to: h.to });\n }\n }\n }\n\n if (!changes.length) return null;\n // Re-validar: que no haya empeorado nada obvio (longitudes ok)\n const after = analyze(src, file);\n if (after.issues.length >= a.issues.length && !changes.some(c => c.k === 'Heading')) return null;\n\n fs.writeFileSync(file, src, 'utf8');\n return { file, changes, before: a.issues.length, after: after.issues.length };\n}\n\nfunction emailHtml(results) {\n const rows = results.map(r => `\n
\n
${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}
\n ${r.changes.map(c => `\n
\n
${c.k}
\n
${c.from}
\n
${c.to}
\n
`).join('')}\n
`).join('');\n return `
\n

🤖 Auto-mejora SEO/GEO aplicada

\n

El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a main.

\n ${rows}\n

AI Security · SEO/GEO bot

\n
`;\n}\n\nasync function sendEmail(results) {\n if (!RESEND_KEY) { console.log(' ⚠️ RESEND_API_KEY no configurada — sin email'); return; }\n const res = await fetch('https://api.resend.com/emails', {\n method: 'POST',\n headers: { 'Authorization': `Bearer ${RESEND_KEY}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n from: 'AI Security SEO Bot ',\n to: [EMAIL_TO],\n subject: `🤖 SEO/GEO: ${results.length} página(s) mejoradas automáticamente`,\n html: emailHtml(results),\n }),\n });\n console.log(` 📧 Email: HTTP ${res.status}`);\n}\n\nasync function main() {\n if (!FILES.length) { console.log('Sin páginas .astro que analizar.'); return; }\n if (!DEEPSEEK_KEY) { console.log('⚠️ DEEPSEEK_API_KEY no configurada — no se puede auto-mejorar.'); return; }\n console.log(`Analizando ${FILES.length} página(s):\\n${FILES.join('\\n')}\\n`);\n\n const results = [];\n for (const f of FILES) {\n try {\n const r = await fixFile(f);\n if (r) { console.log(` ✅ Mejorada: ${f} (${r.changes.length} cambio/s)`); results.push(r); }\n else { console.log(` ➖ Sin cambios: ${f}`); }\n } catch (e) { console.log(` ⚠️ Error en ${f}: ${e.message}`); }\n }\n\n if (!results.length) { console.log('\\nNada que mejorar. No se envía email ni se commitea.'); return; }\n\n // Commit + push\n try {\n execSync('git config user.name \"AI Security SEO Bot\"');\n execSync('git config user.email \"info@aisecurity.es\"');\n execSync('git add ' + results.map(r => `\"${r.file}\"`).join(' '));\n execSync('git commit -m \"chore(seo-bot): auto-mejora SEO/GEO de ' + results.length + ' pagina(s) [skip ci]\"');\n execSync('git push origin HEAD:main');\n console.log('\\n🚀 Cambios commiteados y pusheados a main.');\n } catch (e) {\n console.log('⚠️ Error en git: ' + e.message);\n }\n\n await sendEmail(results);\n}\n\nmain().catch(e => { console.error(e); process.exit(1); });\n"}, "00af31020f7bbc2f0bdd28cabebd5d5cfcef9022": {"t": 1785769869.77631, "n": 9452, "h": "c29c74b28a5c1651", "c": "#!/usr/bin/env node\n'use strict';\n/**\n * seo-autofix — analiza las páginas .astro modificadas, detecta fallos SEO/GEO,\n * los corrige con IA (DeepSeek) mediante reemplazos DIRIGIDOS (nunca reescribe el\n * archivo entero) y, SOLO si mejora algo, hace commit + push y envía un email.\n * Sin mejoras → ni commit ni email.\n */\nconst fs = require('fs');\nconst { execSync } = require('child_process');\n\nconst DEEPSEEK_KEY = (process.env.DEEPSEEK_API_KEY || '').trim();\nconst RESEND_KEY = (process.env.RESEND_API_KEY || '').trim();\nconst EMAIL_TO = (process.env.EMAIL_TO || 'julen.sistemas@gmail.com').trim();\n\nconst FILES = (process.env.CHANGED_FILES || '')\n .split('\\n').map(s => s.trim()).filter(Boolean)\n .filter(f => f.endsWith('.astro') && !f.includes('/api/') && !f.includes('/_'));\n\nconst stripTags = s => s.replace(/<[^>]+>/g, '').replace(/\\s+/g, ' ').trim();\nconst esc = s => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n// ── Extraer title/description del source (const o prop) ──\nfunction getStr(src, key) {\n let m = src.match(new RegExp('const\\\\s+' + key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `const ${key} = \"${m[1]}\"`, repl: v => `const ${key} = \"${v}\"` };\n m = src.match(new RegExp(key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `${key}=\"${m[1]}\"`, repl: v => `${key}=\"${v}\"` };\n return null;\n}\n\nfunction analyze(src, file) {\n const isEn = file.includes('/en/');\n const t = getStr(src, 'title');\n const d = getStr(src, 'description');\n const issues = [];\n if (t && (t.value.length < 40 || t.value.length > 70)) issues.push('title');\n if (d && (d.value.length < 120 || d.value.length > 165)) issues.push('desc');\n\n const hs = [...src.matchAll(/]*>([\\s\\S]*?)<\\/h\\1>/gi)]\n .map(m => ({ inner: m[2], text: stripTags(m[2]), plain: !/[<]/.test(m[2]) }))\n .filter(h => h.text);\n let badHeadings = [];\n if (!isEn && hs.length > 2) {\n const q = hs.filter(h => h.text.startsWith('¿')).length;\n if (q / hs.length < 0.6) {\n badHeadings = hs.filter(h => !h.text.startsWith('¿') && h.plain).slice(0, 8);\n if (badHeadings.length) issues.push('headings');\n }\n }\n return { isEn, t, d, hs, badHeadings, issues };\n}\n\nasync function deepseek(messages) {\n const res = await fetch('https://api.deepseek.com/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_KEY}` },\n body: JSON.stringify({ model: 'deepseek-chat', temperature: 0.4, messages }),\n });\n if (!res.ok) throw new Error('DeepSeek HTTP ' + res.status);\n const d = await res.json();\n let txt = d.choices?.[0]?.message?.content || '';\n txt = txt.replace(/```json|```/g, '').trim();\n const s = txt.indexOf('{'), e = txt.lastIndexOf('}');\n return JSON.parse(txt.slice(s, e + 1));\n}\n\nasync function fixFile(file) {\n if (!fs.existsSync(file)) return null;\n let src = fs.readFileSync(file, 'utf8');\n const a = analyze(src, file);\n if (!a.issues.length) return null;\n\n const lang = a.isEn ? 'English' : 'español';\n const prompt = `Eres un editor SEO senior (idioma: ${lang}). Corrige SOLO lo que se pide. Devuelve EXCLUSIVAMENTE un JSON válido, sin texto extra.\n\nPRIORIDAD (importante): las buenas prácticas **SEO clásicas** mandan (son las que dominan el mercado). Optimiza para clic real en Google (CTR): keyword principal al principio, título claro, atractivo y honesto (sin clickbait). Las buenas prácticas **GEO** (formato pregunta, lenguaje natural para IA) son SECUNDARIAS y **NUNCA** deben empeorar la keyword ni la claridad SEO. Ante duda, gana el SEO.\n\nReglas:\n- \"title\": reescríbelo si mide <40 o >70 caracteres. Keyword principal al inicio + marca \"AI Security\" si ya estaba. 50-65 ideal, orientado a CTR. Si ya está bien, pon null.\n- \"description\": reescríbela si mide <120 o >165 caracteres. 140-160 ideal, con la keyword y un gancho/CTA que suba el CTR. Si está bien, pon null.\n- \"headings\": SOLO español. Convierte a pregunta (¿…?) **solo si sigue siendo natural y conserva la keyword**; si forzar la pregunta perjudica el SEO, NO lo incluyas (deja el titular como está). Devuelve [] si no se piden.\n- No inventes datos. No cambies el idioma.\n\nDatos actuales:\ntitle (${a.t ? a.t.value.length : 0} car.): ${a.t ? a.t.value : '—'}\ndescription (${a.d ? a.d.value.length : 0} car.): ${a.d ? a.d.value : '—'}\nheadings_a_convertir: ${JSON.stringify(a.badHeadings.map(h => h.text))}\n\nFormato de salida:\n{\"title\": null|\"...\", \"description\": null|\"...\", \"headings\": [{\"from\":\"texto original\",\"to\":\"¿pregunta?\"}]}`;\n\n let out;\n try {\n out = await deepseek([{ role: 'user', content: prompt }]);\n } catch (e) { console.log(` ⚠️ DeepSeek falló en ${file}: ${e.message}`); return null; }\n\n const changes = [];\n // Title\n if (a.issues.includes('title') && out.title && typeof out.title === 'string') {\n const nt = out.title.trim();\n if (nt.length >= 40 && nt.length <= 70 && a.t && src.includes(a.t.find)) {\n src = src.replace(a.t.find, a.t.repl(nt.replace(/\"/g, \"'\")));\n changes.push({ k: 'Title', from: a.t.value, to: nt });\n }\n }\n // Description\n if (a.issues.includes('desc') && out.description && typeof out.description === 'string') {\n const nd = out.description.trim();\n if (nd.length >= 120 && nd.length <= 165 && a.d && src.includes(a.d.find)) {\n src = src.replace(a.d.find, a.d.repl(nd.replace(/\"/g, \"'\")));\n changes.push({ k: 'Description', from: a.d.value, to: nd });\n }\n }\n // Headings (solo plano, reemplazo dirigido del texto interno)\n if (Array.isArray(out.headings)) {\n for (const h of out.headings) {\n if (!h || !h.from || !h.to || !h.to.startsWith('¿')) continue;\n const re = new RegExp('(]*>)\\\\s*' + esc(h.from) + '\\\\s*()');\n if (re.test(src)) {\n src = src.replace(re, `$1${h.to}$3`);\n changes.push({ k: 'Heading', from: h.from, to: h.to });\n }\n }\n }\n\n if (!changes.length) return null;\n // Re-validar: que no haya empeorado nada obvio (longitudes ok)\n const after = analyze(src, file);\n if (after.issues.length >= a.issues.length && !changes.some(c => c.k === 'Heading')) return null;\n\n fs.writeFileSync(file, src, 'utf8');\n return { file, changes, before: a.issues.length, after: after.issues.length };\n}\n\nfunction emailHtml(results) {\n const rows = results.map(r => `\n
\n
${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}
\n ${r.changes.map(c => `\n
\n
${c.k}
\n
${c.from}
\n
${c.to}
\n
`).join('')}\n
`).join('');\n return `
\n

🤖 Auto-mejora SEO/GEO aplicada

\n

El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a main.

\n ${rows}\n

AI Security · SEO/GEO bot

\n
`;\n}\n\nasync function sendEmail(results) {\n if (!RESEND_KEY) { console.log(' ⚠️ RESEND_API_KEY no configurada — sin email'); return; }\n const res = await fetch('https://api.resend.com/emails', {\n method: 'POST',\n headers: { 'Authorization': `Bearer ${RESEND_KEY}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n from: 'AI Security SEO Bot ',\n to: [EMAIL_TO],\n subject: `🤖 SEO/GEO: ${results.length} página(s) mejoradas automáticamente`,\n html: emailHtml(results),\n }),\n });\n console.log(` 📧 Email: HTTP ${res.status}`);\n}\n\nasync function main() {\n if (!FILES.length) { console.log('Sin páginas .astro que analizar.'); return; }\n if (!DEEPSEEK_KEY) { console.log('⚠️ DEEPSEEK_API_KEY no configurada — no se puede auto-mejorar.'); return; }\n console.log(`Analizando ${FILES.length} página(s):\\n${FILES.join('\\n')}\\n`);\n\n const results = [];\n for (const f of FILES) {\n try {\n const r = await fixFile(f);\n if (r) { console.log(` ✅ Mejorada: ${f} (${r.changes.length} cambio/s)`); results.push(r); }\n else { console.log(` ➖ Sin cambios: ${f}`); }\n } catch (e) { console.log(` ⚠️ Error en ${f}: ${e.message}`); }\n }\n\n if (!results.length) { console.log('\\nNada que mejorar. No se envía email ni se commitea.'); return; }\n\n // Commit + push\n try {\n execSync('git config user.name \"AI Security SEO Bot\"');\n execSync('git config user.email \"info@aisecurity.es\"');\n execSync('git add ' + results.map(r => `\"${r.file}\"`).join(' '));\n execSync('git commit -m \"chore(seo-bot): auto-mejora SEO/GEO de ' + results.length + ' pagina(s) [skip ci]\"');\n execSync('git push origin HEAD:main');\n console.log('\\n🚀 Cambios commiteados y pusheados a main.');\n } catch (e) {\n console.log('⚠️ Error en git: ' + e.message);\n }\n\n await sendEmail(results);\n}\n\nmain().catch(e => { console.error(e); process.exit(1); });\n"}, "89afb75d7758e71334dbb10d0bba159bc1806229": {"t": 1785769871.3519585, "n": 9000, "h": "6057f6e4541d70bd", "c": "#!/usr/bin/env node\n'use strict';\n/**\n * seo-autofix — analiza las páginas .astro modificadas, detecta fallos SEO/GEO,\n * los corrige con IA (DeepSeek) mediante reemplazos DIRIGIDOS (nunca reescribe el\n * archivo entero) y, SOLO si mejora algo, hace commit + push y envía un email.\n * Sin mejoras → ni commit ni email.\n */\nconst fs = require('fs');\nconst { execSync } = require('child_process');\n\nconst DEEPSEEK_KEY = (process.env.DEEPSEEK_API_KEY || '').trim();\nconst RESEND_KEY = (process.env.RESEND_API_KEY || '').trim();\nconst EMAIL_TO = (process.env.EMAIL_TO || 'julen.sistemas@gmail.com').trim();\n\nconst FILES = (process.env.CHANGED_FILES || '')\n .split('\\n').map(s => s.trim()).filter(Boolean)\n .filter(f => f.endsWith('.astro') && !f.includes('/api/') && !f.includes('/_'));\n\nconst stripTags = s => s.replace(/<[^>]+>/g, '').replace(/\\s+/g, ' ').trim();\nconst esc = s => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n// ── Extraer title/description del source (const o prop) ──\nfunction getStr(src, key) {\n let m = src.match(new RegExp('const\\\\s+' + key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `const ${key} = \"${m[1]}\"`, repl: v => `const ${key} = \"${v}\"` };\n m = src.match(new RegExp(key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `${key}=\"${m[1]}\"`, repl: v => `${key}=\"${v}\"` };\n return null;\n}\n\nfunction analyze(src, file) {\n const isEn = file.includes('/en/');\n const t = getStr(src, 'title');\n const d = getStr(src, 'description');\n const issues = [];\n if (t && (t.value.length < 40 || t.value.length > 70)) issues.push('title');\n if (d && (d.value.length < 120 || d.value.length > 165)) issues.push('desc');\n\n const hs = [...src.matchAll(/]*>([\\s\\S]*?)<\\/h\\1>/gi)]\n .map(m => ({ inner: m[2], text: stripTags(m[2]), plain: !/[<]/.test(m[2]) }))\n .filter(h => h.text);\n let badHeadings = [];\n if (!isEn && hs.length > 2) {\n const q = hs.filter(h => h.text.startsWith('¿')).length;\n if (q / hs.length < 0.6) {\n badHeadings = hs.filter(h => !h.text.startsWith('¿') && h.plain).slice(0, 8);\n if (badHeadings.length) issues.push('headings');\n }\n }\n return { isEn, t, d, hs, badHeadings, issues };\n}\n\nasync function deepseek(messages) {\n const res = await fetch('https://api.deepseek.com/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_KEY}` },\n body: JSON.stringify({ model: 'deepseek-chat', temperature: 0.4, messages }),\n });\n if (!res.ok) throw new Error('DeepSeek HTTP ' + res.status);\n const d = await res.json();\n let txt = d.choices?.[0]?.message?.content || '';\n txt = txt.replace(/```json|```/g, '').trim();\n const s = txt.indexOf('{'), e = txt.lastIndexOf('}');\n return JSON.parse(txt.slice(s, e + 1));\n}\n\nasync function fixFile(file) {\n if (!fs.existsSync(file)) return null;\n let src = fs.readFileSync(file, 'utf8');\n const a = analyze(src, file);\n if (!a.issues.length) return null;\n\n const lang = a.isEn ? 'English' : 'español';\n const prompt = `Eres un editor SEO/GEO. Corrige SOLO lo que se pide de esta página (idioma: ${lang}). Devuelve EXCLUSIVAMENTE un JSON válido, sin texto extra.\n\nReglas:\n- \"title\": reescríbelo si mide <40 o >70 caracteres. Mantén la(s) keyword(s) principal(es) y la marca \"AI Security\" si ya estaba. 50-65 ideal. Si ya está bien, pon null.\n- \"description\": reescríbela si mide <120 o >165 caracteres. 140-160 ideal, atractiva, con keyword. Si está bien, pon null.\n- \"headings\": SOLO para español. Convierte cada titular dado a una pregunta natural que empiece por ¿ y acabe en ?, conservando el significado y las keywords. Devuelve [] si no se piden.\n- No inventes datos. No cambies el idioma.\n\nDatos actuales:\ntitle (${a.t ? a.t.value.length : 0} car.): ${a.t ? a.t.value : '—'}\ndescription (${a.d ? a.d.value.length : 0} car.): ${a.d ? a.d.value : '—'}\nheadings_a_convertir: ${JSON.stringify(a.badHeadings.map(h => h.text))}\n\nFormato de salida:\n{\"title\": null|\"...\", \"description\": null|\"...\", \"headings\": [{\"from\":\"texto original\",\"to\":\"¿pregunta?\"}]}`;\n\n let out;\n try {\n out = await deepseek([{ role: 'user', content: prompt }]);\n } catch (e) { console.log(` ⚠️ DeepSeek falló en ${file}: ${e.message}`); return null; }\n\n const changes = [];\n // Title\n if (a.issues.includes('title') && out.title && typeof out.title === 'string') {\n const nt = out.title.trim();\n if (nt.length >= 40 && nt.length <= 70 && a.t && src.includes(a.t.find)) {\n src = src.replace(a.t.find, a.t.repl(nt.replace(/\"/g, \"'\")));\n changes.push({ k: 'Title', from: a.t.value, to: nt });\n }\n }\n // Description\n if (a.issues.includes('desc') && out.description && typeof out.description === 'string') {\n const nd = out.description.trim();\n if (nd.length >= 120 && nd.length <= 165 && a.d && src.includes(a.d.find)) {\n src = src.replace(a.d.find, a.d.repl(nd.replace(/\"/g, \"'\")));\n changes.push({ k: 'Description', from: a.d.value, to: nd });\n }\n }\n // Headings (solo plano, reemplazo dirigido del texto interno)\n if (Array.isArray(out.headings)) {\n for (const h of out.headings) {\n if (!h || !h.from || !h.to || !h.to.startsWith('¿')) continue;\n const re = new RegExp('(]*>)\\\\s*' + esc(h.from) + '\\\\s*()');\n if (re.test(src)) {\n src = src.replace(re, `$1${h.to}$3`);\n changes.push({ k: 'Heading', from: h.from, to: h.to });\n }\n }\n }\n\n if (!changes.length) return null;\n // Re-validar: que no haya empeorado nada obvio (longitudes ok)\n const after = analyze(src, file);\n if (after.issues.length >= a.issues.length && !changes.some(c => c.k === 'Heading')) return null;\n\n fs.writeFileSync(file, src, 'utf8');\n return { file, changes, before: a.issues.length, after: after.issues.length };\n}\n\nfunction emailHtml(results) {\n const rows = results.map(r => `\n
\n
${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}
\n ${r.changes.map(c => `\n
\n
${c.k}
\n
${c.from}
\n
${c.to}
\n
`).join('')}\n
`).join('');\n return `
\n

🤖 Auto-mejora SEO/GEO aplicada

\n

El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a main.

\n ${rows}\n

AI Security · SEO/GEO bot

\n
`;\n}\n\nasync function sendEmail(results) {\n if (!RESEND_KEY) { console.log(' ⚠️ RESEND_API_KEY no configurada — sin email'); return; }\n const res = await fetch('https://api.resend.com/emails', {\n method: 'POST',\n headers: { 'Authorization': `Bearer ${RESEND_KEY}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n from: 'AI Security SEO Bot ',\n to: [EMAIL_TO],\n subject: `🤖 SEO/GEO: ${results.length} página(s) mejoradas automáticamente`,\n html: emailHtml(results),\n }),\n });\n console.log(` 📧 Email: HTTP ${res.status}`);\n}\n\nasync function main() {\n if (!FILES.length) { console.log('Sin páginas .astro que analizar.'); return; }\n if (!DEEPSEEK_KEY) { console.log('⚠️ DEEPSEEK_API_KEY no configurada — no se puede auto-mejorar.'); return; }\n console.log(`Analizando ${FILES.length} página(s):\\n${FILES.join('\\n')}\\n`);\n\n const results = [];\n for (const f of FILES) {\n try {\n const r = await fixFile(f);\n if (r) { console.log(` ✅ Mejorada: ${f} (${r.changes.length} cambio/s)`); results.push(r); }\n else { console.log(` ➖ Sin cambios: ${f}`); }\n } catch (e) { console.log(` ⚠️ Error en ${f}: ${e.message}`); }\n }\n\n if (!results.length) { console.log('\\nNada que mejorar. No se envía email ni se commitea.'); return; }\n\n // Commit + push\n try {\n execSync('git config user.name \"AI Security SEO Bot\"');\n execSync('git config user.email \"info@aisecurity.es\"');\n execSync('git add ' + results.map(r => `\"${r.file}\"`).join(' '));\n execSync('git commit -m \"chore(seo-bot): auto-mejora SEO/GEO de ' + results.length + ' pagina(s) [skip ci]\"');\n execSync('git push origin HEAD:main');\n console.log('\\n🚀 Cambios commiteados y pusheados a main.');\n } catch (e) {\n console.log('⚠️ Error en git: ' + e.message);\n }\n\n await sendEmail(results);\n}\n\nmain().catch(e => { console.error(e); process.exit(1); });\n"}, "549a153dc846d0e913ead3edd459795f2838d4f5": {"t": 1786068019.842593, "n": 10306, "h": "a1ebf34a094070ca", "c": "#!/usr/bin/env node\n'use strict';\n/**\n * seo-autofix — analiza las páginas .astro modificadas, detecta fallos SEO/GEO,\n * los corrige con IA (DeepSeek) mediante reemplazos DIRIGIDOS (nunca reescribe el\n * archivo entero) y, SOLO si mejora algo, hace commit + push y envía un email.\n * Sin mejoras → ni commit ni email.\n */\nconst fs = require('fs');\nconst { execSync } = require('child_process');\n\nconst DEEPSEEK_KEY = (process.env.DEEPSEEK_API_KEY || '').trim();\nconst RESEND_KEY = (process.env.RESEND_API_KEY || '').trim();\nconst EMAIL_TO = (process.env.EMAIL_TO || 'julen.sistemas@gmail.com').trim();\n\nconst FILES = (process.env.CHANGED_FILES || '')\n .split('\\n').map(s => s.trim()).filter(Boolean)\n .filter(f => f.endsWith('.astro') && !f.includes('/api/') && !f.includes('/_'));\n\nconst stripTags = s => s.replace(/<[^>]+>/g, '').replace(/\\s+/g, ' ').trim();\nconst esc = s => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n// ── Extraer title/description del source (const o prop) ──\nfunction getStr(src, key) {\n let m = src.match(new RegExp('const\\\\s+' + key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `const ${key} = \"${m[1]}\"`, repl: v => `const ${key} = \"${v}\"` };\n m = src.match(new RegExp(key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `${key}=\"${m[1]}\"`, repl: v => `${key}=\"${v}\"` };\n return null;\n}\n\nfunction analyze(src, file) {\n const isEn = file.includes('/en/');\n const t = getStr(src, 'title');\n const d = getStr(src, 'description');\n const issues = [];\n if (t && (t.value.length < 40 || t.value.length > 70)) issues.push('title');\n if (d && (d.value.length < 120 || d.value.length > 165)) issues.push('desc');\n\n // attrs: un heading de artículo va sin atributos (

Texto

). Los que llevan\n // class/style son etiquetas de tarjeta, cabeceras de sección de landing o títulos de\n // vídeo: convertirlos a pregunta destroza la UI y no aporta señal (jul 2026).\n const hs = [...src.matchAll(/]*)>([\\s\\S]*?)<\\/h\\1>/gi)]\n .map(m => ({ inner: m[3], text: stripTags(m[3]), plain: !/[<]/.test(m[3]), bare: !m[2].trim() }))\n .filter(h => h.text);\n // Headings genéricos de cierre/estructura: en pregunta quedan forzados.\n const GENERIC = /^(conclusi[oó]n|recursos adicionales|requisitos previos|introducci[oó]n|resumen|preguntas frecuentes|faq)$/i;\n let badHeadings = [];\n if (!isEn && hs.length > 2) {\n const q = hs.filter(h => h.text.startsWith('¿')).length;\n if (q / hs.length < 0.6) {\n badHeadings = hs\n .filter(h => !h.text.startsWith('¿') && h.plain && h.bare\n && !GENERIC.test(h.text.trim())\n && h.text.trim().split(/\\s+/).length >= 3)\n .slice(0, 3); // máx 3 por página: el objetivo es afinar, no reescribir el índice\n if (badHeadings.length) issues.push('headings');\n }\n }\n return { isEn, t, d, hs, badHeadings, issues };\n}\n\nasync function deepseek(messages) {\n const res = await fetch('https://api.deepseek.com/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_KEY}` },\n body: JSON.stringify({ model: 'deepseek-v4-flash', temperature: 0.4, messages }),\n });\n if (!res.ok) throw new Error('DeepSeek HTTP ' + res.status);\n const d = await res.json();\n let txt = d.choices?.[0]?.message?.content || '';\n txt = txt.replace(/```json|```/g, '').trim();\n const s = txt.indexOf('{'), e = txt.lastIndexOf('}');\n return JSON.parse(txt.slice(s, e + 1));\n}\n\nasync function fixFile(file) {\n if (!fs.existsSync(file)) return null;\n let src = fs.readFileSync(file, 'utf8');\n const a = analyze(src, file);\n if (!a.issues.length) return null;\n\n const lang = a.isEn ? 'English' : 'español';\n const prompt = `Eres un editor SEO senior (idioma: ${lang}). Corrige SOLO lo que se pide. Devuelve EXCLUSIVAMENTE un JSON válido, sin texto extra.\n\nPRIORIDAD (importante): las buenas prácticas **SEO clásicas** mandan (son las que dominan el mercado). Optimiza para clic real en Google (CTR): keyword principal al principio, título claro, atractivo y honesto (sin clickbait). Las buenas prácticas **GEO** (formato pregunta, lenguaje natural para IA) son SECUNDARIAS y **NUNCA** deben empeorar la keyword ni la claridad SEO. Ante duda, gana el SEO.\n\nReglas:\n- \"title\": reescríbelo si mide <40 o >70 caracteres. Keyword principal al inicio + marca \"AI Security\" si ya estaba. 50-65 ideal, orientado a CTR. Si ya está bien, pon null.\n- \"description\": reescríbela si mide <120 o >165 caracteres. 140-160 ideal, con la keyword y un gancho/CTA que suba el CTR. Si está bien, pon null.\n- \"headings\": SOLO español. Convierte a pregunta (¿…?) **solo si sigue siendo natural y conserva la keyword**; si forzar la pregunta perjudica el SEO, NO lo incluyas (deja el titular como está). Devuelve [] si no se piden.\n- No inventes datos. No cambies el idioma.\n\nDatos actuales:\ntitle (${a.t ? a.t.value.length : 0} car.): ${a.t ? a.t.value : '—'}\ndescription (${a.d ? a.d.value.length : 0} car.): ${a.d ? a.d.value : '—'}\nheadings_a_convertir: ${JSON.stringify(a.badHeadings.map(h => h.text))}\n\nFormato de salida:\n{\"title\": null|\"...\", \"description\": null|\"...\", \"headings\": [{\"from\":\"texto original\",\"to\":\"¿pregunta?\"}]}`;\n\n let out;\n try {\n out = await deepseek([{ role: 'user', content: prompt }]);\n } catch (e) { console.log(` ⚠️ DeepSeek falló en ${file}: ${e.message}`); return null; }\n\n const changes = [];\n // Title\n if (a.issues.includes('title') && out.title && typeof out.title === 'string') {\n const nt = out.title.trim();\n if (nt.length >= 40 && nt.length <= 70 && a.t && src.includes(a.t.find)) {\n src = src.replace(a.t.find, a.t.repl(nt.replace(/\"/g, \"'\")));\n changes.push({ k: 'Title', from: a.t.value, to: nt });\n }\n }\n // Description\n if (a.issues.includes('desc') && out.description && typeof out.description === 'string') {\n const nd = out.description.trim();\n if (nd.length >= 120 && nd.length <= 165 && a.d && src.includes(a.d.find)) {\n src = src.replace(a.d.find, a.d.repl(nd.replace(/\"/g, \"'\")));\n changes.push({ k: 'Description', from: a.d.value, to: nd });\n }\n }\n // Headings (solo plano, reemplazo dirigido del texto interno)\n if (Array.isArray(out.headings)) {\n for (const h of out.headings) {\n if (!h || !h.from || !h.to || !h.to.startsWith('¿')) continue;\n const re = new RegExp('(]*>)\\\\s*' + esc(h.from) + '\\\\s*()');\n if (re.test(src)) {\n src = src.replace(re, `$1${h.to}$3`);\n changes.push({ k: 'Heading', from: h.from, to: h.to });\n }\n }\n }\n\n if (!changes.length) return null;\n // Re-validar: que no aparezca ningún problema NUEVO. Antes los cambios de Heading\n // saltaban esta comprobación por completo y se commiteaban aunque no mejorase nada;\n // era la vía por la que entraban las reescrituras masivas de titulares (jul 2026).\n const after = analyze(src, file);\n if (after.issues.some(i => !a.issues.includes(i))) return null;\n\n fs.writeFileSync(file, src, 'utf8');\n return { file, changes, before: a.issues.length, after: after.issues.length };\n}\n\nfunction emailHtml(results) {\n const rows = results.map(r => `\n
\n
${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}
\n ${r.changes.map(c => `\n
\n
${c.k}
\n
${c.from}
\n
${c.to}
\n
`).join('')}\n
`).join('');\n return `
\n

🤖 Auto-mejora SEO/GEO aplicada

\n

El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a main.

\n ${rows}\n

AI Security · SEO/GEO bot

\n
`;\n}\n\nasync function sendEmail(results) {\n if (!RESEND_KEY) { console.log(' ⚠️ RESEND_API_KEY no configurada — sin email'); return; }\n const res = await fetch('https://api.resend.com/emails', {\n method: 'POST',\n headers: { 'Authorization': `Bearer ${RESEND_KEY}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n from: 'AI Security SEO Bot ',\n to: [EMAIL_TO],\n subject: `🤖 SEO/GEO: ${results.length} página(s) mejoradas automáticamente`,\n html: emailHtml(results),\n }),\n });\n console.log(` 📧 Email: HTTP ${res.status}`);\n}\n\nasync function main() {\n if (!FILES.length) { console.log('Sin páginas .astro que analizar.'); return; }\n if (!DEEPSEEK_KEY) { console.log('⚠️ DEEPSEEK_API_KEY no configurada — no se puede auto-mejorar.'); return; }\n console.log(`Analizando ${FILES.length} página(s):\\n${FILES.join('\\n')}\\n`);\n\n const results = [];\n for (const f of FILES) {\n try {\n const r = await fixFile(f);\n if (r) { console.log(` ✅ Mejorada: ${f} (${r.changes.length} cambio/s)`); results.push(r); }\n else { console.log(` ➖ Sin cambios: ${f}`); }\n } catch (e) { console.log(` ⚠️ Error en ${f}: ${e.message}`); }\n }\n\n if (!results.length) { console.log('\\nNada que mejorar. No se envía email ni se commitea.'); return; }\n\n // Commit + push\n try {\n execSync('git config user.name \"AI Security SEO Bot\"');\n execSync('git config user.email \"info@aisecurity.es\"');\n execSync('git add ' + results.map(r => `\"${r.file}\"`).join(' '));\n execSync('git commit -m \"chore(seo-bot): auto-mejora SEO/GEO de ' + results.length + ' pagina(s) [skip ci]\"');\n execSync('git push origin HEAD:main');\n console.log('\\n🚀 Cambios commiteados y pusheados a main.');\n } catch (e) {\n console.log('⚠️ Error en git: ' + e.message);\n }\n\n await sendEmail(results);\n}\n\nmain().catch(e => { console.error(e); process.exit(1); });\n"}, "e6dd39c66e1ff0bf1a91dc8b53efe7b0da9bb367": {"t": 1786068536.1883066, "n": 10306, "h": "a1ebf34a094070ca", "c": "#!/usr/bin/env node\n'use strict';\n/**\n * seo-autofix — analiza las páginas .astro modificadas, detecta fallos SEO/GEO,\n * los corrige con IA (DeepSeek) mediante reemplazos DIRIGIDOS (nunca reescribe el\n * archivo entero) y, SOLO si mejora algo, hace commit + push y envía un email.\n * Sin mejoras → ni commit ni email.\n */\nconst fs = require('fs');\nconst { execSync } = require('child_process');\n\nconst DEEPSEEK_KEY = (process.env.DEEPSEEK_API_KEY || '').trim();\nconst RESEND_KEY = (process.env.RESEND_API_KEY || '').trim();\nconst EMAIL_TO = (process.env.EMAIL_TO || 'julen.sistemas@gmail.com').trim();\n\nconst FILES = (process.env.CHANGED_FILES || '')\n .split('\\n').map(s => s.trim()).filter(Boolean)\n .filter(f => f.endsWith('.astro') && !f.includes('/api/') && !f.includes('/_'));\n\nconst stripTags = s => s.replace(/<[^>]+>/g, '').replace(/\\s+/g, ' ').trim();\nconst esc = s => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n// ── Extraer title/description del source (const o prop) ──\nfunction getStr(src, key) {\n let m = src.match(new RegExp('const\\\\s+' + key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `const ${key} = \"${m[1]}\"`, repl: v => `const ${key} = \"${v}\"` };\n m = src.match(new RegExp(key + '\\\\s*=\\\\s*\"([^\"]*)\"'));\n if (m) return { value: m[1], find: `${key}=\"${m[1]}\"`, repl: v => `${key}=\"${v}\"` };\n return null;\n}\n\nfunction analyze(src, file) {\n const isEn = file.includes('/en/');\n const t = getStr(src, 'title');\n const d = getStr(src, 'description');\n const issues = [];\n if (t && (t.value.length < 40 || t.value.length > 70)) issues.push('title');\n if (d && (d.value.length < 120 || d.value.length > 165)) issues.push('desc');\n\n // attrs: un heading de artículo va sin atributos (

Texto

). Los que llevan\n // class/style son etiquetas de tarjeta, cabeceras de sección de landing o títulos de\n // vídeo: convertirlos a pregunta destroza la UI y no aporta señal (jul 2026).\n const hs = [...src.matchAll(/]*)>([\\s\\S]*?)<\\/h\\1>/gi)]\n .map(m => ({ inner: m[3], text: stripTags(m[3]), plain: !/[<]/.test(m[3]), bare: !m[2].trim() }))\n .filter(h => h.text);\n // Headings genéricos de cierre/estructura: en pregunta quedan forzados.\n const GENERIC = /^(conclusi[oó]n|recursos adicionales|requisitos previos|introducci[oó]n|resumen|preguntas frecuentes|faq)$/i;\n let badHeadings = [];\n if (!isEn && hs.length > 2) {\n const q = hs.filter(h => h.text.startsWith('¿')).length;\n if (q / hs.length < 0.6) {\n badHeadings = hs\n .filter(h => !h.text.startsWith('¿') && h.plain && h.bare\n && !GENERIC.test(h.text.trim())\n && h.text.trim().split(/\\s+/).length >= 3)\n .slice(0, 3); // máx 3 por página: el objetivo es afinar, no reescribir el índice\n if (badHeadings.length) issues.push('headings');\n }\n }\n return { isEn, t, d, hs, badHeadings, issues };\n}\n\nasync function deepseek(messages) {\n const res = await fetch('https://api.deepseek.com/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_KEY}` },\n body: JSON.stringify({ model: 'deepseek-v4-flash', temperature: 0.4, messages }),\n });\n if (!res.ok) throw new Error('DeepSeek HTTP ' + res.status);\n const d = await res.json();\n let txt = d.choices?.[0]?.message?.content || '';\n txt = txt.replace(/```json|```/g, '').trim();\n const s = txt.indexOf('{'), e = txt.lastIndexOf('}');\n return JSON.parse(txt.slice(s, e + 1));\n}\n\nasync function fixFile(file) {\n if (!fs.existsSync(file)) return null;\n let src = fs.readFileSync(file, 'utf8');\n const a = analyze(src, file);\n if (!a.issues.length) return null;\n\n const lang = a.isEn ? 'English' : 'español';\n const prompt = `Eres un editor SEO senior (idioma: ${lang}). Corrige SOLO lo que se pide. Devuelve EXCLUSIVAMENTE un JSON válido, sin texto extra.\n\nPRIORIDAD (importante): las buenas prácticas **SEO clásicas** mandan (son las que dominan el mercado). Optimiza para clic real en Google (CTR): keyword principal al principio, título claro, atractivo y honesto (sin clickbait). Las buenas prácticas **GEO** (formato pregunta, lenguaje natural para IA) son SECUNDARIAS y **NUNCA** deben empeorar la keyword ni la claridad SEO. Ante duda, gana el SEO.\n\nReglas:\n- \"title\": reescríbelo si mide <40 o >70 caracteres. Keyword principal al inicio + marca \"AI Security\" si ya estaba. 50-65 ideal, orientado a CTR. Si ya está bien, pon null.\n- \"description\": reescríbela si mide <120 o >165 caracteres. 140-160 ideal, con la keyword y un gancho/CTA que suba el CTR. Si está bien, pon null.\n- \"headings\": SOLO español. Convierte a pregunta (¿…?) **solo si sigue siendo natural y conserva la keyword**; si forzar la pregunta perjudica el SEO, NO lo incluyas (deja el titular como está). Devuelve [] si no se piden.\n- No inventes datos. No cambies el idioma.\n\nDatos actuales:\ntitle (${a.t ? a.t.value.length : 0} car.): ${a.t ? a.t.value : '—'}\ndescription (${a.d ? a.d.value.length : 0} car.): ${a.d ? a.d.value : '—'}\nheadings_a_convertir: ${JSON.stringify(a.badHeadings.map(h => h.text))}\n\nFormato de salida:\n{\"title\": null|\"...\", \"description\": null|\"...\", \"headings\": [{\"from\":\"texto original\",\"to\":\"¿pregunta?\"}]}`;\n\n let out;\n try {\n out = await deepseek([{ role: 'user', content: prompt }]);\n } catch (e) { console.log(` ⚠️ DeepSeek falló en ${file}: ${e.message}`); return null; }\n\n const changes = [];\n // Title\n if (a.issues.includes('title') && out.title && typeof out.title === 'string') {\n const nt = out.title.trim();\n if (nt.length >= 40 && nt.length <= 70 && a.t && src.includes(a.t.find)) {\n src = src.replace(a.t.find, a.t.repl(nt.replace(/\"/g, \"'\")));\n changes.push({ k: 'Title', from: a.t.value, to: nt });\n }\n }\n // Description\n if (a.issues.includes('desc') && out.description && typeof out.description === 'string') {\n const nd = out.description.trim();\n if (nd.length >= 120 && nd.length <= 165 && a.d && src.includes(a.d.find)) {\n src = src.replace(a.d.find, a.d.repl(nd.replace(/\"/g, \"'\")));\n changes.push({ k: 'Description', from: a.d.value, to: nd });\n }\n }\n // Headings (solo plano, reemplazo dirigido del texto interno)\n if (Array.isArray(out.headings)) {\n for (const h of out.headings) {\n if (!h || !h.from || !h.to || !h.to.startsWith('¿')) continue;\n const re = new RegExp('(]*>)\\\\s*' + esc(h.from) + '\\\\s*()');\n if (re.test(src)) {\n src = src.replace(re, `$1${h.to}$3`);\n changes.push({ k: 'Heading', from: h.from, to: h.to });\n }\n }\n }\n\n if (!changes.length) return null;\n // Re-validar: que no aparezca ningún problema NUEVO. Antes los cambios de Heading\n // saltaban esta comprobación por completo y se commiteaban aunque no mejorase nada;\n // era la vía por la que entraban las reescrituras masivas de titulares (jul 2026).\n const after = analyze(src, file);\n if (after.issues.some(i => !a.issues.includes(i))) return null;\n\n fs.writeFileSync(file, src, 'utf8');\n return { file, changes, before: a.issues.length, after: after.issues.length };\n}\n\nfunction emailHtml(results) {\n const rows = results.map(r => `\n
\n
${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}
\n ${r.changes.map(c => `\n
\n
${c.k}
\n
${c.from}
\n
${c.to}
\n
`).join('')}\n
`).join('');\n return `
\n

🤖 Auto-mejora SEO/GEO aplicada

\n

El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a main.

\n ${rows}\n

AI Security · SEO/GEO bot

\n
`;\n}\n\nasync function sendEmail(results) {\n if (!RESEND_KEY) { console.log(' ⚠️ RESEND_API_KEY no configurada — sin email'); return; }\n const res = await fetch('https://api.resend.com/emails', {\n method: 'POST',\n headers: { 'Authorization': `Bearer ${RESEND_KEY}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n from: 'AI Security SEO Bot ',\n to: [EMAIL_TO],\n subject: `🤖 SEO/GEO: ${results.length} página(s) mejoradas automáticamente`,\n html: emailHtml(results),\n }),\n });\n console.log(` 📧 Email: HTTP ${res.status}`);\n}\n\nasync function main() {\n if (!FILES.length) { console.log('Sin páginas .astro que analizar.'); return; }\n if (!DEEPSEEK_KEY) { console.log('⚠️ DEEPSEEK_API_KEY no configurada — no se puede auto-mejorar.'); return; }\n console.log(`Analizando ${FILES.length} página(s):\\n${FILES.join('\\n')}\\n`);\n\n const results = [];\n for (const f of FILES) {\n try {\n const r = await fixFile(f);\n if (r) { console.log(` ✅ Mejorada: ${f} (${r.changes.length} cambio/s)`); results.push(r); }\n else { console.log(` ➖ Sin cambios: ${f}`); }\n } catch (e) { console.log(` ⚠️ Error en ${f}: ${e.message}`); }\n }\n\n if (!results.length) { console.log('\\nNada que mejorar. No se envía email ni se commitea.'); return; }\n\n // Commit + push\n try {\n execSync('git config user.name \"AI Security SEO Bot\"');\n execSync('git config user.email \"info@aisecurity.es\"');\n execSync('git add ' + results.map(r => `\"${r.file}\"`).join(' '));\n execSync('git commit -m \"chore(seo-bot): auto-mejora SEO/GEO de ' + results.length + ' pagina(s) [skip ci]\"');\n execSync('git push origin HEAD:main');\n console.log('\\n🚀 Cambios commiteados y pusheados a main.');\n } catch (e) {\n console.log('⚠️ Error en git: ' + e.message);\n }\n\n await sendEmail(results);\n}\n\nmain().catch(e => { console.error(e); process.exit(1); });\n"}}}}