// Medidor de Rendimiento Sell-U — App principal (Hero · Analizando · Resultados · Captura lead)
// Llama al backend Laravel para datos reales de PageSpeed + HTML scraping.
const { useState, useEffect, useRef } = React;
const ANALYZE_STEPS = window.ConvierteEngine.CATEGORIES.map((c) => c.label);
function scrollToId(id) {
const el = document.getElementById(id);
if (!el) return;
const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const y = el.getBoundingClientRect().top + window.scrollY - 24;
window.scrollTo({ top: y, behavior: reduce ? "auto" : "smooth" });
}
function normalizeUrl(raw) {
let u = (raw || "").trim();
if (!u) return "";
if (!/^https?:\/\//i.test(u)) u = "https://" + u;
return u;
}
function isValidUrl(raw) {
const u = normalizeUrl(raw);
return /^https?:\/\/([\w-]+\.)+[a-z]{2,}(\/\S*)?$/i.test(u);
}
function prettyHost(url) {
try { return new URL(normalizeUrl(url)).host.replace(/^www\./, ""); }
catch { return url; }
}
/* Nav y Footer del convertidor se eliminaron — ahora la vista Blade
envuelve el #root con y de Sell-U para que
el convertidor se integre visualmente con el resto del sitio. */
/* ===================== HERO ===================== */
function Hero({ url, setUrl, device, setDevice, onAnalyze, error }) {
const inputRef = useRef(null);
return (
Medidor gratis · datos reales de Google
¿Tu página web está perdiendo clientes? Descúbrelo en 30 segundos
Escribe la dirección de tu sitio y te decimos en lenguaje claro
qué está bien, qué está mal y cómo arreglarlo — sin tecnicismos.
Usamos la misma tecnología de Google para medir tu página.
Resolviendo los problemas detectados, basado en tu puntaje actual.
);
}
/* ===================== VELOCIDAD REAL (Core Web Vitals con nombres humanos) ===================== */
function CWVCard({ label, sublabel, explain, value, unit, threshold, lowerBetter = true }) {
if (value === null || value === undefined) {
return (
—
{label}
No pudimos medirlo en este momento.
);
}
// Determinar tono según threshold ([bueno, necesita-mejora])
let tone = "success";
if (lowerBetter) {
if (value > threshold[1]) tone = "danger";
else if (value > threshold[0]) tone = "warning";
} else {
if (value < threshold[0]) tone = "danger";
else if (value < threshold[1]) tone = "warning";
}
const display = unit === "ms" && value > 1000
? (value / 1000).toFixed(1) + "s"
: unit === "ms" ? Math.round(value) + "ms"
: unit === "cls" ? value.toFixed(2)
: value;
const statusText = tone === "success" ? "✓ Muy bien" : tone === "warning" ? "Mejorable" : "✗ Problema";
const statusFull = tone === "success"
? "Tu sitio cumple bien con esta medida."
: tone === "warning"
? "Está aceptable, pero podría mejorar."
: "Está afectando la experiencia de tus visitantes.";
return (
{display}
{label}
{sublabel &&
{sublabel}
}
{statusText}
{statusFull}
{explain &&
{explain}
}
);
}
function CoreWebVitalsSection({ cwv }) {
if (!cwv) return null;
return (
DATOS REALES
Medido en vivo por Google
¿Qué tan rápida y estable es tu página?
Estas 4 mediciones son las que Google usa para decidir cuánto destaca tu sitio
en los resultados de búsqueda y cuán cómoda es tu página para tus visitantes.
);
}
/* ===================== AUDITORÍA TÉCNICA (datos REALES) ===================== */
function AuditoriaTecnicaSection({ a }) {
if (!a || !a.ok) return null;
const { fmtNum, fmtCompact } = window.ConvierteEngine;
const w = a.whois || {};
const c = a.crawl || {};
const s = a.seo || {};
const idx = a.indexed || {};
const t = a.tech || {};
const ssl = a.ssl || {};
const sec = a.security || {};
const ct = a.content || {};
// KPIs reales con lenguaje natural
const kpis = [];
// 1. Páginas indexadas
if (idx.indexed_pages !== null && idx.indexed_pages !== undefined) {
kpis.push({ icon:"globe", label:"Páginas que aparecen en Google", value:fmtCompact(idx.indexed_pages),
hint: idx.indexed_pages > 10 ? "Suficientes páginas en los resultados de búsqueda." : "Pocas páginas. Google podría no estar viendo todo tu sitio.",
tone: idx.indexed_pages > 10 ? "success" : "warning" });
}
// 2. Edad del dominio
if (w.domain_age_years) {
kpis.push({ icon:"network", label:"Tu sitio existe hace", value:w.domain_age_years, suffix:" años",
hint: w.domain_age_years >= 3 ? "Google confía más en dominios viejos." : (w.domain_age_years >= 1 ? "Aún relativamente nuevo." : "Muy nuevo. Google tarda en confiar."),
tone: w.domain_age_years >= 3 ? "success" : (w.domain_age_years >= 1 ? "info" : "warning") });
}
// 3. Páginas crawleadas con OK
kpis.push({ icon:"check", label:"Páginas revisadas", value:c.pages_crawled || 0,
hint: (c.broken_count || 0) === 0 ? "Todas funcionando bien." : `${c.broken_count} con problemas (links rotos).`,
tone: (c.broken_count || 0) === 0 ? "success" : "warning" });
// 4. Links rotos
if ((c.broken_count || 0) > 0) {
kpis.push({ icon:"alert", label:"Enlaces rotos en tu sitio", value:c.broken_count,
hint:"Cuando alguien hace click y la página no existe. Frustra al visitante y a Google.",
tone: c.broken_count > 5 ? "danger" : "warning" });
}
// 5. SSL grade (candado de seguridad)
if (ssl.grade) {
kpis.push({ icon:"shield", label:"Seguridad del candado (HTTPS)", value:ssl.grade, suffix:"/A+",
hint: ssl.grade.startsWith('A') ? "Excelente. Tu sitio es seguro para el visitante." : (ssl.grade === 'B' ? "Aceptable, podría mejorar." : "Tu certificado de seguridad tiene problemas."),
tone: ssl.grade.startsWith('A') ? "success" : (ssl.grade === 'B' ? "info" : "warning") });
} else if (ssl.has_https === false) {
kpis.push({ icon:"alert", label:"Candado de seguridad", value:"NO TIENE", hint:"Tu sitio no usa HTTPS — Chrome lo marca como 'No seguro' y la gente se va.", tone:"danger" });
}
// 6. Security headers (configuración de seguridad)
kpis.push({ icon:"shield", label:"Configuración de seguridad", value:sec.grade || "F",
hint: (sec.missing_count || 0) > 0 ? `Le faltan ${sec.missing_count} protecciones de seguridad estándar.` : "Bien configurado.",
tone: (sec.grade || "F") === "A" ? "success" : ((sec.grade || "F") === "B" ? "info" : "warning") });
// 7. CMS detectado (qué plataforma usa)
if (t.cms) {
kpis.push({ icon:"layers", label:"Hecho con", value:t.cms, hint:"Plataforma con la que está construido tu sitio.", tone:"info" });
} else if (t.frameworks?.length) {
kpis.push({ icon:"layers", label:"Hecho con", value:t.frameworks[0], hint:"Tecnología detectada en tu sitio.", tone:"info" });
}
// 8. E-commerce
if (t.ecommerce) {
kpis.push({ icon:"cursor", label:"Tienda online", value:t.ecommerce, hint:"Detectamos que vendes online con esta plataforma.", tone:"info" });
}
// 9. Schema.org (Datos para Google)
if ((s.meta?.schema_count || 0) > 0) {
kpis.push({ icon:"check", label:"Datos extra para Google", value:"Sí",
hint:"Le das información estructurada a Google (precios, reseñas, etc.) para que destaque mejor tu sitio.",
tone:"success" });
} else {
kpis.push({ icon:"alert", label:"Datos extra para Google", value:"No",
hint:"Pierdes la oportunidad de que Google muestre tu producto con estrellas, precios, etc.",
tone:"warning" });
}
// 10. Open Graph (cómo se ve al compartir)
const ogOk = s.meta?.has_og_image && s.meta?.has_og_title && s.meta?.has_og_desc;
kpis.push({ icon:"globe", label:"Vista previa al compartir", value: ogOk ? "Bien" : "Incompleta",
hint: ogOk ? "Cuando comparten tu link en WhatsApp/Facebook, se ve con foto y descripción." : "Al compartir tu link en redes, no aparece foto/título correctamente.",
tone: ogOk ? "success" : "warning" });
// 11. Sitemap (mapa de tu sitio)
kpis.push({ icon:"layers", label:"Mapa del sitio (sitemap)", value: s.sitemap?.exists ? `${s.sitemap.urls_count} páginas` : "No tiene",
hint: s.sitemap?.exists ? "Le facilitas a Google encontrar todas tus páginas." : "Sin sitemap, Google puede olvidar algunas páginas.",
tone: s.sitemap?.exists ? "success" : "warning" });
// 12. Contenido
kpis.push({ icon:"message", label:"Palabras en tu página principal", value: fmtCompact(ct.word_count || 0),
hint: ct.has_thin_content ? "Muy poco texto (menos de 250 palabras). Google prefiere páginas con más contenido." : "Cantidad de texto adecuada para Google.",
tone: ct.has_thin_content ? "warning" : "success" });
return (
DATOS REALES
Revisamos {c.pages_crawled || 0} páginas de tu sitio en vivo
¿Cómo está la salud técnica de tu sitio?
Estos son aspectos técnicos importantes que afectan cuán bien se ve tu página
en Google, qué tan rápida es y qué tan segura. Todo medido en vivo,
nada de estimaciones.
{kpis.map((m, i) => (
{m.value}{m.suffix ? {m.suffix} : null}
{m.label}
{m.hint}
))}
{/* Tecnologías y analytics si hay */}
{(t.analytics?.length > 0 || t.frameworks?.length > 0) && (
{t.frameworks?.length > 0 && (
Tecnologías que detectamos
{t.frameworks.map((f) => (
{f}
))}
)}
{t.analytics?.length > 0 && (
Herramientas de seguimiento
{t.analytics.map((a) => (
{a}
))}
)}
{t.hosting && (
Dónde está alojado
{t.hosting}
)}
)}
Todo lo de arriba es real — lo medimos directamente entrando a tu sitio,
revisando hasta {c.pages_crawled || 0} páginas, el certificado de seguridad, las tecnologías
que usas y cuánto contenido tienes.
);
}
function CategoryGrid({ categories }) {
return (
{categories.map((c, i) => (
{c.score}
{c.label}
))}
);
}
/* ════════════════════════════════════════════════════════════════════════════
REDISEÑO 2026 — Paleta del handoff. Helpers compartidos para Google notes,
métricas técnicas, áreas y problemas. Tres tonos según score:
>=90 → success (verde) 50-89 → warn (ámbar) <50 → danger (rojo)
Los colores y border-radius vienen tal cual del mock HTML/CSS del handoff.
════════════════════════════════════════════════════════════════════════════ */
const NEW_META = (s) => {
if (s == null || isNaN(s)) return { ring:'#8a90a3', track:'#eef0f4', bg:'#eef0f6', text:'#5b607d', statusLabel:'Sin dato' };
if (s >= 90) return { ring:'#22c55e', track:'#d4f1de', bg:'#e7f8ee', text:'#15803d', statusLabel:'Bien' };
if (s >= 50) return { ring:'#eab308', track:'#f3e9c6', bg:'#fdf6e0', text:'#a16207', statusLabel:'Mejorable' };
return { ring:'#ef4444', track:'#f6d8d8', bg:'#fdecec', text:'#c0271d', statusLabel:'Crítico' };
};
const donutDash = (score, r) => {
const c = 2 * Math.PI * r;
const filled = c * (score == null ? 0 : score) / 100;
return filled.toFixed(1) + ' ' + (c - filled).toFixed(1);
};
/* ─── Tooltip wrapper ─── click/tap en móvil, hover en desktop, esc para cerrar.
Antes solo CSS :hover, no funcionaba en touch. Ahora usa state + outside-click. */
function HelpTip({ children, position = 'bottom' }) {
const [open, setOpen] = useState(false);
const wrapRef = useRef(null);
const isTop = position === 'top';
// Click fuera → cerrar. Esc → cerrar.
useEffect(() => {
if (!open) return;
const onDocClick = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('click', onDocClick);
document.addEventListener('keydown', onKey);
return () => { document.removeEventListener('click', onDocClick); document.removeEventListener('keydown', onKey); };
}, [open]);
return (
{open && (
e.stopPropagation()}>{children}
)}
);
}
/* ─── 1. Google verification band (verde, con check) ─── */
function GoogleVerificationBand({ dx, onReanalizar }) {
const [busy, setBusy] = useState(false);
const fetched = dx.meta?.fetched_at ? new Date(dx.meta.fetched_at) : null;
const ageMin = fetched ? Math.round((Date.now() - fetched.getTime()) / 60000) : null;
const psWebUrl = `https://pagespeed.web.dev/analysis?url=${encodeURIComponent(dx.url)}&form_factor=${dx.device}`;
const handleRe = async () => { setBusy(true); await onReanalizar(); setBusy(false); };
return (
✓
DATOS DIRECTOS DE GOOGLE
Estos no son inventados. Son los mismos números de la herramienta oficial de Google
{fetched && <> · análisis hecho {ageMin < 1 ? 'hace un momento' : `hace ${ageMin} min`}>}
);
}
/* ─── 2. Google 4 notes (donuts con tooltip) ─── */
const GOOGLE_TOOLTIPS = [
{ what:'Mide cuánto tarda tu página en cargar y volverse usable en un celular.', ideal:'90 o más', why:'Una página lenta pierde ventas: muchos se van antes de ver tu producto.' },
{ what:'Revisa si cualquier persona puede leer, tocar y navegar tu página sin problemas.', ideal:'90 o más', why:'Si cuesta usarla, cuesta comprar.' },
{ what:'Comprueba que el sitio esté bien construido: HTTPS, código sano y sin errores.', ideal:'90 o más', why:'Un sitio sólido da confianza y evita fallas durante la compra.' },
{ what:'Mide qué tan bien Google entiende y muestra tu página en los resultados.', ideal:'90 o más', why:'Más visibilidad significa más visitas y más ventas.' },
];
function GoogleNotesSection({ psiScores, psiScoresSource }) {
const labels = ['Velocidad', 'Fácil de usar', 'Calidad técnica', 'Visible en Google'];
const subs = ['Qué tan rápido carga tu página.', 'Qué tan fácil es navegarla.', 'Si está bien hecha por dentro.', 'Qué tan bien la encuentra Google.'];
const keys = ['performance', 'accessibility', 'best_practices', 'seo'];
const src = psiScoresSource || ['unavailable','unavailable','unavailable','unavailable'];
const algunaFalta = src.some(o => o !== 'google');
return (
Las 4 notas de Google
Notas que Google le da a tu página
Las mismas 4 notas que verías en la herramienta oficial de Google. Van de 0 a 100 — mientras más alto, mejor.
);
}
/* Banner de verificación: muestra cuándo se hizo el análisis y permite
comparar con pagespeed.web.dev en una pestaña nueva. */
function VerificacionBanner({ dx, onReanalizar }) {
const [reanalizando, setReanalizando] = useState(false);
const fetched = dx.meta?.fetched_at ? new Date(dx.meta.fetched_at) : null;
const ageMin = fetched ? Math.round((Date.now() - fetched.getTime()) / 60000) : null;
const psWebUrl = `https://pagespeed.web.dev/analysis?url=${encodeURIComponent(dx.url)}&form_factor=${dx.device}`;
const handleRe = async () => {
setReanalizando(true);
await onReanalizar();
setReanalizando(false);
};
return (
DATOS DE GOOGLE
Estos no son inventados. Los obtenemos directo de Google — los mismos números que verías en su herramienta oficial.
{fetched && Análisis hecho {ageMin < 1 ? "hace un momento" : `hace ${ageMin} min`}}
¿Quieres comprobarlo? Toca el botón verde →
);
}
/* ─── 8. Sticky "Volver a medir" abajo a la derecha ─── */
function StickyVolver({ onClick }) {
return (
);
}
function Results({ dx, onReset, onReanalizar }) {
const free = dx.problems.slice(0, dx.freeCount);
const locked = dx.problems.slice(dx.freeCount);
const lockedCount = locked.length;
return (
{/* Breadcrumb */}
{/* 1 · Hero navy con donut grande, verdict y URL chip */}
{/* 2 · Banda verde "DATOS DIRECTOS DE GOOGLE" + comprobar/re-medir */}
{/* 3 · Las 4 notas oficiales de Google (donuts con tooltip) */}
{/* 4 · Métricas técnicas (LCP, CLS, FCP, TBT) */}
{dx.meta?.psi_ok && (
)}
{/* 5 · Acordeón con las 8 áreas Sell-U */}
{/* 6 · Lista de problemas con prioridad y solución bloqueada */}
Lo que hay que arreglar
Problemas detectados, ordenados por impacto
Empieza por los de prioridad alta — son los que más te están costando ventas hoy.
{lockedCount > 0 && <> Te mostramos {dx.freeCount} gratis; los {lockedCount} restantes los recibes en el reporte completo.>}
{/* CTA mid page */}
{/* 7 · Análisis técnico detallado (colapsable, mantiene Auditoría + Methodology
del análisis previo para no perder profundidad técnica). */}
Análisis técnico avanzado
Auditoría completa: indexación, SEO, SSL, seguridad, tecnología detectada y metodología
Ver detalle ▼
{dx.auditoria?.ok && }
{/* 8 · Sticky "Volver a medir" abajo a la derecha */}
);
}
function Methodology({ categories }) {
return (
¿De dónde salen estos números?
Cómo medimos tu página
Usamos la misma tecnología de Google para medir la velocidad y el SEO técnico,
además revisamos cada parte de tu página (titulares, botones, formularios, orden visual).
Cada área pesa distinto: lo que más afecta tus ventas, pesa más.
Sobre los datos: la velocidad, accesibilidad y SEO técnico son
datos reales medidos por Google. Las visitas estimadas y enlaces externos
son cálculos aproximados basados en lo que vemos en tu sitio.
En Sell-U LATAM tenemos un equipo que arregla las cosas que detectó este medidor para que tu página venda más. Déjanos tus datos y te enviamos el reporte completo + una propuesta a tu medida.
{[
'Reporte completo con todo lo que hay que arreglar',
'Plan ordenado por lo que más impacta tus ventas',
'Te contactamos en menos de 24h, sin compromiso',
].map((perk) => (
✓
{perk}
))}
{/* Columna derecha — form (o success state) */}
{sent ? (
✓
¡Listo! Te contactaremos en menos de 24h
Recibimos tu solicitud para {prettyHost(data.sitio)}. Nuestro equipo te enviará el reporte completo y propuesta a {data.correo}.
) : (
<>
{/* Honeypot anti-bot */}
{}} />
{serverErr && (
{serverErr}
)}
Sin compromiso. Usamos tus datos solo para enviarte el reporte y la propuesta.
>
)}
);
}
function Field({ label, id, error, children }) {
return (
{children}
{error ? {error} : null}
);
}
/* ===================== APP ===================== */
function App() {
const [phase, setPhase] = useState("hero"); // hero | analyzing | results
const [url, setUrl] = useState("");
// Default MOBILE para alinear con pagespeed.web.dev (que también muestra mobile primero).
// Si el cliente compara contra Google lado-a-lado, los números deben coincidir.
const [device, setDevice] = useState("mobile");
const [error, setError] = useState("");
const [dx, setDx] = useState(null);
const [analyzeError, setAnalyzeError] = useState("");
const [analyzeDone, setAnalyzeDone] = useState(false);
const analyze = async (noCacheArg) => {
// Defensa: si esta función se conecta directamente a un onClick, React
// pasa el SyntheticEvent como primer argumento. Solo aceptamos `true`
// literal (desde reanalizar()) — cualquier otra cosa es falsy.
const noCache = noCacheArg === true;
if (!isValidUrl(url)) {
setError("Ingresa una URL válida, por ejemplo: tu-sitio.com");
return;
}
setError("");
setAnalyzeError("");
setAnalyzeDone(false);
if (!noCache) {
setPhase("analyzing");
window.scrollTo({ top: 0, behavior: "auto" });
}
try {
const diag = await window.ConvierteEngine.fetchDiagnosis(normalizeUrl(url), device, noCache);
// Mover core_web_vitals al meta también para que el componente CWV los encuentre
if (diag.meta) diag.meta.core_web_vitals = diag.core_web_vitals;
setDx(diag);
setAnalyzeDone(true);
if (!noCache) {
// pequeña pausa visual para que termine la animación del progreso
setTimeout(() => {
setPhase("results");
window.scrollTo({ top: 0, behavior: "auto" });
}, 350);
}
} catch (e) {
const msg = (e && typeof e.message === "string") ? e.message : "Error inesperado.";
if (noCache) {
// Si falló el re-análisis, no romper la UI — mostrar alerta y seguir
alert("No pudimos re-analizar: " + msg);
} else {
setAnalyzeError(msg);
}
}
};
/** Re-analizar invalidando el cache del backend. Se queda en la vista de resultados. */
const reanalizar = async () => analyze(true);
const reset = () => {
setPhase("hero");
setDx(null);
setAnalyzeError("");
setAnalyzeDone(false);
window.scrollTo({ top: 0, behavior: "auto" });
};
return (
{/* ═══════════════════════════════════════════════════════════════════
REDISEÑO 2026 — Reglas mobile-first del medidor de rendimiento.
Los grids y paddings declarados inline en cada componente son los
del desktop (handoff). Estas media queries los apilan en móvil para
que el cliente lo vea bien desde el celular (≥80% del tráfico).
═══════════════════════════════════════════════════════════════════ */}
{/* Botón flotante "Analizar otra página" — solo en phase=analyzing
(en phase=results el rediseño ya trae su propio breadcrumb + sticky). */}
{phase === "analyzing" && (