Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/aisecurity365_syntro.json
T

1 line
50 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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 (<h2>Texto</h2>). 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(/<h([23])([^>]*)>([\\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('(<h([23])[^>]*>)\\\\s*' + esc(h.from) + '\\\\s*(</h\\\\2>)');\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 <div style=\"background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px 20px;margin-bottom:14px;\">\n <div style=\"font-family:monospace;font-size:13px;color:#4f46e5;margin-bottom:10px;\">${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}</div>\n ${r.changes.map(c => `\n <div style=\"margin-bottom:10px;\">\n <div style=\"font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;\">${c.k}</div>\n <div style=\"font-size:13px;color:#b91c1c;text-decoration:line-through;\">${c.from}</div>\n <div style=\"font-size:13px;color:#15803d;font-weight:600;\">${c.to}</div>\n </div>`).join('')}\n </div>`).join('');\n return `<div style=\"font-family:'Segoe UI',sans-serif;max-width:620px;margin:0 auto;background:#f4f6f9;padding:28px;\">\n <h1 style=\"font-size:20px;color:#0f172a;margin:0 0 6px;\">🤖 Auto-mejora SEO/GEO aplicada</h1>\n <p style=\"font-size:14px;color:#475569;margin:0 0 20px;\">El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a <code>main</code>.</p>\n ${rows}\n <p style=\"font-size:12px;color:#94a3b8;margin-top:18px;\">AI Security · SEO/GEO bot</p>\n </div>`;\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 <info@aisecurity.es>',\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 (<h2>Texto</h2>). 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(/<h([23])([^>]*)>([\\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('(<h([23])[^>]*>)\\\\s*' + esc(h.from) + '\\\\s*(</h\\\\2>)');\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 <div style=\"background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px 20px;margin-bottom:14px;\">\n <div style=\"font-family:monospace;font-size:13px;color:#4f46e5;margin-bottom:10px;\">${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}</div>\n ${r.changes.map(c => `\n <div style=\"margin-bottom:10px;\">\n <div style=\"font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;\">${c.k}</div>\n <div style=\"font-size:13px;color:#b91c1c;text-decoration:line-through;\">${c.from}</div>\n <div style=\"font-size:13px;color:#15803d;font-weight:600;\">${c.to}</div>\n </div>`).join('')}\n </div>`).join('');\n return `<div style=\"font-family:'Segoe UI',sans-serif;max-width:620px;margin:0 auto;background:#f4f6f9;padding:28px;\">\n <h1 style=\"font-size:20px;color:#0f172a;margin:0 0 6px;\">🤖 Auto-mejora SEO/GEO aplicada</h1>\n <p style=\"font-size:14px;color:#475569;margin:0 0 20px;\">El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a <code>main</code>.</p>\n ${rows}\n <p style=\"font-size:12px;color:#94a3b8;margin-top:18px;\">AI Security · SEO/GEO bot</p>\n </div>`;\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 <info@aisecurity.es>',\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(/<h([23])[^>]*>([\\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('(<h([23])[^>]*>)\\\\s*' + esc(h.from) + '\\\\s*(</h\\\\2>)');\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 <div style=\"background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px 20px;margin-bottom:14px;\">\n <div style=\"font-family:monospace;font-size:13px;color:#4f46e5;margin-bottom:10px;\">${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}</div>\n ${r.changes.map(c => `\n <div style=\"margin-bottom:10px;\">\n <div style=\"font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;\">${c.k}</div>\n <div style=\"font-size:13px;color:#b91c1c;text-decoration:line-through;\">${c.from}</div>\n <div style=\"font-size:13px;color:#15803d;font-weight:600;\">${c.to}</div>\n </div>`).join('')}\n </div>`).join('');\n return `<div style=\"font-family:'Segoe UI',sans-serif;max-width:620px;margin:0 auto;background:#f4f6f9;padding:28px;\">\n <h1 style=\"font-size:20px;color:#0f172a;margin:0 0 6px;\">🤖 Auto-mejora SEO/GEO aplicada</h1>\n <p style=\"font-size:14px;color:#475569;margin:0 0 20px;\">El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a <code>main</code>.</p>\n ${rows}\n <p style=\"font-size:12px;color:#94a3b8;margin-top:18px;\">AI Security · SEO/GEO bot</p>\n </div>`;\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 <info@aisecurity.es>',\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(/<h([23])[^>]*>([\\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('(<h([23])[^>]*>)\\\\s*' + esc(h.from) + '\\\\s*(</h\\\\2>)');\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 <div style=\"background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px 20px;margin-bottom:14px;\">\n <div style=\"font-family:monospace;font-size:13px;color:#4f46e5;margin-bottom:10px;\">${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}</div>\n ${r.changes.map(c => `\n <div style=\"margin-bottom:10px;\">\n <div style=\"font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;\">${c.k}</div>\n <div style=\"font-size:13px;color:#b91c1c;text-decoration:line-through;\">${c.from}</div>\n <div style=\"font-size:13px;color:#15803d;font-weight:600;\">${c.to}</div>\n </div>`).join('')}\n </div>`).join('');\n return `<div style=\"font-family:'Segoe UI',sans-serif;max-width:620px;margin:0 auto;background:#f4f6f9;padding:28px;\">\n <h1 style=\"font-size:20px;color:#0f172a;margin:0 0 6px;\">🤖 Auto-mejora SEO/GEO aplicada</h1>\n <p style=\"font-size:14px;color:#475569;margin:0 0 20px;\">El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a <code>main</code>.</p>\n ${rows}\n <p style=\"font-size:12px;color:#94a3b8;margin-top:18px;\">AI Security · SEO/GEO bot</p>\n </div>`;\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 <info@aisecurity.es>',\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(/<h([23])[^>]*>([\\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('(<h([23])[^>]*>)\\\\s*' + esc(h.from) + '\\\\s*(</h\\\\2>)');\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 <div style=\"background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px 20px;margin-bottom:14px;\">\n <div style=\"font-family:monospace;font-size:13px;color:#4f46e5;margin-bottom:10px;\">${r.file.replace('src/pages/', '/').replace(/\\.astro$/, '')}</div>\n ${r.changes.map(c => `\n <div style=\"margin-bottom:10px;\">\n <div style=\"font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;\">${c.k}</div>\n <div style=\"font-size:13px;color:#b91c1c;text-decoration:line-through;\">${c.from}</div>\n <div style=\"font-size:13px;color:#15803d;font-weight:600;\">${c.to}</div>\n </div>`).join('')}\n </div>`).join('');\n return `<div style=\"font-family:'Segoe UI',sans-serif;max-width:620px;margin:0 auto;background:#f4f6f9;padding:28px;\">\n <h1 style=\"font-size:20px;color:#0f172a;margin:0 0 6px;\">🤖 Auto-mejora SEO/GEO aplicada</h1>\n <p style=\"font-size:14px;color:#475569;margin:0 0 20px;\">El bot detectó y corrigió mejoras en ${results.length} página(s). Cambios ya commiteados a <code>main</code>.</p>\n ${rows}\n <p style=\"font-size:12px;color:#94a3b8;margin-top:18px;\">AI Security · SEO/GEO bot</p>\n </div>`;\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 <info@aisecurity.es>',\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"}}}}