// Convertidor 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 (
Diagnóstico de conversión · gratis · datos reales
Descubre por qué tu página no convierte —en 30 segundos
Pega tu enlace y recibe un diagnóstico real con Performance, Accessibility,
Best Practices, SEO y Core Web Vitals — más los problemas que están frenando tus ventas.
setDevice("desktop")}>
Escritorio
setDevice("mobile")}>
Móvil
{error
?
{error}
:
Sin registro. Datos reales de Google PageSpeed. }
100% gratis
Sin registro
Datos reales
);
}
/* ===================== ANALIZANDO ===================== */
function Analyzing({ url, device, done, error, onRetry }) {
const reduce = useRef(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches).current;
const total = ANALYZE_STEPS.length;
// 'done' viene del padre cuando termina el fetch — pero animamos visualmente
// un progreso suave mientras esperamos la respuesta del backend.
const [step, setStep] = useState(0);
useEffect(() => {
if (done) { setStep(total); return; }
const perStep = reduce ? 280 : 1100; // analisis backend tarda ~10–20s
const id = setInterval(() => {
setStep((s) => Math.min(total - 1, s + 1));
}, perStep);
return () => clearInterval(id);
}, [done]);
const pct = Math.round((step / total) * 100);
if (error) {
return (
No pudimos completar el análisis
{error}
← Intentar con otra URL
);
}
return (
Analizando {prettyHost(url)}…
Ejecutando auditoría en {device === "mobile" ? "móvil" : "escritorio"} con Google PageSpeed.
Esto puede tomar 10–20 segundos.
{ANALYZE_STEPS.map((label, i) => {
const state = i < step ? "done" : i === step ? "active" : "wait";
return (
{state === "done" ?
: state === "active" ? : null}
{label}
{state === "done" ? "Listo" : state === "active" ? "Revisando" : ""}
);
})}
);
}
/* ===================== RESULTADOS ===================== */
function PotentialCard({ pct }) {
return (
Mejora potencial de conversión estimada
+{pct}%
Resolviendo los problemas detectados, basado en tu puntaje actual.
);
}
/* ===================== CORE WEB VITALS (nuevo: datos reales PSI) ===================== */
function CWVCard({ label, value, unit, threshold, lowerBetter = true }) {
if (value === null || value === undefined) {
return (
—
{label}
Sin datos suficientes.
);
}
// 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;
return (
{display}
{label}
{tone === "success" ? "Bueno" : tone === "warning" ? "Necesita mejora" : "Pobre"}
{" "}— meta: {lowerBetter ? "≤" : "≥"} {unit === "ms" && threshold[0] > 1000 ? (threshold[0]/1000) + "s" : threshold[0] + (unit === "cls" ? "" : unit)}
);
}
function CoreWebVitalsSection({ cwv }) {
if (!cwv) return null;
return (
Core Web Vitals · datos reales (Google Lighthouse)
Métricas de rendimiento
Estas son las métricas oficiales de Google que afectan tu posicionamiento en búsqueda
y la experiencia del usuario. Medidas en vivo en tu URL.
);
}
/* ===================== 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 || {};
// 12 KPIs reales (sin estimaciones)
const kpis = [];
// 1. Páginas indexadas
if (idx.indexed_pages !== null && idx.indexed_pages !== undefined) {
kpis.push({ icon:"globe", label:"Páginas indexadas en Google", value:fmtCompact(idx.indexed_pages),
hint: idx.method === 'bing-scrape' ? "Medido vía site: query en Bing (proxy a Google)." : "Estimado del sitemap.xml.",
tone: idx.indexed_pages > 10 ? "success" : "warning" });
}
// 2. Edad del dominio
if (w.domain_age_years) {
kpis.push({ icon:"network", label:"Edad del dominio", value:w.domain_age_years, suffix:" años",
hint: w.registrar ? `Registrado en ${w.registrar}.` : "Antigüedad real (WHOIS/RDAP).",
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 auditadas", value:c.pages_crawled || 0,
hint:`${c.pages_ok || 0} OK · ${c.broken_count || 0} con problemas. Profundidad máx ${c.max_depth || 0}.`,
tone: (c.broken_count || 0) === 0 ? "success" : "warning" });
// 4. Links rotos
if ((c.broken_count || 0) > 0) {
kpis.push({ icon:"alert", label:"Links rotos detectados", value:c.broken_count,
hint:"Páginas internas que devuelven 4xx/5xx. Reducen UX y SEO.",
tone: c.broken_count > 5 ? "danger" : "warning" });
}
// 5. SSL grade
if (ssl.grade) {
kpis.push({ icon:"shield", label:"Grado SSL", value:ssl.grade, suffix:"/A+",
hint: ssl.cert_expiry ? `Certificado válido hasta ${ssl.cert_expiry}.` : "Evaluación SSL Labs.",
tone: ssl.grade.startsWith('A') ? "success" : (ssl.grade === 'B' ? "info" : "warning") });
} else if (ssl.has_https === false) {
kpis.push({ icon:"alert", label:"HTTPS", value:"NO", hint:"Tu sitio no usa HTTPS — crítico.", tone:"danger" });
}
// 6. Security headers
kpis.push({ icon:"shield", label:"Security headers", value:sec.grade || "F",
suffix:` · ${sec.present_count || 0}/${(sec.present_count || 0) + (sec.missing_count || 0)}`,
hint: (sec.missing_count || 0) > 0 ? `Faltan: ${(sec.missing || []).slice(0,2).map(m=>m.header).join(', ')}.` : "Todos presentes.",
tone: (sec.grade || "F") === "A" ? "success" : ((sec.grade || "F") === "B" ? "info" : "warning") });
// 7. CMS detectado
if (t.cms) {
kpis.push({ icon:"layers", label:"CMS detectado", value:t.cms, hint:"Plataforma identificada por fingerprinting.", tone:"info" });
} else if (t.frameworks?.length) {
kpis.push({ icon:"layers", label:"Framework principal", value:t.frameworks[0], hint:"Detectado por análisis del HTML.", tone:"info" });
}
// 8. E-commerce
if (t.ecommerce) {
kpis.push({ icon:"cursor", label:"Plataforma e-commerce", value:t.ecommerce, hint:"Tienda online detectada.", tone:"info" });
}
// 9. Schema.org
if ((s.meta?.schema_count || 0) > 0) {
kpis.push({ icon:"check", label:"Schema.org markup", value:s.meta.schema_count, suffix:" bloques",
hint: s.meta.schema_types?.length ? `Tipos: ${s.meta.schema_types.slice(0,3).join(', ')}.` : "Datos estructurados detectados.",
tone:"success" });
} else {
kpis.push({ icon:"alert", label:"Schema.org markup", value:"0", hint:"Sin datos estructurados — pierdes rich snippets en Google.", tone:"warning" });
}
// 10. Open Graph
const ogOk = s.meta?.has_og_image && s.meta?.has_og_title && s.meta?.has_og_desc;
kpis.push({ icon:"globe", label:"Open Graph completo", value: ogOk ? "Sí" : "No",
suffix: " · " + (s.meta?.og_count || 0) + " tags",
hint: ogOk ? "Bien previsualizado en redes sociales." : "Faltan tags og:image / og:title / og:description.",
tone: ogOk ? "success" : "warning" });
// 11. Sitemap
kpis.push({ icon:"layers", label:"Sitemap.xml", value: s.sitemap?.exists ? `${s.sitemap.urls_count} URLs` : "No",
hint: s.sitemap?.exists ? "Encontrado y procesado." : "Sin sitemap — Google rastrea peor.",
tone: s.sitemap?.exists ? "success" : "warning" });
// 12. Contenido
kpis.push({ icon:"message", label:"Palabras en la home", value: fmtCompact(ct.word_count || 0),
suffix: ` · ${ct.text_html_ratio || 0}%`,
hint: ct.has_thin_content ? "Contenido muy corto (<250 palabras). Google penaliza thin content." : "Densidad de contenido aceptable.",
tone: ct.has_thin_content ? "warning" : "success" });
return (
Auditoría técnica · datos reales del sitio
REAL
Auditoría técnica profunda
Crawleamos hasta {c.pages_crawled || 0} páginas internas, analizamos tecnologías, verificamos
SSL, security headers, Schema.org, sitemap y más. Todos los datos son reales ,
medidos directamente sobre tu sitio.
{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 && (
Frameworks / librerías
{t.frameworks.map((f) => (
{f}
))}
)}
{t.analytics?.length > 0 && (
Analytics / pixels
{t.analytics.map((a) => (
{a}
))}
)}
{t.hosting && (
Hosting / CDN
{t.hosting}
)}
)}
100% datos reales — sin estimaciones. Auditamos tu sitio en vivo
(crawler propio + WHOIS + SSL Labs + Bing index + análisis HTML).
Las métricas de backlinks externos NO se incluyen aquí (requieren Ahrefs/Semrush).
);
}
function CategoryGrid({ categories }) {
return (
{categories.map((c, i) => (
))}
);
}
function ProblemCard({ p, index, locked, onUnlock }) {
return (
{p.catLabel}
{!locked && Quick win }
{p.problema}
Impacto en ventas: {p.impacto}
{locked ? "Solución bloqueada" : "Cómo solucionarlo"}
{locked ? (
{p.solucion}
Desbloquea las soluciones completas
) : (
{p.solucion}
)}
);
}
/* 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 reales medidos por Google PageSpeed Insights.
{fetched && <> Analizado {ageMin < 1 ? "ahora mismo" : `hace ${ageMin} min`} · {fetched.toLocaleString('es-LA')}>}
Lighthouse tiene ±5-10 puntos de variabilidad normal entre runs.
);
}
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 (
Tu diagnóstico de conversión
Detectamos {dx.problems.length} problemas que están frenando tus ventas.
Aquí están tus quick wins gratis y la ruta para arreglar el resto.
scrollToId("captura")}>Quiero que lo arreglen por mí
scrollToId("captura")}>
Recibir reporte completo
{/* Core Web Vitals reales */}
{dx.meta?.psi_ok && }
{/* Auditoría técnica REAL (sustituye a la sección Tráfico antigua) */}
{dx.auditoria?.ok && }
{/* Categorías */}
Optimización de conversión por área
Puntaje por categoría
{/* Problemas detectados */}
Problemas de tu página web
Problemas detectados y cómo resolverlos
Las 3 primeras son quick wins con la solución completa. Las {lockedCount} restantes
se desbloquean con el reporte completo.
{free.map((p, i) => (
))}
{locked.map((p, i) => (
scrollToId("captura")} />
))}
← Analizar otra página
);
}
function Methodology({ categories }) {
return (
Cómo se calcula este puntaje
¿En qué se basa tu diagnóstico?
Combinamos Google PageSpeed Insights (Performance, Accessibility,
Best Practices, SEO, Core Web Vitals reales) con análisis del HTML de tu página
(titular, CTAs, formularios, jerarquía). Cada categoría pondera por su impacto en conversión.
{categories.map((c) => (
{c.label}
Impacto {c.weight >= 1.3 ? "alto" : c.weight >= 1.1 ? "medio" : "base"}
{c.signals.map((s, i) => (
{s}
))}
))}
Sobre los datos: Performance, Accessibility, Best Practices, SEO y Core Web Vitals
son reales de Google PageSpeed Insights API. Tráfico orgánico y backlinks son
estimaciones derivadas — la versión completa los mide con Ahrefs.
);
}
/* ===================== CAPTURA ===================== */
function Capture({ url, dx }) {
const [data, setData] = useState({ nombre: "", correo: "", sitio: url || "", objetivo: "", mensaje: "" });
const [touched, setTouched] = useState({});
const [sent, setSent] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [serverErr, setServerErr] = useState("");
useEffect(() => { setData((d) => ({ ...d, sitio: d.sitio || url || "" })); }, [url]);
const errors = {
nombre: !data.nombre.trim() ? "Ingresa tu nombre." : "",
correo: !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.correo) ? "Ingresa un correo válido." : "",
objetivo: !data.objetivo ? "Elige una opción." : "",
};
const valid = !errors.nombre && !errors.correo && !errors.objetivo;
const set = (k) => (e) => setData((d) => ({ ...d, [k]: e.target.value }));
const blur = (k) => () => setTouched((t) => ({ ...t, [k]: true }));
const submit = async () => {
setTouched({ nombre: true, correo: true, objetivo: true });
if (!valid) return;
setSubmitting(true);
setServerErr("");
try {
// Snapshot del diagnóstico actual para guardarlo con el lead
const payload = {
nombre: data.nombre.trim(),
correo: data.correo.trim().toLowerCase(),
sitio: data.sitio.trim() || null,
objetivo: data.objetivo,
mensaje: data.mensaje.trim() || null,
};
if (dx) {
payload.url_analizada = dx.url;
payload.dispositivo = dx.device;
payload.score_general = dx.overall;
payload.scores_categorias = Object.fromEntries(dx.categories.map(c => [c.key, c.score]));
payload.core_web_vitals = dx.core_web_vitals || dx.meta?.core_web_vitals || null;
payload.problemas_count = dx.problems.length;
payload.snapshot_diagnostico = {
overall: dx.overall,
potentialPct: dx.potentialPct,
problems: dx.problems,
categories: dx.categories.map(c => ({ key: c.key, label: c.label, score: c.score })),
};
}
await window.ConvierteEngine.submitLead(payload);
setSent(true);
} catch (e) {
setServerErr(e.message || "No pudimos enviar tu solicitud. Intenta de nuevo.");
} finally {
setSubmitting(false);
}
};
return (
Implementamos las mejoras por ti
¿Quieres que solucionemos estos problemas por ti?
En Sell-U LATAM implementamos las mejoras y aumentamos tu tasa de conversión.
Déjanos tus datos y te enviamos el reporte completo + una propuesta a tu medida.
Reporte completo con todas las soluciones
Plan de mejoras priorizado por impacto
Propuesta sin compromiso en menos de 24h
{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} .
{ setSent(false); setData({nombre:"",correo:"",sitio:url||"",objetivo:"",mensaje:""}); }}>Enviar otra solicitud
) : (
)}
);
}
function Field({ label, id, error, children }) {
return (
{label}
{children}
{error ? {error} : null}
);
}
/* ===================== APP ===================== */
function App() {
const [phase, setPhase] = useState("hero"); // hero | analyzing | results
const [url, setUrl] = useState("");
const [device, setDevice] = useState("desktop");
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 (
{/* Botón flotante para reset/volver al inicio del convertidor */}
{phase !== "hero" && (
← Analizar otra página
{phase === "results" && (
scrollToId("captura")}>Quiero que lo arreglen
)}
)}
{phase === "hero" && (
{ setUrl(v); if (error) setError(""); }}
device={device} setDevice={setDevice} onAnalyze={analyze} error={error} />
)}
{phase === "analyzing" && (
)}
{phase === "results" && dx && (
<>
>
)}
);
}
ReactDOM.createRoot(document.getElementById("root")).render( );