/* Landings explicativas por prueba de especialidad (plantilla única). * * Datos: src/pruebas-landings-data.js. Reusa las piezas del formato FIT / * Helicobacter de src/pages-recuperadas.jsx (RcpHeroAzul, RcpClaves, * RcpRelacionados, RcpPro, RcpAviso, useRevela, useEstudiosApi) y su hoja de * estilo. Lo propio de esta plantilla vive en , scope `.plp`. * * Orden: hero → claves → pruebas del catálogo (en vivo) → explicación por * audiencia → lo que dice el doctor → descargas → relacionadas → cierre. */ const PL = () => window.PRUEBAS_LANDINGS; /* El nombre de la prueba no se parte entre renglones («Q / Sepsis»). */ const plpNb = (s) => String(s || "").replace(/ /g, "\u00a0"); function PruebaStyles() { return ; } /* Estudios del tema en InfectoApp: los ids conocidos y, además, los que * encuentre la búsqueda y cumplan la regla de nombre (por si dieron de alta la * prueba con un id nuevo). Lo que no existe en InfectoApp no se muestra. */ function useEstudiosDelTema(catalogo) { const [lista, setLista] = React.useState(null); React.useEffect(() => { const API = window.InfectoAPI, E = window.InfectoEnrich; if (!API || !catalogo) { setLista([]); return; } let vivo = true; const ids = catalogo.ids || []; const porId = Promise.all(ids.map((id) => API.getEstudio(id).catch(() => null))); const porBusqueda = catalogo.buscar ? API.getEstudios({ q: catalogo.buscar, per_page: 50 }).then((r) => r.estudios).catch(() => []) : Promise.resolve([]); Promise.all([porId, porBusqueda]).then(([a, b]) => { if (!vivo) return; const vistos = {}; const extra = b.filter((s) => catalogo.nombre && catalogo.nombre.test(s.name || s.titulo || "")); const ocultar = catalogo.ocultar || []; const todos = a.filter(Boolean).concat(extra).filter((s) => { if (ocultar.indexOf(Number(s.id)) >= 0) return false; if (vistos[s.id]) return false; vistos[s.id] = true; return true; }); setLista(todos.map((s) => (E ? E.enriquecer(s) : s))); }); return () => { vivo = false; }; }, [catalogo]); return lista; } function plpWa(texto) { return PL().WA + (texto ? "?text=" + encodeURIComponent(texto) : ""); } function PlpBoton({ cta, clase, navigate }) { if (!cta) return null; if (cta.tipo === "orden") { return ( {cta.texto} ); } return ( {cta.texto} ); } function PlpComparativo({ c }) { return (
{c.titulo}
Dato {(c.cols || [])[0] || "Q Sepsis"} {(c.cols || [])[1] || "Hemocultivo"}
{c.filas.map((f) => (
{f.k} {f.q} {f.h}
))}
Fuente: {c.fuente}
); } function PlpEstudios({ l, openStudy, openCatalog }) { const lista = useEstudiosDelTema(l.catalogo); const et = (l.catalogo && l.catalogo.etiquetas) || {}; return ( ); } /* El nombre oficial de InfectoApp suele venir en mayúsculas sostenidas. */ function N_tituloOficial(nombre) { const N = window.InfectoNorm; return "Nombre en InfectoApp: " + (N && N.tituloClinico ? N.tituloClinico(nombre) : nombre); } /* Las tres vistas se renderizan siempre: la inactiva se oculta con `hidden`, * no se desmonta, para que el contenido de todas quede en el DOM (SEO). */ function PlpAudiencias({ l, navigate }) { const auds = l.audiencias || []; const [activa, setActiva] = React.useState(auds[0] && auds[0].id); if (!auds.length) return null; const teclas = (e, i) => { if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return; const n = (i + (e.key === "ArrowRight" ? 1 : -1) + auds.length) % auds.length; setActiva(auds[n].id); const b = document.getElementById("plp-tab-" + auds[n].id); if (b) b.focus(); }; return (
Explicado para ti

¿Qué es {plpNb(l.nombre)}?

La misma prueba, contada según quién la necesita.

{auds.map((x, i) => ( ))}
{auds.map((a) => { const base = a.hereda ? auds.find((x) => x.id === a.hereda) : null; return ( ); })}
); } /* Secciones del material del doctor. Tipos: * tabla — cols + filas (la primera celda es encabezado de fila) * grupos — tarjetas con lista (gérmenes, anticuerpos…); `italica` para nombres científicos * cifras — números grandes sobre azul * pasos — algoritmo numerado * notas — puntos numerados (interpretación, criterios) */ function PlpSeccion({ s }) { return (
{s.kicker && {s.kicker}}

{s.titulo}

{s.intro &&

{s.intro}

} {s.tipo === "tabla" && (
{s.cols.map((c, i) => )} {s.filas.map((f, i) => ( {(f.celdas || f).slice(1).map((c, j) => ( ))} ))}
{c}
{(f.celdas || f)[0]}{s.cols[j + 1]}{c}
)} {s.tipo === "grupos" && (
= 4 ? " plp-cuatro" : "")}> {s.grupos.map((g) => (
{g.grupo} {g.sub &&
{g.sub}
} {g.texto &&

{g.texto}

} {g.items &&
    {g.items.map((x) =>
  • {x}
  • )}
}
))}
)} {s.tipo === "cifras" && (
{s.cifras.map((c, i) => (
{c.v}{c.k}
))}
)} {s.tipo === "pasos" && (
    {s.pasos.map((p, i) => (
  1. {p.titulo}

    {p.texto}

  2. ))}
)} {s.tipo === "notas" && (
{s.puntos.map((p, i) => (
{i + 1}

{p}

))}
)} {s.nota && {s.nota}}
); } function PlpDoctor({ l, openArticle }) { const d = l.doctor; if (!d) return null; return (
{d.sinDoctor ? "Lo que dice la infografía" : "Lo que dice el especialista"}

{d.sinDoctor ? <>{plpNb(l.nombre)}, en detalle : <>El Dr. Samuel Navarro explica {plpNb(l.nombre)}}

{d.intro &&

{d.intro}

}
{/* Material sin autoría del doctor: no se le atribuye cita ni se muestra su retrato. */} {!d.sinDoctor &&
Dr. Samuel Navarro Álvarez
{d.cita ?
«{d.cita}»
:
Esta explicación sale del material que el doctor preparó sobre la prueba.
} Dr. Samuel Navarro Álvarez · Infectólogo e internista · Director médico de InfectoLab
} {d.bloques && (
{d.bloques.map((b, i) => (
{b.titulo} {[].concat(b.texto).map((x, j) =>

{x}

)}
))}
)} {/* Figura sola a ancho completo; con `texto`, va junto a su explicación. */} {d.figuras && d.figuras.filter((f) => !f.texto).length > 0 && (
{d.figuras.filter((f) => !f.texto).map((f) => (
{f.alt}
{f.pie}
))}
)} {(d.figuras || []).filter((f) => f.texto).map((f) => (
{f.alt}

{f.titulo}

{[].concat(f.texto).map((x, i) =>

{x}

)}
))} {(d.secciones || []).map((s) => )}
{d.referencias && <>{d.referenciasTitulo || "Referencias"}: {d.referencias.join(" · ")}} {d.articulo && openArticle && (
)}
); } function PlpDescargas({ l }) { if (!l.descargas || !l.descargas.length) return null; return (
{l.doctor && l.doctor.sinDoctor ? "Material de InfectoLab" : "Material del Dr. Navarro"}

Descarga y comparte

{l.doctor && l.doctor.sinDoctor ? "La infografía de " : "El material que preparó el doctor sobre "}{plpNb(l.nombre)}.

{l.infografia && ( {l.infografia.alt} Ver la infografía completa {l.infografia.pie || "Se abre en una pestaña nueva."} )}
{l.descargas.map((f) => ( {f.formato} {f.titulo} {f.para} · {f.formato}, {f.peso} ))}
); } /* Arte del hero: comparativo, o la portada ilustrada del tema. */ function PlpArte({ l }) { if (l.hero.comparativo) return ; if (!l.hero.imagen) return null; return (
{l.hero.imagenAlt {l.hero.chips && (
{l.hero.chips.map((c) => {c.texto})}
)}
); } /* ── SEO por landing ── * El sitio es un SPA: index.html trae título y description genéricos. Al * montar la landing se escriben title, description, canonical, Open Graph y * JSON-LD (MedicalWebPage → MedicalTest + BreadcrumbList); al salir se * restaura lo anterior. Ojo: esto sólo lo ve un buscador que ejecute JS; el * HTML servido sigue siendo el cascarón (ver bitácora, problema de SPA). */ const PLP_ORIGEN = "https://infectolab.mx"; function plpMeta(atributo, clave, valor) { let el = document.head.querySelector("meta[" + atributo + '="' + clave + '"]'); const nuevo = !el; const antes = el ? el.getAttribute("content") : null; if (!el) { el = document.createElement("meta"); el.setAttribute(atributo, clave); document.head.appendChild(el); } el.setAttribute("content", valor); return () => { if (nuevo) el.remove(); else el.setAttribute("content", antes); }; } function plpJsonLd(l) { if (l.jsonld) return l.jsonld; /* páginas que no son de una prueba (p. ej. Calidad) */ const url = PLP_ORIGEN + l.ruta; const img = l.og && l.og.imagen ? PLP_ORIGEN + l.og.imagen : undefined; return { "@context": "https://schema.org", "@graph": [ { "@type": "MedicalWebPage", "@id": url + "#pagina", url: url, name: l.title, description: l.description, inLanguage: "es-MX", lastReviewed: l.revisadoISO, image: img, audience: [ { "@type": "PeopleAudience", audienceType: "Pacientes" }, { "@type": "MedicalAudience", audienceType: "Clinician" }, ], reviewedBy: (l.doctor && l.doctor.sinDoctor) ? undefined : { "@type": "Physician", name: "Dr. Samuel Navarro Álvarez", url: PLP_ORIGEN + "/dr-samuel-navarro-alvarez/" }, publisher: { "@type": "MedicalOrganization", name: "InfectoLab", url: PLP_ORIGEN + "/" }, breadcrumb: { "@id": url + "#migas" }, about: l.hero && { "@type": "MedicalTest", name: l.nombre, alternateName: l.nombreAlterno, description: l.hero.entrada, usedToDiagnose: (l.diagnostica || []).map((n) => ({ "@type": "MedicalCondition", name: n })), }, }, { "@type": "BreadcrumbList", "@id": url + "#migas", itemListElement: [ { "@type": "ListItem", position: 1, name: "Inicio", item: PLP_ORIGEN + "/" }, { "@type": "ListItem", position: 2, name: "Estudios", item: PLP_ORIGEN + "/estudios-de-laboratorio/" }, { "@type": "ListItem", position: 3, name: l.nombre, item: url }, ], }, ], }; } function usePlpSeo(l) { React.useEffect(() => { const url = PLP_ORIGEN + l.ruta; const tituloAntes = document.title; document.title = l.title; const deshacer = [ plpMeta("name", "description", l.description), plpMeta("property", "og:type", "article"), plpMeta("property", "og:title", l.title), plpMeta("property", "og:description", l.description), plpMeta("property", "og:url", url), plpMeta("property", "og:locale", "es_MX"), plpMeta("property", "og:site_name", "InfectoLab"), plpMeta("name", "twitter:card", "summary_large_image"), ]; /* Landing armada pero no publicada: fuera de buscadores. */ if (l.publicada !== true) deshacer.push(plpMeta("name", "robots", "noindex, nofollow")); if (l.og && l.og.imagen) { deshacer.push(plpMeta("property", "og:image", PLP_ORIGEN + l.og.imagen)); if (l.og.alt) deshacer.push(plpMeta("property", "og:image:alt", l.og.alt)); } let canon = document.head.querySelector('link[rel="canonical"]'); const canonNuevo = !canon; const canonAntes = canon ? canon.getAttribute("href") : null; if (!canon) { canon = document.createElement("link"); canon.rel = "canonical"; document.head.appendChild(canon); } canon.setAttribute("href", url); /* El HTML estático trae un JSON-LD para bots; al montar la landing se * reemplaza por el de la página viva para no duplicar structured data. */ document.querySelectorAll('script[type="application/ld+json"][data-prerender]').forEach((s) => s.remove()); const ld = document.createElement("script"); ld.type = "application/ld+json"; ld.id = "plp-jsonld"; ld.textContent = JSON.stringify(plpJsonLd(l)); document.head.appendChild(ld); return () => { document.title = tituloAntes; deshacer.forEach((f) => f()); if (canonNuevo) canon.remove(); else canon.setAttribute("href", canonAntes); ld.remove(); }; }, [l.slug]); } function PruebaLandingPage({ landing, navigate, openCatalog, openStudy, openArticle }) { const l = landing; const raiz = React.useRef(null); useRevela(raiz); usePlpSeo(l); /* «Ver también» sólo enlaza landings publicadas. */ const verTambien = (l.verTambien || []) .map((v) => ({ v, otra: (PL().LANDINGS || []).find((x) => x.slug === v.slug && x.publicada === true) })) .filter((x) => x.otra); return (
navigate("home") }, { label: "Estudios", go: () => navigate("catalogo") }, { label: l.nombre }, ]} eyebrow={l.hero.eyebrow} icono="dna" titulo={l.hero.titulo} enfasis={l.hero.enfasis} entrada={l.hero.entrada} cta={ } arte={} /> {l.relacionadas && ( )} {/* Enlace a otras landings del mismo tema (las relacionadas llevan a la ficha). */} {verTambien.length > 0 && (
)}

{l.cierre.titulo}

{l.cierre.texto}

Información para orientar. El resultado de {l.nombre} lo interpreta el médico tratante y no sustituye su valoración. Contenido revisado: {l.revisado}.
); } /* La página de Calidad (src/pages-landings.jsx) reusa el mismo SEO. */ window.usePlpSeo = usePlpSeo;