{"id":2608,"date":"2025-10-09T12:12:11","date_gmt":"2025-10-09T10:12:11","guid":{"rendered":"https:\/\/edukia.org\/?post_type=avada_portfolio&#038;p=2608"},"modified":"2025-10-09T12:12:11","modified_gmt":"2025-10-09T10:12:11","slug":"analitzador-estrategic-seo","status":"publish","type":"avada_portfolio","link":"https:\/\/edukia.org\/ca\/recursos\/analitzador-estrategic-seo\/","title":{"rendered":"Analitzador estrat\u00e8gic SEO"},"content":{"rendered":"<p><div class=\"fusion-fullwidth fullwidth-box fusion-builder-row-1 fusion-flex-container has-pattern-background has-mask-background nonhundred-percent-fullwidth non-hundred-percent-height-scrolling\" style=\"--awb-border-radius-top-left:10px;--awb-border-radius-top-right:10px;--awb-border-radius-bottom-right:10px;--awb-border-radius-bottom-left:10px;--awb-overflow:hidden;--awb-padding-top:30px;--awb-padding-bottom:30px;--awb-background-color:#eaeaea;--awb-flex-wrap:wrap;\" ><div class=\"fusion-builder-row fusion-row fusion-flex-align-items-flex-start fusion-flex-content-wrap\" style=\"max-width:1310.4px;margin-left: calc(-4% \/ 2 );margin-right: calc(-4% \/ 2 );\"><div class=\"fusion-layout-column fusion_builder_column fusion-builder-column-0 fusion_builder_column_1_1 1_1 fusion-flex-column\" style=\"--awb-bg-size:cover;--awb-width-large:100%;--awb-margin-top-large:0px;--awb-spacing-right-large:1.92%;--awb-margin-bottom-large:20px;--awb-spacing-left-large:1.92%;--awb-width-medium:100%;--awb-order-medium:0;--awb-spacing-right-medium:1.92%;--awb-spacing-left-medium:1.92%;--awb-width-small:100%;--awb-order-small:0;--awb-spacing-right-small:1.92%;--awb-spacing-left-small:1.92%;\"><div class=\"fusion-column-wrapper fusion-column-has-shadow fusion-flex-justify-content-flex-start fusion-content-layout-column\"><div class=\"fusion-text fusion-text-1\"><h1><\/h1>\n<h2><span class=\"selected\">\u00bfTe preguntas por qu\u00e9 tu competencia te supera en Google? \u00a1Desc\u00fabrelo aqu\u00ed!<\/span><\/h2>\n<p><span class=\"selected\">Dejar de adivinar es el primer paso para ganar. Con nuestro <\/span><strong><span class=\"selected\">analizador estrat\u00e9gico SEO<\/span><\/strong><span class=\"selected\">, no solo obtendr\u00e1s datos, sino respuestas claras y una hoja de ruta para empezar a escalar posiciones en los resultados de b\u00fasqueda.<\/span><\/p>\n<p><strong><span class=\"selected\">\u00bfQu\u00e9 puedes conseguir con esta herramienta?<\/span><\/strong><\/p>\n<ul>\n<li><strong><span class=\"selected\">Esp\u00eda a tu competencia (de forma \u00e9tica):<\/span><\/strong><span class=\"selected\"> \u00bfQu\u00e9 palabras clave est\u00e1n usando que t\u00fa no? \u00bfC\u00f3mo estructuran su contenido para gustarle a Google? Pon su web y la tuya cara a cara y descubre al instante sus puntos fuertes y sus debilidades.<\/span><\/li>\n<li><strong><span class=\"selected\">Encuentra tesoros ocultos en tu contenido:<\/span><\/strong><span class=\"selected\"> A menudo, peque\u00f1as mejoras traen grandes resultados. La herramienta te se\u00f1alar\u00e1 \u00abkeyword gaps\u00bb, es decir, t\u00e9rminos que tu p\u00fablico busca, que tu competencia usa, y que t\u00fa has pasado por alto. \u00a1Es una mina de oro para atraer nuevo tr\u00e1fico!<\/span><\/li>\n<li><strong><span class=\"selected\">Optimiza tu web sin ser un experto t\u00e9cnico:<\/span><\/strong><span class=\"selected\"> \u00bfTu p\u00e1gina es lenta? \u00bfSe ve mal en m\u00f3viles? \u00bfEst\u00e1s cometiendo errores t\u00e9cnicos que te penalizan sin que lo sepas? Nuestro analizador revisa los aspectos t\u00e9cnicos m\u00e1s importantes y te dice qu\u00e9 falla.<\/span><\/li>\n<li><strong><span class=\"selected\">Consigue un plan de acci\u00f3n claro:<\/span><\/strong><span class=\"selected\"> Olv\u00eddate de los informes llenos de datos que no entiendes. Al final de cada an\u00e1lisis, recibir\u00e1s un <\/span><strong><span class=\"selected\">Resumen Ejecutivo<\/span><\/strong><span class=\"selected\"> con un plan de acci\u00f3n priorizado: las 3-5 tareas m\u00e1s importantes que puedes hacer <\/span><em><span class=\"selected\">hoy mismo<\/span><\/em><span class=\"selected\"> para empezar a mejorar.<\/span><\/li>\n<li><strong><span class=\"selected\">Informes profesionales para tus clientes:<\/span><\/strong><span class=\"selected\"> Si te dedicas al SEO, podr\u00e1s descargar un completo informe en PDF con un dise\u00f1o limpio y profesional, listo para a\u00f1adir tu logo y entreg\u00e1rselo a tus clientes.<\/span><\/li>\n<\/ul>\n<p><span class=\"selected\">Deja de luchar a ciegas por las primeras posiciones. Usa nuestro <\/span><strong><span class=\"selected\">analizador estrat\u00e9gico SEO<\/span><\/strong><span class=\"selected\"> y empieza a tomar decisiones basadas en datos para superar a tu competencia.<\/span><\/p>\n<\/div><\/div><\/div><\/div><\/div><div class=\"fusion-fullwidth fullwidth-box fusion-builder-row-2 fusion-flex-container has-pattern-background has-mask-background hundred-percent-fullwidth non-hundred-percent-height-scrolling\" style=\"--awb-border-radius-top-left:0px;--awb-border-radius-top-right:0px;--awb-border-radius-bottom-right:10px;--awb-border-radius-bottom-left:10px;--awb-overflow:hidden;--awb-background-color:#eaeaea;--awb-flex-wrap:wrap;\" ><div class=\"fusion-builder-row fusion-row fusion-flex-align-items-flex-start fusion-flex-content-wrap\" style=\"width:104% !important;max-width:104% !important;margin-left: calc(-4% \/ 2 );margin-right: calc(-4% \/ 2 );\"><div class=\"fusion-layout-column fusion_builder_column fusion-builder-column-1 fusion_builder_column_1_1 1_1 fusion-flex-column\" style=\"--awb-bg-size:cover;--awb-width-large:100%;--awb-margin-top-large:0px;--awb-spacing-right-large:1.92%;--awb-margin-bottom-large:20px;--awb-spacing-left-large:1.92%;--awb-width-medium:100%;--awb-order-medium:0;--awb-spacing-right-medium:1.92%;--awb-spacing-left-medium:1.92%;--awb-width-small:100%;--awb-order-small:0;--awb-spacing-right-small:1.92%;--awb-spacing-left-small:1.92%;\"><div class=\"fusion-column-wrapper fusion-column-has-shadow fusion-flex-justify-content-flex-start fusion-content-layout-column\"><!DOCTYPE html>\n<html lang=\"es\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Analizador Estrat\u00e9gico SEO<\/title>\n    <script src=\"https:\/\/cdn.tailwindcss.com\"><\/script>\n    <link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n    <link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n    <link href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n    <!-- Librer\u00eda pdfmake -->\n    <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdfmake\/0.2.10\/pdfmake.min.js\" xintegrity=\"sha512-w61kvDEdEhHsvLOA\/B3EMEtQEEXoEV9v3G3MAzVWvkvu5iE3VdsRyrz33sFNLqL3RjWRjS_icB_C3jKCIH_gWQ==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"><\/script>\n    <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdfmake\/0.2.10\/vfs_fonts.js\" xintegrity=\"sha512-nNkHPz+lD0Wf0eF_0H\/OayS2S4C4I_3oOhWro3EDimBOgrQx44iIeA46iTqMvLAkK8sO\/jL2w2sP2XhT2s9vA==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"><\/script>\n    <style>\n        body {\n            font-family: 'Inter', sans-serif;\n        }\n        .loader {\n            border: 4px solid #f3f3f3;\n            border-top: 4px solid #3b82f6;\n            border-radius: 50%;\n            width: 40px;\n            height: 40px;\n            animation: spin 1s linear infinite;\n        }\n        @keyframes spin {\n            0% { transform: rotate(0deg); }\n            100% { transform: rotate(360deg); }\n        }\n        .details-content {\n            max-height: 0;\n            overflow: hidden;\n            transition: max-height 0.5s ease-out;\n        }\n        details[open] .details-content {\n            max-height: 1000px; \/* Adjust as needed *\/\n        }\n        .progress-ring__circle {\n            transition: stroke-dashoffset 0.5s;\n            transform: rotate(-90deg);\n            transform-origin: 50% 50%;\n        }\n    <\/style>\n<\/head>\n<body class=\"bg-gray-900 text-white min-h-screen p-4 sm:p-6 md:p-8\">\n\n    <div class=\"w-full max-w-6xl mx-auto\">\n        \n        <main class=\"w-full pt-8\">\n            <!-- Secci\u00f3n de Instrucciones -->\n            <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg mb-8 border border-gray-700\">\n                <details>\n                    <summary class=\"cursor-pointer font-semibold text-lg text-blue-400 hover:text-blue-300\">\n                        \u00bfC\u00f3mo usar esta herramienta? (Gu\u00eda r\u00e1pida)\n                    <\/summary>\n                    <div class=\"mt-4 text-gray-300 space-y-3 text-sm border-t border-gray-700 pt-4\">\n                        <p><strong>Paso 1: Elige tu tipo de an\u00e1lisis.<\/strong> Tienes dos opciones:<\/p>\n                        <ul class=\"list-disc list-inside ml-4\">\n                            <li><strong>An\u00e1lisis de P\u00e1gina:<\/strong> Compara dos URLs espec\u00edficas para ver todos los detalles (contenido, velocidad, etc.).<\/li>\n                            <li><strong>An\u00e1lisis de Dominio:<\/strong> Obtiene una vista general de la estrategia de contenido de dos sitios web completos.<\/li>\n                        <\/ul>\n                        <p><strong>Paso 2: Introduce las URLs o Dominios.<\/strong> \u00a1No te preocupes por el \"https:\/\/\", la herramienta lo a\u00f1ade por ti!<\/p>\n                        <p><strong>Paso 3 (Opcional pero recomendado):<\/strong> Para el an\u00e1lisis de velocidad, pega tu <strong>API Key de Google PageSpeed<\/strong>. Es gratis y evita errores por l\u00edmite de uso.<\/p>\n                        <p><strong>Paso 4 (\u00a1Dale tu toque!):<\/strong> Si quieres un informe PDF con tu marca (solo en An\u00e1lisis de P\u00e1gina), sube tu logo. Aparecer\u00e1 en la portada para darle un aspecto 100% profesional.<\/p>\n                        <p><strong>Paso 5: \u00a1Analiza y descubre!<\/strong> Revisa los resultados. Si est\u00e1s en \"An\u00e1lisis de P\u00e1gina\", podr\u00e1s descargar tu informe personalizado en PDF.<\/p>\n                    <\/div>\n                <\/details>\n            <\/div>\n\n            <!-- Bot\u00f3n de Descarga PDF (inicialmente oculto) -->\n            <div id=\"pdf-controls\" class=\"text-center mb-8 hidden\">\n                <button id=\"downloadPdfBtn\" class=\"bg-teal-600 hover:bg-teal-700 text-white font-semibold py-2 px-5 rounded-lg transition duration-300 ease-in-out transform hover:scale-105 flex items-center justify-center mx-auto\">\n                    <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-5 w-5 mr-2\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4\" \/>\n                    <\/svg>\n                    Descargar Informe en PDF\n                <\/button>\n            <\/div>\n\n            <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg mb-8 sticky top-4 z-10 border border-gray-700\">\n                <!-- Selector de Modo -->\n                <div class=\"mb-6 flex justify-center bg-gray-700 p-1 rounded-lg\">\n                    <button id=\"pageAnalysisBtn\" class=\"w-full py-2 px-4 text-sm font-semibold rounded-md bg-blue-600 text-white\">An\u00e1lisis de P\u00e1gina<\/button>\n                    <button id=\"domainAnalysisBtn\" class=\"w-full py-2 px-4 text-sm font-semibold rounded-md text-gray-300\">An\u00e1lisis de Dominio<\/button>\n                <\/div>\n                \n                <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                    <div>\n                        <label for=\"urlInput\" id=\"urlInputLabel\" class=\"block text-sm font-medium text-gray-300 mb-2\">Tu URL<\/label>\n                        <input type=\"url\" id=\"urlInput\" placeholder=\"tu-web.com\/pagina\" class=\"w-full bg-gray-700 text-white placeholder-gray-500 border border-gray-600 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 transition\">\n                    <\/div>\n                    <div>\n                        <label for=\"competitorUrlInput\" id=\"competitorUrlInputLabel\" class=\"block text-sm font-medium text-gray-300 mb-2\">URL del Competidor<\/label>\n                        <input type=\"url\" id=\"competitorUrlInput\" placeholder=\"competidor.com\/pagina\" class=\"w-full bg-gray-700 text-white placeholder-gray-500 border border-gray-600 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-teal-500 transition\">\n                    <\/div>\n                <\/div>\n                <!-- API Key and Logo Upload -->\n                <div class=\"mt-4 grid grid-cols-1 md:grid-cols-2 gap-4\">\n                    <div>\n                        <label for=\"apiKeyInput\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n                            Google PageSpeed API Key (Opcional)\n                            <a href=\"https:\/\/developers.google.com\/speed\/docs\/insights\/v5\/get-started#key\" target=\"_blank\" class=\"text-blue-400 hover:text-blue-300 text-xs ml-2\" rel=\"noopener\">(\u00bfC\u00f3mo obtenerla?)<\/a>\n                        <\/label>\n                        <input type=\"text\" id=\"apiKeyInput\" placeholder=\"Introduce tu clave de API\" class=\"w-full bg-gray-700 text-white placeholder-gray-500 border border-gray-600 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 transition text-sm\">\n                    <\/div>\n                     <div>\n                        <label for=\"logoUpload\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n                            Tu Logo (Opcional para PDF)\n                            <span class=\"text-xs text-gray-400 ml-2\">Recomendado: 400x100px<\/span>\n                        <\/label>\n                        <input type=\"file\" id=\"logoUpload\" accept=\"image\/png, image\/jpeg\" class=\"w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700\">\n                        <img id=\"logoPreview\" class=\"hidden h-12 mt-2 rounded bg-white p-1\">\n                    <\/div>\n                <\/div>\n                <div class=\"mt-6\">\n                    <button id=\"analyzeBtn\" class=\"w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-6 rounded-lg transition duration-300 ease-in-out transform hover:scale-105 flex items-center justify-center\">\n                        <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-6 w-6 mr-2\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"><\/path><\/svg>\n                        <span id=\"analyzeBtnText\">Analizar Estrategia de P\u00e1gina<\/span>\n                    <\/button>\n                <\/div>\n            <\/div>\n\n            <div id=\"statusContainer\" class=\"text-center my-8\">\n                <div id=\"loader\" class=\"loader mx-auto hidden\"><\/div>\n                <p id=\"loaderText\" class=\"text-gray-400 mt-2 hidden\"><\/p>\n                <div id=\"error\" class=\"text-red-400 bg-red-900\/50 p-4 rounded-lg hidden\"><\/div>\n            <\/div>\n            \n            <div id=\"results\" class=\"space-y-12\"><\/div>\n        <\/main>\n    <\/div>\n\n<script>\n    \/\/ --- State and DOM Elements ---\n    let analysisMode = 'page'; \/\/ 'page' or 'domain'\n    let latestAnalysisData = null;\n    let logoDataUrl = null;\n    const urlInput = document.getElementById('urlInput');\n    const competitorUrlInput = document.getElementById('competitorUrlInput');\n    const analyzeBtn = document.getElementById('analyzeBtn');\n    const resultsContainer = document.getElementById('results');\n    const loader = document.getElementById('loader');\n    const loaderText = document.getElementById('loaderText');\n    const errorContainer = document.getElementById('error');\n    const pageAnalysisBtn = document.getElementById('pageAnalysisBtn');\n    const domainAnalysisBtn = document.getElementById('domainAnalysisBtn');\n    const urlInputLabel = document.getElementById('urlInputLabel');\n    const competitorUrlInputLabel = document.getElementById('competitorUrlInputLabel');\n    const analyzeBtnText = document.getElementById('analyzeBtnText');\n    const pdfControls = document.getElementById('pdf-controls');\n    const downloadPdfBtn = document.getElementById('downloadPdfBtn');\n    const apiKeyInput = document.getElementById('apiKeyInput');\n    const logoUpload = document.getElementById('logoUpload');\n    const logoPreview = document.getElementById('logoPreview');\n\n    \/\/ --- Event Listeners ---\n    analyzeBtn.addEventListener('click', () => {\n        analysisMode === 'page' ? analyzePageStrategy() : analyzeDomainStrategy();\n    });\n    pageAnalysisBtn.addEventListener('click', () => switchMode('page'));\n    domainAnalysisBtn.addEventListener('click', () => switchMode('domain'));\n    downloadPdfBtn.addEventListener('click', generatePdf);\n    logoUpload.addEventListener('change', (event) => {\n        const file = event.target.files[0];\n        if (file) {\n            const reader = new FileReader();\n            reader.onload = (e) => {\n                logoDataUrl = e.target.result;\n                logoPreview.src = logoDataUrl;\n                logoPreview.classList.remove('hidden');\n            };\n            reader.readAsDataURL(file);\n        }\n    });\n\n    \/\/ --- PDF Generation using pdfmake ---\n    function generatePdf() {\n        if (analysisMode !== 'page' || !latestAnalysisData) {\n            console.error(\"PDF generation is only available for page analysis results.\");\n            return;\n        }\n\n        const userUrl = urlInput.value;\n        const competitorUrl = competitorUrlInput.value;\n        const timestamp = new Date().toLocaleDateString('es-ES');\n        const filename = `informe-seo-pagina-${timestamp.replace(\/\\\/\/g, '-')}.pdf`;\n        \n        const { userData, competitorData } = latestAnalysisData;\n\n        pdfMake.fonts = {\n           Roboto: {\n             normal: 'Roboto-Regular.ttf',\n             bold: 'Roboto-Medium.ttf',\n             italics: 'Roboto-Italic.ttf',\n             bolditalics: 'Roboto-MediumItalic.ttf'\n           }\n        };\n\n        const docDefinition = {\n            content: [],\n            styles: {\n                h1: { fontSize: 26, bold: true, color: '#1d4ed8', alignment: 'center', margin: [0, 20, 0, 10] },\n                h2: { fontSize: 20, bold: true, color: '#1e3a8a', margin: [0, 20, 0, 10], pageBreak: 'before' },\n                h3: { fontSize: 14, bold: true, color: '#1e40af', margin: [0, 15, 0, 5] },\n                p: { margin: [0, 5, 0, 15], lineHeight: 1.5 },\n                url: { color: '#6b7280', alignment: 'center', margin: [0, 5, 0, 5]},\n                toc: { fontSize: 12, bold: true, color: '#1d4ed8', margin: [0, 10, 0, 5] },\n                listItem: { margin: [10, 5, 0, 5] },\n                tableHeader: { bold: true, fontSize: 10, color: 'black' },\n            },\n            defaultStyle: {\n                font: 'Roboto'\n            },\n            footer: function(currentPage, pageCount) {\n                return { text: `P\u00e1gina ${currentPage.toString()} de ${pageCount}`, alignment: 'center', fontSize: 9, margin: [0, 20, 0, 0] };\n            }\n        };\n\n        const titlePageContent = [];\n        if (logoDataUrl) {\n            titlePageContent.push({ image: logoDataUrl, width: 150, alignment: 'center', margin: [0, 0, 0, 40] });\n        }\n        titlePageContent.push({ text: 'An\u00e1lisis Estrat\u00e9gico SEO', style: 'h1' });\n        titlePageContent.push({ text: 'Informe de Comparaci\u00f3n de P\u00e1gina', alignment: 'center', color: '#6b7280', margin: [0, 0, 0, 40] });\n        titlePageContent.push({ text: `URL Analizada: ${userUrl}`, style: 'url' });\n        titlePageContent.push({ text: `URL Competidor: ${competitorUrl}`, style: 'url' });\n        titlePageContent.push({ text: `Fecha: ${timestamp}`, alignment: 'center', margin: [0, 80, 0, 0], color: '#9ca3af' });\n        \n        docDefinition.content.push({ stack: titlePageContent, pageBreak: 'after' });\n        \n        docDefinition.content.push({ toc: { title: { text: '\u00cdndice de Contenidos', style: 'h2' } } });\n        \n        const { verdict, contentParagraph, techParagraph, sortedActions } = getExecutiveSummaryData(userData, competitorData);\n        const actionItems = sortedActions.map((action, index) => {\n             const { text } = getActionItemText(action);\n             return { text: `${index + 1}. ${text.replace(\/<strong>\/g, '').replace(\/<\\\/strong>\/g, '')}`};\n        });\n        docDefinition.content.push({ text: 'Resumen Ejecutivo', style: 'h2', tocItem: true });\n        docDefinition.content.push({ text: 'Veredicto General', style: 'h3' });\n        docDefinition.content.push({ text: verdict, style: 'p' });\n        docDefinition.content.push({ text: contentParagraph.replace(\/<strong>\/g, '').replace(\/<\\\/strong>\/g, ''), style: 'p' });\n        docDefinition.content.push({ text: techParagraph.replace(\/<strong>\/g, '').replace(\/<\\\/strong>\/g, ''), style: 'p' });\n        docDefinition.content.push({ text: 'Plan de Acci\u00f3n Recomendado', style: 'h3' });\n        docDefinition.content.push({ ul: actionItems, style: 'listItem' });\n\n        const keywordGaps = competitorData.keywords.filter(([kw]) => !new Set(userData.keywords.map(k => k[0])).has(kw) && kw.split(' ').length > 1);\n        const contentAnalysisContent = [\n            { text: '1. An\u00e1lisis de Contenido y Palabras Clave', style: 'h2', tocItem: true },\n            { text: 'T\u00e9rminos m\u00e1s frecuentes en el contenido principal de cada web.', style: 'p' },\n            {\n                columns: [\n                    { width: '*', stack: [\n                        { text: 'T\u00e9rminos Clave en tu Web', style: 'h3'},\n                        { table: { widths: ['*', 'auto'], body: [['T\u00e9rmino', 'Usos'], ...userData.keywords] }, layout: 'lightHorizontalLines' }\n                    ]},\n                    { width: '*', stack: [\n                        { text: 'T\u00e9rminos Clave del Competidor', style: 'h3'},\n                        { table: { widths: ['*', 'auto'], body: [['T\u00e9rmino', 'Usos'], ...competitorData.keywords] }, layout: 'lightHorizontalLines' }\n                    ]}\n                ], columnGap: 20\n            }\n        ];\n        if(keywordGaps.length > 0) {\n             contentAnalysisContent.push({ text: 'Oportunidades (Keyword Gaps)', style: 'h3'});\n             contentAnalysisContent.push({ text: 'Frases clave que tu competidor utiliza y t\u00fa no.', style: 'p' });\n             contentAnalysisContent.push({ table: { widths: ['*', 'auto'], body: [['T\u00e9rmino', 'Usos'], ...keywordGaps] }, layout: 'lightHorizontalLines' });\n        }\n        docDefinition.content.push(contentAnalysisContent);\n        \n        docDefinition.content.push([\n            { text: '2. An\u00e1lisis de Estructura y Jerarqu\u00eda', style: 'h2', tocItem: true },\n            {\n                columns: [\n                    { width: '*', stack: [\n                        { text: 'Estructura de tu Web', style: 'h3'},\n                        ...userData.headings.map(h => ({text: `${h.level}: ${h.text}`, margin: [(parseInt(h.level.charAt(1)) - 1) * 15, 0, 0, 5]}))\n                    ]},\n                    { width: '*', stack: [\n                        { text: 'Estructura del Competidor', style: 'h3'},\n                        ...competitorData.headings.map(h => ({text: `${h.level}: ${h.text}`, margin: [(parseInt(h.level.charAt(1)) - 1) * 15, 0, 0, 5]}))\n                    ]}\n                ], columnGap: 20\n            }\n        ]);\n\n        docDefinition.content.push([\n            { text: '3. An\u00e1lisis T\u00e9cnico y de Enlaces', style: 'h2', tocItem: true },\n            {\n                table: {\n                    widths: ['*', 'auto', 'auto'],\n                    body: [\n                        [{text: 'M\u00e9trica', style: 'tableHeader'}, {text: 'Tu Web', style: 'tableHeader', alignment: 'center'}, {text: 'Competidor', style: 'tableHeader', alignment: 'center'}],\n                        ['Total de Enlaces', {text: userData.tech.totalLinks, alignment: 'center'}, {text: competitorData.tech.totalLinks, alignment: 'center'}],\n                        ['Enlaces Internos', {text: userData.tech.internalLinks, alignment: 'center'}, {text: competitorData.tech.internalLinks, alignment: 'center'}],\n                        ['Enlaces Externos', {text: userData.tech.externalLinks, alignment: 'center'}, {text: competitorData.tech.externalLinks, alignment: 'center'}],\n                        ['Total de Im\u00e1genes', {text: userData.tech.totalImages, alignment: 'center'}, {text: competitorData.tech.totalImages, alignment: 'center'}],\n                        ['Im\u00e1genes sin \"alt\"', {text: userData.tech.imagesWithoutAlt, alignment: 'center'}, {text: competitorData.tech.imagesWithoutAlt, alignment: 'center'}]\n                    ]\n                },\n                layout: 'lightHorizontalLines'\n            }\n        ]);\n\n         docDefinition.content.push([\n            { text: '4. An\u00e1lisis de Ventaja Competitiva', style: 'h2', tocItem: true },\n            {\n                table: {\n                    widths: ['*', 'auto', 'auto'],\n                    body: [\n                       [{text: 'Factor', style: 'tableHeader'}, {text: 'Tu Web', style: 'tableHeader', alignment: 'center'}, {text: 'Competidor', style: 'tableHeader', alignment: 'center'}],\n                       ['Datos Estructurados (Schema)', {text: userData.schema.join(', ') || 'No detectado', alignment: 'center'}, {text: competitorData.schema.join(', ') || 'No detectado', alignment: 'center'}],\n                       ['Profundidad (N\u00ba Palabras)', {text: userData.wordCount, alignment: 'center'}, {text: competitorData.wordCount, alignment: 'center'}]\n                    ]\n                },\n                 layout: 'lightHorizontalLines'\n            }\n        ]);\n\n        docDefinition.content.push([\n             { text: '5. An\u00e1lisis de Rendimiento y Velocidad', style: 'h2', tocItem: true },\n             {\n                columns: [\n                    { width: '*', stack: [\n                        { text: 'Rendimiento de tu Web', style: 'h3'},\n                        { table: {\n                            widths: ['*', 'auto', 'auto'],\n                            body: [\n                                [{text: 'M\u00e9trica', style: 'tableHeader'}, {text: 'M\u00f3vil', style: 'tableHeader', alignment: 'center'}, {text: 'Ordenador', style: 'tableHeader', alignment: 'center'}],\n                                ['Puntuaci\u00f3n', {text: userData.performance.mobile.performanceScore || 'N\/A', alignment: 'center'}, {text: userData.performance.desktop.performanceScore || 'N\/A', alignment: 'center'}],\n                                ['FCP', {text: `${(userData.performance.mobile.fcp\/1000).toFixed(2)}s`, alignment: 'center'}, {text: `${(userData.performance.desktop.fcp\/1000).toFixed(2)}s`, alignment: 'center'}],\n                                ['LCP', {text: `${(userData.performance.mobile.lcp\/1000).toFixed(2)}s`, alignment: 'center'}, {text: `${(userData.performance.desktop.lcp\/1000).toFixed(2)}s`, alignment: 'center'}]\n                            ]\n                        }, layout: 'lightHorizontalLines'}\n                    ]},\n                    { width: '*', stack: [\n                        { text: 'Rendimiento del Competidor', style: 'h3'},\n                         { table: {\n                            widths: ['*', 'auto', 'auto'],\n                            body: [\n                                [{text: 'M\u00e9trica', style: 'tableHeader'}, {text: 'M\u00f3vil', style: 'tableHeader', alignment: 'center'}, {text: 'Ordenador', style: 'tableHeader', alignment: 'center'}],\n                                ['Puntuaci\u00f3n', {text: competitorData.performance.mobile.performanceScore || 'N\/A', alignment: 'center'}, {text: competitorData.performance.desktop.performanceScore || 'N\/A', alignment: 'center'}],\n                                ['FCP', {text: `${(competitorData.performance.mobile.fcp\/1000).toFixed(2)}s`, alignment: 'center'}, {text: `${(competitorData.performance.desktop.fcp\/1000).toFixed(2)}s`, alignment: 'center'}],\n                                ['LCP', {text: `${(competitorData.performance.mobile.lcp\/1000).toFixed(2)}s`, alignment: 'center'}, {text: `${(competitorData.performance.desktop.lcp\/1000).toFixed(2)}s`, alignment: 'center'}]\n                            ]\n                        }, layout: 'lightHorizontalLines'}\n                    ]}\n                ], columnGap: 20\n             }\n        ]);\n\n        const createSocialCard = (data) => {\n            if (!data.found) return { text: 'No se encontraron metaetiquetas sociales.', style: 'p' };\n            return {\n                stack: [\n                    { text: data.siteName || 'SITENAME.COM', bold: true, fontSize: 8, color: '#6b7280', margin: [0, 0, 0, 2] },\n                    { text: data.title || 'T\u00edtulo no encontrado', bold: true },\n                    { text: data.description || 'Descripci\u00f3n no encontrada.', fontSize: 9, margin: [0, 2, 0, 0] }\n                ],\n                margin: [10, 10, 10, 10]\n            };\n        };\n         docDefinition.content.push([\n            { text: '6. An\u00e1lisis de Redes Sociales', style: 'h2', tocItem: true },\n             {\n                columns: [\n                    { width: '*', stack: [\n                        { text: 'Previsualizaci\u00f3n de tu Web', style: 'h3'},\n                        createSocialCard(userData.social)\n                    ]},\n                    { width: '*', stack: [\n                        { text: 'Previsualizaci\u00f3n del Competidor', style: 'h3'},\n                        createSocialCard(competitorData.social)\n                    ]}\n                ], columnGap: 20\n             }\n        ]);\n\n        docDefinition.content.push([\n            { text: '7. An\u00e1lisis de Indexabilidad', style: 'h2', tocItem: true },\n             { table: {\n                widths: ['*', 'auto', 'auto'],\n                body: [\n                    [{text: 'Directiva', style: 'tableHeader'}, {text: 'Tu Web', style: 'tableHeader'}, {text: 'Competidor', style: 'tableHeader'}],\n                    ['Meta Robots', userData.crawlability?.metaRobots || 'N\/A', competitorData.crawlability?.metaRobots || 'N\/A'],\n                    ['URL Can\u00f3nica', userData.crawlability?.canonical || 'N\/A', competitorData.crawlability?.canonical || 'N\/A'],\n                    ['robots.txt', userData.crawlability?.robotsTxt ? 'Encontrado' : 'No Encontrado', competitorData.crawlability?.robotsTxt ? 'Encontrado' : 'No Encontrado'],\n                    ['sitemap.xml', userData.crawlability?.sitemapXml ? 'Encontrado' : 'No Encontrado', competitorData.crawlability?.sitemapXml ? 'Encontrado' : 'No Encontrado']\n                ]\n            }, layout: 'lightHorizontalLines'}\n        ]);\n        \n        docDefinition.content.push([\n            { text: '8. An\u00e1lisis de Seguridad', style: 'h2', tocItem: true },\n            { table: {\n                widths: ['*', 'auto', 'auto'],\n                body: [\n                     [{text: 'Factor', style: 'tableHeader'}, {text: 'Tu Web', style: 'tableHeader'}, {text: 'Competidor', style: 'tableHeader'}],\n                     ['Conexi\u00f3n Segura (HTTPS)', userData.security?.isSecure ? 'S\u00ed' : 'No', competitorData.security?.isSecure ? 'S\u00ed' : 'No'],\n                     ['Contenido Mixto', userData.security?.mixedContentCount, competitorData.security?.mixedContentCount]\n                ]\n            }, layout: 'lightHorizontalLines'}\n        ]);\n        \n        pdfMake.createPdf(docDefinition).download(filename);\n    }\n\n\n    \/\/ --- Mode Switching ---\n    function switchMode(mode) {\n        analysisMode = mode;\n        pdfControls.classList.add('hidden'); \/\/ Hide on mode switch\n        if (mode === 'page') {\n            pageAnalysisBtn.classList.add('bg-blue-600', 'text-white');\n            domainAnalysisBtn.classList.remove('bg-blue-600', 'text-white');\n            urlInputLabel.textContent = 'Tu URL';\n            competitorUrlInputLabel.textContent = 'URL del Competidor';\n            urlInput.placeholder = 'tu-web.com\/pagina';\n            competitorUrlInput.placeholder = 'competidor.com\/pagina';\n            analyzeBtnText.textContent = 'Analizar Estrategia de P\u00e1gina';\n        } else {\n            domainAnalysisBtn.classList.add('bg-blue-600', 'text-white');\n            pageAnalysisBtn.classList.remove('bg-blue-600', 'text-white');\n            urlInputLabel.textContent = 'Tu Dominio';\n            competitorUrlInputLabel.textContent = 'Dominio del Competidor';\n            urlInput.placeholder = 'tu-web.com';\n            competitorUrlInput.placeholder = 'competidor.com';\n            analyzeBtnText.textContent = 'Mapear Autoridad Tem\u00e1tica';\n        }\n    }\n\n    \/\/ --- Core Fetching Logic ---\n    function normalizeUrl(url) {\n        if (!url) return '';\n        url = url.trim();\n        if (!\/^(https?:\\\/\\\/)\/.test(url)) {\n            return 'https:\/\/' + url;\n        }\n        return url;\n    }\n\n    async function fetchAndParseUrl(url) {\n        if (!url) return null;\n        const normalizedUrl = normalizeUrl(url);\n        try {\n            const proxyUrl = `https:\/\/corsproxy.io\/?${encodeURIComponent(normalizedUrl)}`;\n            const response = await fetch(proxyUrl);\n            if (!response.ok) throw new Error(`Proxy error for ${url} (Status: ${response.status}).`);\n            const html = await response.text();\n            if (!html) throw new Error(`Could not fetch content for ${url}. The response from the proxy was empty.`);\n            const parser = new DOMParser();\n            return { doc: parser.parseFromString(html, 'text\/html'), finalUrl: normalizedUrl };\n        } catch (err) {\n            throw err;\n        }\n    }\n    \n    async function fetchPageSpeedData(url, apiKey) {\n        const strategies = ['mobile', 'desktop'];\n        const results = {};\n        for (const strategy of strategies) {\n            try {\n                let apiUrl = `https:\/\/www.googleapis.com\/pagespeedonline\/v5\/runPagespeed?url=${encodeURIComponent(url)}&strategy=${strategy}&category=PERFORMANCE`;\n                if (apiKey) {\n                    apiUrl += `&key=${apiKey}`;\n                }\n                const response = await fetch(apiUrl);\n                if (!response.ok) {\n                    if (response.status === 429) {\n                         results[strategy] = { error: 'L\u00edmite de API alcanzado. Usa una API Key o espera un minuto.' };\n                    } else {\n                         results[strategy] = { error: `HTTP error ${response.status}` };\n                    }\n                    continue;\n                }\n                const data = await response.json();\n                if (data.error) {\n                    results[strategy] = { error: 'La API de Google devolvi\u00f3 un error.' };\n                    continue;\n                }\n                \n                const performanceScore = Math.round((data.lighthouseResult.categories.performance.score || 0) * 100);\n                const audits = data.lighthouseResult.audits;\n                const fcp = audits['first-contentful-paint']?.numericValue || 0;\n                const lcp = audits['largest-contentful-paint']?.numericValue || 0;\n                \n                results[strategy] = { performanceScore, fcp, lcp };\n\n            } catch (e) {\n                results[strategy] = { error: 'La petici\u00f3n a la API fall\u00f3.' };\n            }\n        }\n        return results;\n    }\n\n    \/\/ --- Shared Analysis Functions ---\n    function getDomainName(url) {\n        try { return new URL(normalizeUrl(url)).hostname.replace(\/^www\\.\/, '').split('.')[0]; } \n        catch (e) { return ''; }\n    }\n    \n    \/\/ --- PAGE ANALYSIS MODE ---\n    async function analyzePageStrategy() {\n        const userUrl = urlInput.value;\n        const competitorUrl = competitorUrlInput.value;\n        const apiKey = apiKeyInput.value.trim();\n        if (!userUrl || !competitorUrl) {\n            showError(\"Por favor, introduce ambas URLs para la comparaci\u00f3n.\");\n            return;\n        }\n        setupAnalysisUI(\"Analizando contenido, estructura y rendimiento...\");\n\n        try {\n            const [\n                userResult, \n                competitorResult, \n                userPerfResult, \n                competitorPerfResult\n            ] = await Promise.allSettled([ \n                fetchAndParseUrl(userUrl), \n                fetchAndParseUrl(competitorUrl),\n                fetchPageSpeedData(normalizeUrl(userUrl), apiKey),\n                fetchPageSpeedData(normalizeUrl(competitorUrl), apiKey)\n            ]);\n\n            handlePromiseErrors(userResult, competitorResult, userUrl, competitorUrl);\n\n            const { doc: userDoc, finalUrl: finalUserUrl } = userResult.value;\n            const { doc: competitorDoc, finalUrl: finalCompetitorUrl } = competitorResult.value;\n            \n            const [userCrawlResult, competitorCrawlResult] = await Promise.allSettled([\n                extractCrawlabilityData(userDoc, finalUserUrl),\n                extractCrawlabilityData(competitorDoc, finalCompetitorUrl)\n            ]);\n\n            const userDomain = getDomainName(finalUserUrl);\n            const competitorDomain = getDomainName(finalCompetitorUrl);\n            const noiseForAll = ['inicio', 'contacto', 'tienda', 'blog', 'productos', 'servicios', 'nosotros', 'comprar', 'precio', 'oferta', 'env\u00edo', 'gratis', 'ver', 'm\u00e1s', 'informaci\u00f3n', 'aviso', 'legal', 'pol\u00edtica', 'privacidad', 'cookies', 'carrito', 'cuenta', 'mi', 'cesta', 'buscar', 'home', 'kave', 'mobles', 'alvarez', userDomain, competitorDomain];\n\n            const userMainTextData = extractMainText(userDoc);\n            const competitorMainTextData = extractMainText(competitorDoc);\n\n            const userPerformanceData = userPerfResult.status === 'fulfilled' ? userPerfResult.value : { mobile: { error: 'Analysis failed' }, desktop: { error: 'Analysis failed' } };\n            const competitorPerformanceData = competitorPerfResult.status === 'fulfilled' ? competitorPerfResult.value : { mobile: { error: 'Analysis failed' }, desktop: { error: 'Analysis failed' } };\n\n            const userData = {\n                wordCount: userMainTextData.wordCount,\n                keywords: getKeywords(userMainTextData.text, noiseForAll),\n                headings: extractHeadings(userDoc),\n                tech: extractTechnicalData(userDoc, finalUserUrl),\n                schema: extractSchema(userDoc),\n                performance: userPerformanceData,\n                social: extractSocialData(userDoc, finalUserUrl),\n                crawlability: userCrawlResult.status === 'fulfilled' ? userCrawlResult.value : null,\n                security: extractSecurityData(userDoc, finalUserUrl)\n            };\n            const competitorData = {\n                wordCount: competitorMainTextData.wordCount,\n                keywords: getKeywords(competitorMainTextData.text, noiseForAll),\n                headings: extractHeadings(competitorDoc),\n                tech: extractTechnicalData(competitorDoc, finalCompetitorUrl),\n                schema: extractSchema(competitorDoc),\n                performance: competitorPerformanceData,\n                social: extractSocialData(competitorDoc, finalCompetitorUrl),\n                crawlability: competitorCrawlResult.status === 'fulfilled' ? competitorCrawlResult.value : null,\n                security: extractSecurityData(competitorDoc, finalCompetitorUrl)\n            };\n            \n            latestAnalysisData = { userData, competitorData };\n            \n            resultsContainer.innerHTML = ''; \n            displayExecutiveSummary(userData, competitorData);\n            displayContentAnalysis(userData, competitorData);\n            displayStructureAnalysis(userData.headings, competitorData.headings);\n            displayTechnicalAnalysis(userData.tech, competitorData.tech);\n            displayCompetitiveAdvantage(userData, competitorData);\n            displayPerformanceAnalysis(userData, competitorData);\n            displaySocialAnalysis(userData, competitorData);\n            displayCrawlabilityAnalysis(userData, competitorData);\n            displaySecurityAnalysis(userData, competitorData);\n\n            pdfControls.classList.remove('hidden');\n            \n        } catch (err) {\n            showError(err.message);\n        } finally {\n            teardownAnalysisUI();\n        }\n    }\n\n    \/\/ --- DOMAIN ANALYSIS MODE ---\n    async function analyzeDomainStrategy() {\n        const userDomain = urlInput.value;\n        const competitorDomain = competitorUrlInput.value;\n         if (!userDomain || !competitorDomain) {\n            showError(\"Por favor, introduce ambos dominios para la comparaci\u00f3n.\");\n            return;\n        }\n        setupAnalysisUI(\"Rastreando dominios... Esto puede tardar un minuto.\");\n\n        try {\n            const [userPages, competitorPages] = await Promise.all([\n                crawlSite(userDomain),\n                crawlSite(competitorDomain)\n            ]);\n\n            const topicKeywords = ['sal\u00f3n', 'dormitorio', 'cocina', 'comedor', 'oficina', 'jard\u00edn', 'decoraci\u00f3n', 'iluminaci\u00f3n', 'textil', 'sof\u00e1', 'mesa', 'silla', 'cama', 'armario', 'infantil', 'juvenil'];\n            const userClusters = clusterTopics(userPages, topicKeywords);\n            const competitorClusters = clusterTopics(competitorPages, topicKeywords);\n\n            const userDomainName = getDomainName(userDomain);\n            const competitorDomainName = getDomainName(competitorDomain);\n            \n            latestAnalysisData = { userClusters, competitorClusters, userDomainName, competitorDomainName };\n\n            displayThematicMap(userClusters, competitorClusters, userDomainName, competitorDomainName);\n\n        } catch (err) {\n            showError(err.message);\n        } finally {\n            teardownAnalysisUI();\n        }\n    }\n    \n    async function crawlSite(domain) {\n        const MAX_PAGES_TO_CRAWL = 10;\n        const entryUrl = normalizeUrl(domain);\n        const { doc: homepageDoc } = await fetchAndParseUrl(entryUrl);\n        \n        const internalLinks = new Set();\n        const baseUrl = new URL(entryUrl);\n        homepageDoc.querySelectorAll('a[href]').forEach(a => {\n            try {\n                const href = a.getAttribute('href');\n                if (!href || href.startsWith('#')) return;\n                const absoluteUrl = new URL(href, baseUrl.href).href.split('#')[0];\n                if (new URL(absoluteUrl).hostname === baseUrl.hostname) {\n                    internalLinks.add(absoluteUrl);\n                }\n            } catch (e) { \/* ignore invalid URLs *\/ }\n        });\n        \n        const pagesToFetch = [entryUrl, ...Array.from(internalLinks).slice(0, MAX_PAGES_TO_CRAWL -1)];\n        const results = await Promise.allSettled(pagesToFetch.map(url => fetchAndParseUrl(url)));\n        \n        return results\n            .filter(r => r.status === 'fulfilled' && r.value.doc)\n            .map(r => ({\n                url: r.value.finalUrl,\n                title: r.value.doc.title,\n                h1: r.value.doc.querySelector('h1')?.innerText || ''\n            }));\n    }\n\n    function clusterTopics(pages, topicKeywords) {\n        const clusters = {};\n        topicKeywords.forEach(kw => clusters[kw] = []);\n\n        pages.forEach(page => {\n            const content = `${page.title.toLowerCase()} ${page.h1.toLowerCase()}`;\n            topicKeywords.forEach(kw => {\n                if (content.includes(kw)) {\n                    clusters[kw].push(page);\n                }\n            });\n        });\n        \n        return Object.entries(clusters)\n            .filter(([, pages]) => pages.length > 0)\n            .sort(([, a], [, b]) => b.length - a.length);\n    }\n    \n    \/\/ --- Helper functions for analysis ---\n    function setupAnalysisUI(text = \"\") {\n        resultsContainer.innerHTML = '';\n        errorContainer.classList.add('hidden');\n        pdfControls.classList.add('hidden');\n        loader.classList.remove('hidden');\n        if (text) {\n            loaderText.textContent = text;\n            loaderText.classList.remove('hidden');\n        }\n        analyzeBtn.disabled = true;\n        analyzeBtn.classList.add('opacity-50', 'cursor-not-allowed');\n    }\n\n    function teardownAnalysisUI() {\n        loader.classList.add('hidden');\n        loaderText.classList.add('hidden');\n        analyzeBtn.disabled = false;\n        analyzeBtn.classList.remove('opacity-50', 'cursor-not-allowed');\n    }\n\n    function handlePromiseErrors(userResult, competitorResult, userUrl, competitorUrl) {\n         const errors = [];\n        if (userResult.status === 'rejected') errors.push(`Error al analizar tu URL (${userUrl}): ${userResult.reason.message}`);\n        if (competitorResult.status === 'rejected') errors.push(`Error al analizar la URL del competidor (${competitorUrl}): ${competitorResult.reason.message}`);\n        if (errors.length > 0) throw new Error(errors.join(' | '));\n    }\n\n    function showError(message) {\n        errorContainer.textContent = message;\n        errorContainer.classList.remove('hidden');\n    }\n    \n    function extractMainText(doc) {\n        if (!doc) return { text: '', wordCount: 0 };\n        const contentNode = doc.body.cloneNode(true);\n        contentNode.querySelectorAll('script, style, nav, footer, aside, header').forEach(el => el.remove());\n        const mainContent = contentNode.querySelector('main') || contentNode.querySelector('article') || contentNode;\n        const text = mainContent ? mainContent.innerText.trim() : '';\n        const wordCount = text.split(\/\\s+\/).filter(Boolean).length;\n        return { text, wordCount };\n    }\n\n    function getKeywords(text, customNoiseWords = []) {\n        const spanishStopWords = new Set(['de', 'la', 'el', 'en', 'y', 'a', 'los', 'las', 'un', 'una', 'con', 'por', 'para', 'su', 'es', 'que', 'al', 'del', 'se', 'no', 'o', 'lo', 'como', 'm\u00e1s', 'pero', 'sus', 'le', 'ya', 'ha', 'muy', 'mi', 'sin', 'sobre', 'este', 'esta', 'estos', 'estas', 'ser', 'estar', 'nuestro', 'nuestra', 'desde', 'cuando', 'donde', 'quien', 'cual', 'son', 'tus', 'porque', 'entre', 'eso', 'esa', 'esos', 'esas', 'otro', 'otra', 'otros', 'otras', 'hasta', 'todo', 'todos', 'toda', 'todas']);\n        const noiseWords = new Set(customNoiseWords.map(w => w.toLowerCase()));\n\n        const words = text.toLowerCase().replace(\/[^\\w\\s\u00c1-\u00fa]\/g, '').split(\/\\s+\/).filter(word => \n            word.length > 3 && !spanishStopWords.has(word) && !noiseWords.has(word)\n        );\n\n        const frequency = {};\n        words.forEach(word => { frequency[word] = (frequency[word] || 0) + 1; });\n\n        for (let i = 0; i < words.length - 2; i++) {\n            const bigram = `${words[i]} ${words[i+1]}`;\n            if (bigram.split(' ').some(part => noiseWords.has(part))) continue;\n            frequency[bigram] = (frequency[bigram] || 0) + 1;\n            \n            const trigram = `${bigram} ${words[i+2]}`;\n             if (trigram.split(' ').some(part => noiseWords.has(part))) continue;\n            frequency[trigram] = (frequency[trigram] || 0) + 1;\n        }\n        return Object.entries(frequency).sort(([, a], [, b]) => b - a).slice(0, 15);\n    }\n\n    function extractHeadings(doc) {\n        if (!doc) return [];\n        const headings = [];\n        doc.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(h => {\n            if (h.innerText.trim()) {\n                headings.push({ level: h.tagName.toUpperCase(), text: h.innerText.trim() });\n            }\n        });\n        return headings;\n    }\n    \n    function extractSchema(doc) {\n        if (!doc) return [];\n        const schemas = [];\n        doc.querySelectorAll('script[type=\"application\/ld+json\"]').forEach(script => {\n            try {\n                const json = JSON.parse(script.textContent);\n                const types = Array.isArray(json['@type']) ? json['@type'] : [json['@type']];\n                types.forEach(type => {\n                    if (type && !schemas.includes(type)) {\n                        schemas.push(type);\n                    }\n                });\n            } catch (e) { \/* Ignore parsing errors *\/ }\n        });\n        return schemas;\n    }\n\n    function extractTechnicalData(doc, url) {\n        if (!doc) return { internalLinks: 0, externalLinks: 0, totalLinks: 0, totalImages: 0, imagesWithoutAlt: 0 };\n        let internalLinks = 0, externalLinks = 0;\n        const pageUrl = new URL(url);\n        const pageHostname = pageUrl.hostname;\n        doc.querySelectorAll('a[href]').forEach(a => {\n            try {\n                const href = a.getAttribute('href');\n                if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return;\n                const linkUrl = new URL(href, pageUrl.href);\n                if (linkUrl.hostname === pageHostname) internalLinks++;\n                else externalLinks++;\n            } catch (e) { \/* Invalid URL *\/ }\n        });\n        const totalImages = doc.querySelectorAll('img').length;\n        const imagesWithoutAlt = Array.from(doc.querySelectorAll('img')).filter(img => !img.alt || img.alt.trim() === '').length;\n        return { internalLinks, externalLinks, totalLinks: internalLinks + externalLinks, totalImages, imagesWithoutAlt };\n    }\n\n    function extractSocialData(doc, baseUrl) {\n        if (!doc) return { found: false };\n        const getMeta = (prop) => doc.querySelector(`meta[property=\"${prop}\"], meta[name=\"${prop}\"]`)?.getAttribute('content') || null;\n        const data = {\n            ogTitle: getMeta('og:title'),\n            ogDescription: getMeta('og:description'),\n            ogImage: getMeta('og:image'),\n            ogSiteName: getMeta('og:site_name'),\n            twitterCard: getMeta('twitter:card'),\n            twitterTitle: getMeta('twitter:title'),\n            twitterDescription: getMeta('twitter:description'),\n            twitterImage: getMeta('twitter:image'),\n        };\n\n        let imageUrl = data.ogImage || data.twitterImage;\n        if (imageUrl && baseUrl) {\n            try {\n                imageUrl = new URL(imageUrl, baseUrl).href;\n            } catch (e) { \/* malformed url, let it be *\/ }\n        }\n        \n        const finalData = {\n            title: data.ogTitle || data.twitterTitle,\n            description: data.ogDescription || data.twitterDescription,\n            image: imageUrl,\n            siteName: data.ogSiteName,\n            cardType: data.twitterCard,\n        };\n        finalData.found = !!(finalData.title && finalData.description && finalData.image);\n        return finalData;\n    }\n\n    async function extractCrawlabilityData(doc, url) {\n        if (!doc) return null;\n        const origin = new URL(url).origin;\n\n        const metaRobots = doc.querySelector('meta[name=\"robots\"]')?.getAttribute('content') || 'index, follow';\n        const canonical = doc.querySelector('link[rel=\"canonical\"]')?.getAttribute('href') || 'No especificada';\n\n        const checkFile = async (fileUrl) => {\n            try {\n                const proxyUrl = `https:\/\/corsproxy.io\/?${encodeURIComponent(fileUrl)}`;\n                const response = await fetch(proxyUrl);\n                return response.ok;\n            } catch (e) {\n                return false;\n            }\n        };\n\n        const [robotsTxtExists, sitemapXmlExists] = await Promise.all([\n            checkFile(`${origin}\/robots.txt`),\n            checkFile(`${origin}\/sitemap.xml`)\n        ]);\n\n        return {\n            metaRobots,\n            canonical,\n            robotsTxt: robotsTxtExists,\n            sitemapXml: sitemapXmlExists\n        };\n    }\n\n    function extractSecurityData(doc, url) {\n        if (!doc) return null;\n        const isSecure = url.startsWith('https:\/\/');\n        let mixedContentCount = 0;\n        if (isSecure) {\n            doc.querySelectorAll('img[src^=\"http:\"], script[src^=\"http:\"], link[href^=\"http:\"]').forEach(() => {\n                mixedContentCount++;\n            });\n        }\n        return { isSecure, mixedContentCount };\n    }\n    \n    function getHowToInstructions(actionType) {\n        const instructions = {\n            SCHEMA_MISSING: {\n                rankMath: `<ol class=\"list-decimal pl-5 space-y-2\"><li>Edita la p\u00e1gina o entrada en WordPress.<\/li><li>En el editor de bloques, busca el bot\u00f3n de Rank Math en la esquina superior derecha y haz clic en la pesta\u00f1a \"Schema\".<\/li><li>Haz clic en \"Schema Generator\" y elige el tipo de Schema adecuado (ej. \"Art\u00edculo\", \"Producto\", \"FAQ\").<\/li><li>Rellena los campos que te pida el asistente (t\u00edtulo, descripci\u00f3n, SKU, etc.).<\/li><li>Guarda el Schema y actualiza la p\u00e1gina.<\/li><\/ol>`,\n                yoast: `<ol class=\"list-decimal pl-5 space-y-2\"><li>Con Yoast SEO Premium, puedes a\u00f1adir bloques de Schema estructurados (como FAQ o How-to) directamente desde el editor de bloques de WordPress.<\/li><li>Busca el bloque de Yoast que necesites (ej. \"Yoast FAQ\") y a\u00f1\u00e1delo a tu contenido.<\/li><li>Rellena las preguntas y respuestas. Yoast se encargar\u00e1 de generar el c\u00f3digo Schema correcto.<\/li><li>Para otros tipos de Schema (como Producto), Yoast a menudo lo integra autom\u00e1ticamente si usas plugins como WooCommerce.<\/li><\/ol>`\n            },\n            KEYWORD_GAP: {\n                rankMath: `<ol class=\"list-decimal pl-5 space-y-2\"><li>Edita la p\u00e1gina que quieres optimizar.<\/li><li>En la caja de meta de Rank Math, introduce la frase clave de oportunidad en el campo \"Palabra Clave Objetivo\".<\/li><li>Rank Math analizar\u00e1 tu contenido y te dar\u00e1 una puntuaci\u00f3n y recomendaciones sobre c\u00f3mo y d\u00f3nde incluir esa palabra clave (en el t\u00edtulo, encabezados, contenido, etc.).<\/li><li>Sigue las recomendaciones para mejorar tu puntuaci\u00f3n.<\/li><\/ol>`,\n                yoast: `<ol class=\"list-decimal pl-5 space-y-2\"><li>Edita la p\u00e1gina en WordPress.<\/li><li>Busca la caja de meta de Yoast SEO debajo del editor de contenido.<\/li><li>Introduce la frase clave de oportunidad en el campo \"Frase clave objetivo\".<\/li><li>Yoast evaluar\u00e1 tu p\u00e1gina y te mostrar\u00e1 los resultados del an\u00e1lisis con puntos verdes, naranjas o rojos.<\/li><li>Abre la secci\u00f3n de \"An\u00e1lisis SEO\" y sigue las sugerencias para integrar la frase clave de forma natural en tu texto.<\/li><\/ol>`\n            },\n            ALT_TAG_ISSUE: {\n                rankMath: `<ol class=\"list-decimal pl-5 space-y-2\"><li>Ve a la \"Biblioteca de Medios\" de WordPress.<\/li><li>Localiza una de las im\u00e1genes sin texto 'alt'. Puedes cambiar a la vista de lista para ver m\u00e1s informaci\u00f3n.<\/li><li>Haz clic en la imagen para ver los detalles del archivo adjunto.<\/li><li>Rellena el campo \"Texto Alternativo\" con una descripci\u00f3n breve y precisa de lo que muestra la imagen. Idealmente, incluye tu palabra clave si es natural.<\/li><li>Los cambios se guardan autom\u00e1ticamente. Repite el proceso para todas las im\u00e1genes.<\/li><\/ol>`,\n                yoast: `<ol class=\"list-decimal pl-5 space-y-2\"><li>El proceso es el mismo que con Rank Math, ya que esto se gestiona desde el n\u00facleo de WordPress.<\/li><li>Ve a la \"Biblioteca de Medios\".<\/li><li>Haz clic en la imagen que quieres editar.<\/li><li>En la parte derecha, busca el campo \"Texto Alternativo\".<\/li><li>Escribe una descripci\u00f3n concisa de la imagen.<\/li><li>Cierra la ventana de detalles. El cambio se guardar\u00e1.<\/li><\/ol>`\n            },\n            H1_ISSUE: {\n                rankMath: `<ol class=\"list-decimal pl-5 space-y-2\"><li>Edita la p\u00e1gina con el editor de bloques de WordPress.<\/li><li>Haz clic en el t\u00edtulo principal de tu p\u00e1gina. Generalmente es el primer bloque de texto.<\/li><li>En la barra de herramientas del bloque, aseg\u00farate de que est\u00e1 configurado como \"Encabezado\" y que el nivel es \"H1\".<\/li><li>Revisa el resto de tu contenido y aseg\u00farate de que ning\u00fan otro bloque de encabezado est\u00e9 configurado como H1. C\u00e1mbialos a H2, H3, etc., seg\u00fan la jerarqu\u00eda.<\/li><li>Actualiza la p\u00e1gina.<\/li><\/ol>`,\n                yoast: `<ol class=\"list-decimal pl-5 space-y-2\"><li>La gesti\u00f3n de encabezados es una funci\u00f3n de WordPress, no del plugin de SEO. El proceso es id\u00e9ntico.<\/li><li>Abre la p\u00e1gina en el editor de WordPress.<\/li><li>Selecciona el t\u00edtulo que deber\u00eda ser tu \u00fanico H1. En la barra de herramientas del bloque, verifica que es un \"Encabezado\" de nivel \"H1\".<\/li><li>Busca cualquier otro encabezado H1 y degr\u00e1dalo a un nivel inferior (H2, por ejemplo) usando la misma barra de herramientas.<\/li><li>Guarda los cambios.<\/li><\/ol>`\n            },\n            INTERNAL_LINKING: {\n                 rankMath: `<ol class=\"list-decimal pl-5 space-y-2\"><li>Mientras editas una entrada o p\u00e1gina, Rank Math te ofrece \"Sugerencias de Enlaces\" en la barra lateral.<\/li><li>Analizar\u00e1 tu contenido y te sugerir\u00e1 otras p\u00e1ginas de tu web que son relevantes y que podr\u00edas enlazar.<\/li><li>Simplemente copia el enlace sugerido y a\u00f1\u00e1delo en el texto ancla (anchor text) que consideres m\u00e1s apropiado.<\/li><li>Tambi\u00e9n, al a\u00f1adir un enlace manualmente, el buscador de WordPress te permite buscar entradas por t\u00edtulo para enlazar f\u00e1cilmente.<\/li><\/ol>`,\n                yoast: `<ol class=\"list-decimal pl-5 space-y-2\"><li>Yoast SEO Premium incluye una herramienta de \"Sugerencias de enlazado interno\".<\/li><li>Mientras escribes, Yoast analizar\u00e1 tu texto y te mostrar\u00e1 una lista de entradas relacionadas de tu web en la barra lateral.<\/li><li>Puedes copiar la URL o arrastrar el enlace directamente a tu texto para crear el enlace interno.<\/li><li>Sin la versi\u00f3n premium, puedes a\u00f1adir enlaces manualmente seleccionando texto, haciendo clic en el icono de enlace y buscando por t\u00edtulo la p\u00e1gina que quieres enlazar.<\/li><\/ol>`\n            }\n        };\n        return instructions[actionType] || null;\n    }\n    \n    function getExecutiveSummaryData(userData, competitorData) {\n        const actions = [];\n        const userKeywordSet = new Set(userData.keywords.map(([kw]) => kw));\n        const keywordGaps = competitorData.keywords.filter(([kw]) => !userKeywordSet.has(kw) && kw.split(' ').length > 1).slice(0, 2);\n        const schemaAdvantage = competitorData.schema.length > 0 && userData.schema.length === 0;\n        const contentDepthAdvantage = competitorData.wordCount > userData.wordCount * 1.25;\n        const internalLinkAdvantage = competitorData.tech.internalLinks > (userData.tech.internalLinks + 5);\n        const userAltTagIssueCount = userData.tech.imagesWithoutAlt;\n        const userH1Count = userData.headings.filter(h => h.level === 'H1').length;\n        const userH1Issue = userH1Count !== 1;\n\n        if (schemaAdvantage) actions.push({ type: 'SCHEMA_MISSING', priority: 1 });\n        if (keywordGaps.length > 0) actions.push({ type: 'KEYWORD_GAP', priority: 1, details: keywordGaps[0][0] });\n        if (userAltTagIssueCount > 0) actions.push({ type: 'ALT_TAG_ISSUE', priority: 2, details: userAltTagIssueCount });\n        if (userH1Issue) actions.push({ type: 'H1_ISSUE', priority: 2, details: userH1Count });\n        if (contentDepthAdvantage) actions.push({ type: 'CONTENT_DEPTH', priority: 3, details: { user: userData.wordCount, comp: competitorData.wordCount } });\n        if (internalLinkAdvantage) actions.push({ type: 'INTERNAL_LINKING', priority: 3 });\n        \n        const sortedActions = actions.sort((a, b) => a.priority - b.priority);\n        \n        let verdict, contentParagraph, techParagraph;\n        const userTechScore = (userData.schema.length > 0 ? 1 : 0) + (userH1Issue ? 0 : 1) + (userAltTagIssueCount === 0 ? 1 : 0);\n        const competitorTechScore = (competitorData.schema.length > 0 ? 1 : 0) + (competitorData.headings.filter(h => h.level === 'H1').length === 1 ? 1 : 0) + (competitorData.tech.imagesWithoutAlt === 0 ? 1 : 0);\n        \n        if (userTechScore > competitorTechScore) {\n            verdict = 'Tu web presenta una base t\u00e9cnica m\u00e1s s\u00f3lida, pero existen claras oportunidades para mejorar tu estrategia de contenido y superar a tu competidor.';\n        } else if (competitorTechScore > userTechScore) {\n            verdict = 'Tu competidor tiene una ventaja t\u00e9cnica, especialmente en \u00e1reas que mejoran la visibilidad en Google. Sin embargo, atacando estos puntos d\u00e9biles y potenciando tu contenido, puedes revertir la situaci\u00f3n.';\n        } else {\n            verdict = 'Ambas webs est\u00e1n en un punto de partida similar. La victoria se decidir\u00e1 por los detalles: la profundidad del contenido y la optimizaci\u00f3n de elementos t\u00e9cnicos clave.';\n        }\n\n        if (contentDepthAdvantage) {\n            contentParagraph = `En el \u00e1mbito del contenido, tu competidor presenta una p\u00e1gina con un <strong>${Math.round((competitorData.wordCount \/ userData.wordCount - 1) * 100)}% m\u00e1s de texto<\/strong> (${competitorData.wordCount} vs ${userData.wordCount} palabras). Esto puede indicar a Google que su p\u00e1gina responde de forma m\u00e1s completa a la intenci\u00f3n de b\u00fasqueda del usuario. `;\n        } else {\n            contentParagraph = `En cuanto a la profundidad del contenido, ambas p\u00e1ginas son comparables en longitud. La clave para diferenciarte estar\u00e1 en la calidad y en el enfoque de las palabras clave. `;\n        }\n        if (keywordGaps.length > 0) {\n            contentParagraph += `Hemos detectado que tu rival est\u00e1 posicionando para t\u00e9rminos estrat\u00e9gicos como <strong>\"${keywordGaps.map(([kw]) => kw).join('\"<\/strong> y <strong>\"')}\"<\/strong>, los cuales no tienen una presencia significativa en tu p\u00e1gina. Integrar estas ideas en tu contenido podr\u00eda atraer tr\u00e1fico relevante que ahora mismo est\u00e1s perdiendo.`;\n        }\n\n        if (schemaAdvantage) {\n            techParagraph = `T\u00e9cnicamente, la diferencia m\u00e1s notable es el uso de <strong>Datos Estructurados (Schema)<\/strong> por parte de tu competidor. Este c\u00f3digo \"oculto\" le permite a Google mostrar resultados enriquecidos (como estrellas o precios), d\u00e1ndole una visibilidad extra en los resultados de b\u00fasqueda que t\u00fa no tienes. `;\n        } else if (userData.schema.length > 0 && competitorData.schema.length === 0) {\n            techParagraph = `\u00a1Excelente trabajo en el apartado t\u00e9cnico! El uso de <strong>Datos Estructurados (Schema)<\/strong> te da una ventaja competitiva importante, permiti\u00e9ndote optar a resultados enriquecidos que tu competidor no puede alcanzar. `;\n        } else {\n            techParagraph = `En el apartado t\u00e9cnico, ninguna de las dos webs est\u00e1 aprovechando los <strong>Datos Estructurados (Schema)<\/strong>, una gran oportunidad perdida para destacar en Google. `;\n        }\n        if (userH1Issue || userAltTagIssueCount > 0) {\n            techParagraph += `Adem\u00e1s, hemos encontrado \u00e1reas de mejora en tu propia p\u00e1gina: ${userH1Issue ? `la estructura de encabezados H1 no es la \u00f3ptima` : ''}${userH1Issue && userAltTagIssueCount > 0 ? ' y ' : ''}${userAltTagIssueCount > 0 ? `tienes <strong>${userAltTagIssueCount} im\u00e1genes sin texto alternativo<\/strong>` : ''}. Corregir estos puntos es fundamental para una base SEO s\u00f3lida.`\n        }\n\n        return { verdict, contentParagraph, techParagraph, sortedActions };\n    }\n    \n    function getActionItemText(action) {\n        let text;\n        switch(action.type) {\n            case 'SCHEMA_MISSING':\n                text = \"<strong>Implementa Datos Estructurados (Schema):<\/strong> Tu competidor te saca ventaja aqu\u00ed. \u00dasalos para optar a resultados enriquecidos y aumentar tu visibilidad.\";\n                break;\n            case 'KEYWORD_GAP':\n                text = `<strong>Ataca los \"Keyword Gaps\":<\/strong> Tu rival usa frases como <strong>\"${action.details}\"<\/strong> que t\u00fa no. Int\u00e9gralas en tu contenido para captar m\u00e1s tr\u00e1fico.`;\n                break;\n            case 'ALT_TAG_ISSUE':\n                text = `<strong>Optimiza tus Im\u00e1genes:<\/strong> Tienes <strong>${action.details}<\/strong> im\u00e1genes sin texto 'alt'. A\u00f1adir descripciones mejorar\u00e1 tu SEO y la accesibilidad.`;\n                break;\n            case 'H1_ISSUE':\n                text = `<strong>Corrige la Jerarqu\u00eda de Encabezados:<\/strong> Tu p\u00e1gina tiene <strong>${action.details}<\/strong> etiquetas H1. Lo ideal es tener solo una para definir el tema principal.`;\n                break;\n            case 'INTERNAL_LINKING':\n                text = `<strong>Refuerza tu Enlazado Interno:<\/strong> Tu competidor te supera. Enlaza m\u00e1s entre tus p\u00e1ginas para mejorar la navegaci\u00f3n y distribuir la autoridad.`;\n                break;\n            case 'CONTENT_DEPTH':\n                text = `<strong>Aumenta la Profundidad del Contenido:<\/strong> Tu p\u00e1gina es significativamente m\u00e1s corta (${action.details.user} vs ${action.details.comp} palabras). Ampl\u00eda tu contenido para aportar m\u00e1s valor.`;\n                break;\n            default:\n                text = \"Acci\u00f3n recomendada.\";\n        }\n        return { text };\n    }\n\n    \/\/ --- Display Functions ---\n    function displayExecutiveSummary(userData, competitorData) {\n        const { verdict, contentParagraph, techParagraph, sortedActions } = getExecutiveSummaryData(userData, competitorData);\n        const summaryHtml = `\n         <div class=\"bg-gradient-to-br from-blue-800 to-gray-800 p-8 rounded-xl shadow-lg border border-blue-600\">\n            <h2 class=\"text-3xl font-bold text-white mb-6\">Resumen Ejecutivo<\/h2>\n            <div class=\"space-y-6 text-gray-300 text-lg mb-8\">\n                <p><strong>Veredicto General:<\/strong> ${verdict}<\/p>\n                <p>${contentParagraph}<\/p>\n                <p>${techParagraph}<\/p>\n            <\/div>\n            <div class=\"mt-8 pt-6 border-t border-blue-700\">\n                <h3 class=\"text-2xl font-semibold text-white mb-6\">Plan de Acci\u00f3n Recomendado<\/h3>\n                <div class=\"space-y-6\">\n                    ${sortedActions.length > 0 ? sortedActions.map((action, index) => generateActionItemHTML(action, index)).join('') : '<p class=\"text-gray-400\">\u00a1Felicidades! No se han detectado acciones urgentes.<\/p>'}\n                <\/div>\n            <\/div>\n        <\/div>`;\n        resultsContainer.innerHTML += summaryHtml;\n    }\n\n    function generateActionItemHTML(action, index) {\n        const { text } = getActionItemText(action);\n        const howTo = getHowToInstructions(action.type);\n        \n        return `\n        <div class=\"bg-gray-900\/50 p-4 rounded-lg\">\n            <div class=\"flex items-start\">\n                <span class=\"flex-shrink-0 h-8 w-8 rounded-full bg-blue-500 text-white font-bold text-lg flex items-center justify-center mr-4\">${index + 1}<\/span>\n                <p class=\"text-gray-200 text-base flex-1\">${text}<\/p>\n            <\/div>\n            ${howTo ? `\n            <details class=\"mt-4 ml-12\">\n                <summary class=\"cursor-pointer text-sm font-semibold text-blue-400 hover:text-blue-300\">C\u00f3mo hacerlo en WordPress...<\/summary>\n                <div class=\"details-content mt-4 pl-4 border-l-2 border-gray-700\">\n                    <h4 class=\"text-md font-bold text-gray-200 mb-2\">Con Rank Math SEO<\/h4>\n                    <div class=\"text-sm text-gray-400 space-y-2 mb-4\">${howTo.rankMath}<\/div>\n                    <h4 class=\"text-md font-bold text-gray-200 mb-2\">Con Yoast SEO<\/h4>\n                    <div class=\"text-sm text-gray-400 space-y-2\">${howTo.yoast}<\/div>\n                <\/div>\n            <\/details>\n            ` : ''}\n        <\/div>\n        `;\n    }\n    \n    \/\/ ... other display functions (displayContentAnalysis, etc.) remain largely the same\n    function displayContentAnalysis(userData, competitorData) {\n        const competitorKeywordSet = new Set(competitorData.keywords.map(([kw]) => kw));\n        const userKeywordSet = new Set(userData.keywords.map(([kw]) => kw));\n        const keywordGaps = competitorData.keywords.filter(([kw]) => !userKeywordSet.has(kw) && kw.split(' ').length > 1);\n\n        let resultHtml = `\n        <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-700\">\n            <h2 class=\"text-2xl font-bold text-gray-200 mb-6 flex items-center\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-8 w-8 text-blue-400 mr-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"><\/path><\/svg>1. An\u00e1lisis de Contenido y Palabras Clave<\/h2>\n            <p class=\"text-gray-400 mb-6\">T\u00e9rminos m\u00e1s frecuentes en el contenido principal de cada web. Ayuda a entender el enfoque y a detectar ausencias en tu propia p\u00e1gina.<\/p>\n            <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8 mb-8\">\n                <div>\n                    <h3 class=\"text-xl font-semibold text-gray-300 mb-4 text-center\">T\u00e9rminos Clave en tu Web<\/h3>\n                    <ul class=\"space-y-2\">${generateKeywordList(userData.keywords, competitorKeywordSet)}<\/ul>\n                <\/div>\n                <div>\n                    <h3 class=\"text-xl font-semibold text-gray-300 mb-4 text-center\">T\u00e9rminos Clave del Competidor<\/h3>\n                    <ul class=\"space-y-2\">${generateKeywordList(competitorData.keywords, userKeywordSet)}<\/ul>\n                <\/div>\n            <\/div>\n            <div>\n                 <h3 class=\"text-xl font-semibold text-teal-400 mb-4 flex items-center\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-6 w-6 mr-2\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547a2 2 0 00-.547 1.806l.477 2.387a6 6 0 00.517 3.86l.158.318a6 6 0 00.517 3.86l2.387.477a2 2 0 001.806-.547a2 2 0 00.547-1.806l-.477-2.387a6 6 0 00-.517-3.86l-.158-.318a6 6 0 00-.517-3.86l-2.387-.477zM12 6.5a2.5 2.5 0 110-5 2.5 2.5 0 010 5z\"><\/path><\/svg>Oportunidades (Keyword Gaps)<\/h3>\n                <p class=\"text-gray-400 mb-4\">Frases clave que tu competidor utiliza y que no aparecen de forma prominente en tu web. Considera incluirlas.<\/p>\n                ${keywordGaps.length > 0 ? `<ul class=\"space-y-2\">${keywordGaps.map(([kw, count]) => `<li class=\"bg-gray-700\/50 p-3 rounded-lg flex justify-between items-center\"><span>${kw}<\/span><span class=\"text-xs font-bold text-teal-400 bg-teal-900\/50 px-2 py-1 rounded-full\">${count} usos<\/span><\/li>`).join('')}<\/ul>` : '<p class=\"text-gray-500\">\u00a1Buen trabajo! No se han detectado grandes ausencias de palabras clave.<\/p>'}\n            <\/div>\n        <\/div>`;\n        resultsContainer.innerHTML += resultHtml;\n    }\n\n    function generateKeywordList(keywords, competitorSet) {\n        if (keywords.length === 0) return '<li class=\"text-gray-500 text-center\">No se pudo extraer texto relevante.<\/li>';\n        return keywords.map(([kw, count]) => {\n            const isShared = competitorSet.has(kw);\n            const isPhrase = kw.split(' ').length > 1;\n            const badge = (isShared && isPhrase) ? `<span class=\"text-xs font-medium text-blue-300 ml-2\" title=\"T\u00e9rmino compartido\">Compartido<\/span>` : '';\n            return `<li class=\"bg-gray-700 p-3 rounded-lg flex justify-between items-center\"><span class=\"truncate pr-2\">${kw}${badge}<\/span><span class=\"text-xs font-bold text-gray-300 bg-gray-600 px-2 py-1 rounded-full\">${count}<\/span><\/li>`;\n        }).join('');\n    }\n\n    function displayStructureAnalysis(userHeadings, competitorHeadings) {\n        const userH1Count = userHeadings.filter(h => h.level === 'H1').length;\n        const competitorH1Count = competitorHeadings.filter(h => h.level === 'H1').length;\n        const getH1Feedback = (count) => count === 1 ? '<span class=\"text-green-400\">Correcto (1 H1)<\/span>' : `<span class=\"text-yellow-400\">Atenci\u00f3n (${count} H1s)<\/span>`;\n        const structureHtml = `\n        <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-700\">\n            <h2 class=\"text-2xl font-bold text-gray-200 mb-6 flex items-center\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-8 w-8 text-blue-400 mr-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 12h16M4 18h7\"><\/path><\/svg>2. An\u00e1lisis de Estructura y Jerarqu\u00eda<\/h2>\n            <p class=\"text-gray-400 mb-6\">Una buena estructura de encabezados (H1, H2...) ayuda a Google a entender tu contenido.<\/p>\n            <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n                <div>\n                    <h3 class=\"text-xl font-semibold text-gray-300 mb-4 text-center\">Estructura de tu Web<\/h3>\n                    <div class=\"bg-gray-900\/50 p-3 rounded-lg mb-4 text-center text-sm\">Total Encabezados: <span class=\"font-bold text-white\">${userHeadings.length}<\/span> | H1: ${getH1Feedback(userH1Count)}<\/div>\n                    <ul class=\"space-y-2\">${generateHeadingList(userHeadings)}<\/ul>\n                <\/div>\n                <div>\n                    <h3 class=\"text-xl font-semibold text-gray-300 mb-4 text-center\">Estructura del Competidor<\/h3>\n                     <div class=\"bg-gray-900\/50 p-3 rounded-lg mb-4 text-center text-sm\">Total Encabezados: <span class=\"font-bold text-white\">${competitorHeadings.length}<\/span> | H1: ${getH1Feedback(competitorH1Count)}<\/div>\n                    <ul class=\"space-y-2\">${generateHeadingList(competitorHeadings)}<\/ul>\n                <\/div>\n            <\/div>\n        <\/div>`;\n        resultsContainer.innerHTML += structureHtml;\n    }\n\n    function generateHeadingList(headings) {\n        if (headings.length === 0) return '<li class=\"text-gray-500 text-center bg-gray-700 p-3 rounded-lg\">No se encontraron encabezados.<\/li>';\n        return headings.map(h => {\n            const paddingClasses = { 'H1': 'pl-2', 'H2': 'pl-6', 'H3': 'pl-10', 'H4': 'pl-14', 'H5': 'pl-16', 'H6': 'pl-16' };\n            const textSize = h.level === 'H1' ? 'text-base' : 'text-sm';\n            return `<li class=\"bg-gray-700\/80 p-2 rounded-md ${paddingClasses[h.level] || 'pl-2'} ${textSize}\"><strong class=\"font-mono text-blue-400\/80 mr-2\">${h.level}:<\/strong><span class=\"text-gray-300\">${h.text}<\/span><\/li>`;\n        }).join('');\n    }\n\n    function displayTechnicalAnalysis(userTechData, competitorTechData) {\n        const getWinner = (userVal, compVal, lowerIsBetter = false) => {\n            if (userVal === compVal) return 'Empate';\n            return (lowerIsBetter ? userVal < compVal : userVal > compVal) ? 'T\u00fa' : 'Competidor';\n        };\n        const rows = [\n            { metric: 'Total de Enlaces', user: userTechData.totalLinks, comp: competitorTechData.totalLinks },\n            { metric: 'Enlaces Internos', user: userTechData.internalLinks, comp: competitorTechData.internalLinks },\n            { metric: 'Enlaces Externos', user: userTechData.externalLinks, comp: competitorTechData.externalLinks },\n            { metric: 'Total de Im\u00e1genes', user: userTechData.totalImages, comp: competitorTechData.totalImages },\n            { metric: 'Im\u00e1genes sin \"alt\"', user: userTechData.imagesWithoutAlt, comp: competitorTechData.imagesWithoutAlt, lowerIsBetter: true },\n        ];\n        const technicalHtml = `\n        <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-700\">\n            <h2 class=\"text-2xl font-bold text-gray-200 mb-6 flex items-center\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-8 w-8 text-blue-400 mr-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\"><\/path><\/svg>3. An\u00e1lisis T\u00e9cnico y de Enlaces<\/h2>\n            <p class=\"text-gray-400 mb-6\">Compara m\u00e9tricas t\u00e9cnicas clave. Un buen enlazado y el uso correcto de im\u00e1genes son se\u00f1ales de calidad para Google.<\/p>\n            <div class=\"overflow-x-auto\"><table class=\"w-full text-left\"><thead><tr class=\"border-b border-gray-700\"><th class=\"p-4 text-sm font-semibold text-gray-400\">M\u00e9trica<\/th><th class=\"p-4 text-sm font-semibold text-gray-400 text-center\">Tu Web<\/th><th class=\"p-4 text-sm font-semibold text-gray-400 text-center\">Competidor<\/th><th class=\"p-4 text-sm font-semibold text-gray-400 text-center\">Ganador<\/th><\/tr><\/thead><tbody class=\"divide-y divide-gray-700\">\n                ${rows.map(row => {\n                    const winner = getWinner(row.user, row.comp, row.lowerIsBetter);\n                    return `\n                    <tr class=\"hover:bg-gray-700\/50\"><td class=\"p-4 font-medium\">${row.metric}<\/td><td class=\"p-4 text-center font-mono text-lg\">${row.user}<\/td><td class=\"p-4 text-center font-mono text-lg\">${row.comp}<\/td><td class=\"p-4 winner-cell font-bold ${winner === 'T\u00fa' ? 'text-green-400' : (winner === 'Competidor' ? 'text-red-400' : 'text-gray-400')}\">${winner}<\/td><\/tr>`;\n                }).join('')}\n            <\/tbody><\/table><\/div>\n        <\/div>`;\n        resultsContainer.innerHTML += technicalHtml;\n    }\n\n    function displayCompetitiveAdvantage(userData, competitorData) {\n        const getWinner = (userVal, compVal, lowerIsBetter = false) => {\n            if (userVal === compVal) return 'Empate';\n            return (lowerIsBetter ? userVal < compVal : userVal > compVal) ? 'T\u00fa' : 'Competidor';\n        };\n        const winnerWordCount = getWinner(userData.wordCount, competitorData.wordCount);\n        const winnerSchema = getWinner(userData.schema.length, competitorData.schema.length);\n        \n        const advantageHtml = `\n        <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-700\">\n            <h2 class=\"text-2xl font-bold text-gray-200 mb-6 flex items-center\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-8 w-8 text-blue-400 mr-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l4 4m0 0l4 4m-4-4H4m16 8l-4 4m0 0l-4 4m4-4h12\"><\/path><\/svg>4. An\u00e1lisis de Ventaja Competitiva<\/h2>\n            <p class=\"text-gray-400 mb-6\">Estos factores pueden darte una ventaja decisiva en los resultados de b\u00fasqueda, como aparecer con estrellas, precios, o simplemente ofreciendo m\u00e1s informaci\u00f3n que tu rival.<\/p>\n            <div class=\"overflow-x-auto\"><table class=\"w-full text-left\"><thead><tr class=\"border-b border-gray-700\"><th class=\"p-4 text-sm font-semibold text-gray-400\">Factor<\/th><th class=\"p-4 text-sm font-semibold text-gray-400 text-center\">Tu Web<\/th><th class=\"p-4 text-sm font-semibold text-gray-400 text-center\">Competidor<\/th><th class=\"p-4 text-sm font-semibold text-gray-400 text-center\">Ventaja<\/th><\/tr><\/thead><tbody class=\"divide-y divide-gray-700\">\n                <tr class=\"hover:bg-gray-700\/50\">\n                    <td class=\"p-4 font-medium\">Datos Estructurados (Schema)<\/td>\n                    <td class=\"p-4 text-center text-sm\">${userData.schema.length > 0 ? userData.schema.map(s => `<span class=\"bg-blue-900\/70 text-blue-300 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded\">${s}<\/span>`).join('') : '<span class=\"text-gray-500\">No detectado<\/span>'}<\/td>\n                    <td class=\"p-4 text-center text-sm\">${competitorData.schema.length > 0 ? competitorData.schema.map(s => `<span class=\"bg-blue-900\/70 text-blue-300 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded\">${s}<\/span>`).join('') : '<span class=\"text-gray-500\">No detectado<\/span>'}<\/td>\n                    <td class=\"p-4 winner-cell font-bold ${winnerSchema === 'T\u00fa' ? 'text-green-400' : (winnerSchema === 'Competidor' ? 'text-red-400' : 'text-gray-400')}\">${winnerSchema}<\/td>\n                <\/tr>\n                <tr class=\"hover:bg-gray-700\/50\">\n                    <td class=\"p-4 font-medium\">Profundidad (N\u00ba Palabras)<\/td>\n                    <td class=\"p-4 text-center font-mono text-lg\">${userData.wordCount}<\/td>\n                    <td class=\"p-4 text-center font-mono text-lg\">${competitorData.wordCount}<\/td>\n                    <td class=\"p-4 winner-cell font-bold ${winnerWordCount === 'T\u00fa' ? 'text-green-400' : (winnerWordCount === 'Competidor' ? 'text-red-400' : 'text-gray-400')}\">${winnerWordCount}<\/td>\n                <\/tr>\n            <\/tbody><\/table><\/div>\n        <\/div>`;\n        resultsContainer.innerHTML += advantageHtml;\n    }\n\n    function displayPerformanceAnalysis(userData, competitorData) {\n        const performanceHtml = `\n        <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-700\">\n            <h2 class=\"text-2xl font-bold text-gray-200 mb-6 flex items-center\">\n                <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-8 w-8 text-blue-400 mr-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 10V3L4 14h7v7l9-11h-7z\"><\/path><\/svg>\n                5. An\u00e1lisis de Rendimiento y Velocidad (Core Web Vitals)\n            <\/h2>\n            <p class=\"text-gray-400 mb-8\">La velocidad de carga es un factor de posicionamiento crucial. Google premia las webs que ofrecen una experiencia de usuario r\u00e1pida y fluida, especialmente en dispositivos m\u00f3viles.<\/p>\n            <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n                <div>\n                    <h3 class=\"text-xl font-semibold text-gray-300 mb-4 text-center\">Rendimiento de tu Web<\/h3>\n                    <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-6\">\n                        ${generatePerformanceCard(userData.performance.mobile, 'M\u00f3vil')}\n                        ${generatePerformanceCard(userData.performance.desktop, 'Ordenador')}\n                    <\/div>\n                <\/div>\n                 <div>\n                    <h3 class=\"text-xl font-semibold text-gray-300 mb-4 text-center\">Rendimiento del Competidor<\/h3>\n                    <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-6\">\n                        ${generatePerformanceCard(competitorData.performance.mobile, 'M\u00f3vil')}\n                        ${generatePerformanceCard(competitorData.performance.desktop, 'Ordenador')}\n                    <\/div>\n                <\/div>\n            <\/div>\n        <\/div>`;\n        resultsContainer.innerHTML += performanceHtml;\n    }\n    \n    function displaySocialAnalysis(userData, competitorData) {\n        const socialHtml = `\n        <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-700\">\n            <h2 class=\"text-2xl font-bold text-gray-200 mb-6 flex items-center\">\n                <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-8 w-8 text-blue-400 mr-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8.684 13.342C8.886 12.938 9 12.482 9 12s-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.368a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z\"><\/path><\/svg>\n                6. An\u00e1lisis de Redes Sociales (Open Graph & Twitter Cards)\n            <\/h2>\n            <p class=\"text-gray-400 mb-8\">Controla c\u00f3mo se ven tus enlaces al compartirse en redes sociales como Facebook, X (Twitter) o WhatsApp. Una buena previsualizaci\u00f3n aumenta los clics.<\/p>\n            <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n                <div>\n                    <h3 class=\"text-xl font-semibold text-gray-300 mb-4 text-center\">Previsualizaci\u00f3n de tu Web<\/h3>\n                    ${generateSocialPreview(userData.social)}\n                <\/div>\n                 <div>\n                    <h3 class=\"text-xl font-semibold text-gray-300 mb-4 text-center\">Previsualizaci\u00f3n del Competidor<\/h3>\n                    ${generateSocialPreview(competitorData.social)}\n                <\/div>\n            <\/div>\n        <\/div>`;\n        resultsContainer.innerHTML += socialHtml;\n    }\n\n    function displayCrawlabilityAnalysis(userData, competitorData) {\n        const crawlabilityHtml = `\n        <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-700\">\n            <h2 class=\"text-2xl font-bold text-gray-200 mb-6 flex items-center\">\n                <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-8 w-8 text-blue-400 mr-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n                  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15 12a3 3 0 11-6 0 3 3 0 016 0z\" \/>\n                  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z\" \/>\n                <\/svg>\n                7. An\u00e1lisis de Indexabilidad y Rastreo\n            <\/h2>\n            <p class=\"text-gray-400 mb-8\">Comprueba si le est\u00e1s dando a Google las directrices correctas para encontrar y mostrar tu contenido en los resultados de b\u00fasqueda.<\/p>\n            <div class=\"overflow-x-auto\">\n                <table class=\"w-full text-left\">\n                    <thead>\n                        <tr class=\"border-b border-gray-700\">\n                            <th class=\"p-4 text-sm font-semibold text-gray-400\">Directiva<\/th>\n                            <th class=\"p-4 text-sm font-semibold text-gray-400\">Tu Web<\/th>\n                            <th class=\"p-4 text-sm font-semibold text-gray-400\">Competidor<\/th>\n                        <\/tr>\n                    <\/thead>\n                    <tbody class=\"divide-y divide-gray-700\">\n                        ${generateCrawlabilityRow('Meta Robots', userData.crawlability?.metaRobots, competitorData.crawlability?.metaRobots)}\n                        ${generateCrawlabilityRow('URL Can\u00f3nica', userData.crawlability?.canonical, competitorData.crawlability?.canonical)}\n                        ${generateCrawlabilityRow('robots.txt', userData.crawlability?.robotsTxt, competitorData.crawlability?.robotsTxt)}\n                        ${generateCrawlabilityRow('sitemap.xml', userData.crawlability?.sitemapXml, competitorData.crawlability?.sitemapXml)}\n                    <\/tbody>\n                <\/table>\n            <\/div>\n        <\/div>`;\n        resultsContainer.innerHTML += crawlabilityHtml;\n    }\n    \n    function displaySecurityAnalysis(userData, competitorData) {\n        const securityHtml = `\n        <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-700\">\n            <h2 class=\"text-2xl font-bold text-gray-200 mb-6 flex items-center\">\n                <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-8 w-8 text-blue-400 mr-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n                  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z\" \/>\n                <\/svg>\n                8. An\u00e1lisis de Seguridad\n            <\/h2>\n            <p class=\"text-gray-400 mb-8\">Una conexi\u00f3n segura (HTTPS) es una se\u00f1al de confianza para usuarios y un factor de posicionamiento para Google.<\/p>\n            <div class=\"overflow-x-auto\">\n                <table class=\"w-full text-left\">\n                    <thead>\n                        <tr class=\"border-b border-gray-700\">\n                            <th class=\"p-4 text-sm font-semibold text-gray-400\">Factor<\/th>\n                            <th class=\"p-4 text-sm font-semibold text-gray-400\">Tu Web<\/th>\n                            <th class=\"p-4 text-sm font-semibold text-gray-400\">Competidor<\/th>\n                        <\/tr>\n                    <\/thead>\n                    <tbody class=\"divide-y divide-gray-700\">\n                        ${generateSecurityRow('Conexi\u00f3n Segura (HTTPS)', userData.security, competitorData.security, 'isSecure')}\n                        ${generateSecurityRow('Contenido Mixto Inseguro', userData.security, competitorData.security, 'mixedContent')}\n                    <\/tbody>\n                <\/table>\n            <\/div>\n        <\/div>`;\n        resultsContainer.innerHTML += securityHtml;\n    }\n\n    function generateSecurityRow(metric, userData, competitorData, type) {\n        const formatCell = (data) => {\n            if (data === null || typeof data === 'undefined') {\n                return '<td class=\"p-4 text-sm text-gray-500\">Error al obtener<\/td>';\n            }\n            let text, icon;\n            \n            if (type === 'isSecure') {\n                text = data.isSecure ? 'S\u00ed' : 'No';\n                icon = data.isSecure \n                    ? '<svg class=\"w-5 h-5 text-green-400 mr-2 inline-block\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\"><\/path><\/svg>' \n                    : '<svg class=\"w-5 h-5 text-red-400 mr-2 inline-block\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\"><\/path><\/svg>';\n            } else { \/\/ mixedContent\n                text = data.mixedContentCount;\n                icon = data.mixedContentCount === 0\n                    ? '<svg class=\"w-5 h-5 text-green-400 mr-2 inline-block\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\"><\/path><\/svg>'\n                    : '<svg class=\"w-5 h-5 text-yellow-400 mr-2 inline-block\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"><\/path><\/svg>';\n            }\n\n            return `<td class=\"p-4 text-sm font-mono\">${icon}${text}<\/td>`;\n        };\n        \n        return `\n            <tr class=\"hover:bg-gray-700\/50\">\n                <td class=\"p-4 font-medium text-gray-300\">${metric}<\/td>\n                ${formatCell(userData)}\n                ${formatCell(competitorData)}\n            <\/tr>\n        `;\n    }\n\n\n    function generateCrawlabilityRow(metric, userData, competitorData) {\n        const formatCell = (data) => {\n            if (data === null || typeof data === 'undefined') {\n                return '<td class=\"p-4 text-sm text-gray-500\">Error al obtener<\/td>';\n            }\n            let text = data;\n            let icon = '';\n            \n            if (metric === 'Meta Robots') {\n                if (String(data).includes('noindex')) {\n                    icon = '<svg class=\"w-5 h-5 text-red-400 mr-2 inline-block\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636\"><\/path><\/svg>';\n                } else {\n                    icon = '<svg class=\"w-5 h-5 text-green-400 mr-2 inline-block\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\"><\/path><\/svg>';\n                }\n            } else if (metric === 'robots.txt' || metric === 'sitemap.xml') {\n                text = data ? 'Encontrado' : 'No Encontrado';\n                 icon = data ? '<svg class=\"w-5 h-5 text-green-400 mr-2 inline-block\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\"><\/path><\/svg>' : '<svg class=\"w-5 h-5 text-yellow-400 mr-2 inline-block\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"><\/path><\/svg>';\n            } else if (metric === 'URL Can\u00f3nica') {\n                 icon = data === 'No especificada' ? '' : '<svg class=\"w-5 h-5 text-green-400 mr-2 inline-block\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\"><\/path><\/svg>';\n            }\n\n            return `<td class=\"p-4 text-sm font-mono break-all\">${icon}${text}<\/td>`;\n        };\n        \n        return `\n            <tr class=\"hover:bg-gray-700\/50\">\n                <td class=\"p-4 font-medium text-gray-300\">${metric}<\/td>\n                ${formatCell(userData)}\n                ${formatCell(competitorData)}\n            <\/tr>\n        `;\n    }\n\n    function generateSocialPreview(socialData) {\n        const { title, description, image, siteName, found } = socialData;\n        const statusText = found ? 'Optimizado' : 'Mejorable';\n        const statusColor = found ? 'text-green-400' : 'text-yellow-400';\n        const proxiedImage = image ? `https:\/\/corsproxy.io\/?${encodeURIComponent(image)}` : '';\n\n        return `\n        <div class=\"bg-gray-900\/50 rounded-lg p-4\">\n            <div class=\"border border-gray-700 rounded-lg overflow-hidden\">\n                <div class=\"w-full aspect-video bg-gray-700 flex items-center justify-center\">\n                    ${image ? `<img decoding=\"async\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" data-orig-src=\"${proxiedImage}\" alt=\"Vista previa de la imagen\" class=\"lazyload w-full h-full object-cover\" crossorigin=\"anonymous\" onerror=\"this.style.display='none'; this.nextSibling.style.display='flex'\"> <div style=\"display:none;\" class=\"text-gray-500 text-sm p-4 text-center\">No se pudo cargar la imagen.<\/div>` : '<div class=\"text-gray-500 text-sm\">Imagen no encontrada<\/div>'}\n                <\/div>\n                <div class=\"p-4 bg-gray-800\">\n                    <p class=\"text-xs text-gray-400 uppercase truncate\">${siteName || 'SITENAME.COM'}<\/p>\n                    <h4 class=\"font-bold text-gray-200 truncate\">${title || 'T\u00edtulo no encontrado'}<\/h4>\n                    <p class=\"text-sm text-gray-400 h-10 overflow-hidden\">${description || 'Descripci\u00f3n no encontrada.'}<\/p>\n                <\/div>\n            <\/div>\n            <div class=\"mt-4 text-center text-sm\">\n                <p><strong>Estado:<\/strong> <span class=\"${statusColor} font-semibold\">${statusText}<\/span><\/p>\n            <\/div>\n        <\/div>`;\n    }\n\n\n    function generatePerformanceCard(perfData, title) {\n        if (perfData.error) {\n            return `\n            <div class=\"bg-gray-700\/50 p-4 rounded-lg text-center\">\n                <h4 class=\"font-bold text-lg\">${title}<\/h4>\n                <p class=\"text-sm text-gray-400 mt-4\">No se pudo obtener el an\u00e1lisis de rendimiento.<\/p>\n                <p class=\"text-xs text-red-400 mt-1\">${perfData.error}<\/p>\n            <\/div>`;\n        }\n        \n        const score = perfData.performanceScore;\n        const colorClass = score >= 90 ? 'text-green-400' : score >= 50 ? 'text-yellow-400' : 'text-red-400';\n        \n        return `\n        <div class=\"bg-gray-700\/50 p-4 rounded-lg text-center\">\n            <h4 class=\"font-bold text-lg\">${title}<\/h4>\n            <div class=\"my-4\">\n                ${generateScoreRing(score)}\n            <\/div>\n            <div class=\"text-sm space-y-2\">\n                <p><strong>FCP:<\/strong> ${perfData.fcp > 0 ? (perfData.fcp \/ 1000).toFixed(2) + 's' : 'N\/A'}<\/p>\n                <p><strong>LCP:<\/strong> ${perfData.lcp > 0 ? (perfData.lcp \/ 1000).toFixed(2) + 's' : 'N\/A'}<\/p>\n            <\/div>\n        <\/div>`;\n    }\n\n    function generateScoreRing(score) {\n        const radius = 50;\n        const circumference = 2 * Math.PI * radius;\n        const offset = circumference - (score \/ 100) * circumference;\n        const color = score >= 90 ? '#4ade80' : score >= 50 ? '#facc15' : '#f87171';\n\n        return `\n        <div class=\"relative inline-flex items-center justify-center\">\n            <svg class=\"w-32 h-32\">\n                <circle class=\"text-gray-600\" stroke-width=\"8\" stroke=\"currentColor\" fill=\"transparent\" r=\"${radius}\" cx=\"64\" cy=\"64\"\/>\n                <circle class=\"progress-ring__circle\" stroke-width=\"8\" stroke-dasharray=\"${circumference}\" stroke-dashoffset=\"${offset}\"\n                    stroke-linecap=\"round\" stroke=\"${color}\" fill=\"transparent\" r=\"${radius}\" cx=\"64\" cy=\"64\"\/>\n            <\/svg>\n            <span class=\"absolute text-3xl font-bold\" style=\"color:${color}\">${score}<\/span>\n        <\/div>`;\n    }\n\n    function displayThematicMap(userClusters, competitorClusters, userDomain, competitorDomain) {\n        const mapHtml = `\n        <div class=\"bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-700\">\n            <h2 class=\"text-2xl font-bold text-gray-200 mb-6 flex items-center\">\n                <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"h-8 w-8 text-teal-400 mr-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\"><\/path><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 11a3 3 0 11-6 0 3 3 0 016 0z\"><\/path><\/svg>\n                Mapa de Autoridad Tem\u00e1tica\n            <\/h2>\n            <p class=\"text-gray-400 mb-8\">Este mapa visualiza los principales temas que cada dominio trata, bas\u00e1ndose en un rastreo limitado de sus p\u00e1ginas. Un cl\u00faster m\u00e1s grande indica una mayor autoridad y profundidad en ese tema.<\/p>\n            <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n                <div>\n                    <h3 class=\"text-xl font-semibold text-gray-300 mb-4 text-center\">${userDomain}<\/h3>\n                    <div class=\"space-y-4\">${generateClusterHTML(userClusters)}<\/div>\n                <\/div>\n                 <div>\n                    <h3 class=\"text-xl font-semibold text-gray-300 mb-4 text-center\">${competitorDomain}<\/h3>\n                    <div class=\"space-y-4\">${generateClusterHTML(competitorClusters)}<\/div>\n                <\/div>\n            <\/div>\n        <\/div>`;\n        resultsContainer.innerHTML = mapHtml;\n    }\n\n    function generateClusterHTML(clusters) {\n        if (clusters.length === 0) {\n            return '<p class=\"text-gray-500 text-center bg-gray-700\/50 p-4 rounded-lg\">No se pudieron identificar cl\u00fasteres de contenido claros.<\/p>';\n        }\n        return clusters.map(([topic, pages]) => `\n            <div class=\"bg-gray-700\/50 p-4 rounded-lg\">\n                <p class=\"text-lg font-bold text-teal-300 capitalize\">${topic}<\/p>\n                <p class=\"text-sm text-gray-400 mb-2\">${pages.length} p\u00e1ginas relacionadas encontradas.<\/p>\n                <div class=\"flex flex-wrap gap-2\">\n                    ${pages.map(p => `<a href=\"${p.url}\" target=\"_blank\" class=\"text-xs bg-gray-600 hover:bg-gray-500 text-gray-200 px-2 py-1 rounded-full truncate\" title=\"${p.title}\">${p.title}<\/a>`).join('')}\n                <\/div>\n            <\/div>\n        `).join('');\n    }\n\n<\/script>\n<\/body>\n<\/html><\/div><\/div><\/div><\/div><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Utiliza nuestro explorador de inteligencias artificiales para mantenerte al d\u00eda. \u00a1Actualizamos constantemente esta fant\u00e1stica lista!<\/p>","protected":false},"author":1,"featured_media":2611,"menu_order":0,"comment_status":"open","ping_status":"closed","template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"portfolio_category":[53],"portfolio_skills":[],"portfolio_tags":[59,58],"class_list":["post-2608","avada_portfolio","type-avada_portfolio","status-publish","format-standard","has-post-thumbnail","hentry","portfolio_category-utilidades","portfolio_tags-marketing","portfolio_tags-utilidades-seo"],"acf":[],"_links":{"self":[{"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/avada_portfolio\/2608","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/avada_portfolio"}],"about":[{"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/types\/avada_portfolio"}],"author":[{"embeddable":true,"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/comments?post=2608"}],"version-history":[{"count":2,"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/avada_portfolio\/2608\/revisions"}],"predecessor-version":[{"id":2610,"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/avada_portfolio\/2608\/revisions\/2610"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/media\/2611"}],"wp:attachment":[{"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/media?parent=2608"}],"wp:term":[{"taxonomy":"portfolio_category","embeddable":true,"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/portfolio_category?post=2608"},{"taxonomy":"portfolio_skills","embeddable":true,"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/portfolio_skills?post=2608"},{"taxonomy":"portfolio_tags","embeddable":true,"href":"https:\/\/edukia.org\/ca\/wp-json\/wp\/v2\/portfolio_tags?post=2608"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}