Implementación
Cómo está construido
Un único componente con API mínima: quote (obligatorio), name (obligatorio), role? (opcional), rating? (opcional, default 5). Devuelve un <article class="rcard"> con tres bloques: estrellas, <blockquote>, <footer> con avatar de iniciales + nombre + rol. CERO JavaScript. CERO emisión de JSON-LD (el schema vive en lib/seo.ts).
El componente vive en ReviewCard.astro y expone cuatro props a propósito: quote (texto plano, requerido), name (requerido), role (opcional), rating (1–5, default 5, normalizado con Math.max/min/round). Las iniciales del avatar se generan in-component: name.split(/\s+/).slice(0,2).map(w => w.charAt(0)).join("").toUpperCase() —dos letras como máximo, mayúsculas, fuente del heading sobre degradé de --c-primary—. Las estrellas son SVG inline (un <path> reutilizado por set:html sobre cinco <span>) con clase .is-on (#f5a623, amarillo cálido) o .is-off (--c-border). Cero JavaScript de gestión de estado: la card es estática a propósito (sin hover ni click).
El esquema JSON-LD NO se emite por el componente. Esta es una decisión DELIBERADA distinta a FAQAccordion (que sí acepta emitSchema=true como salida de emergencia): el ReviewCard no expone esa prop porque la regla B4 (Google penaliza self-serving reviews) exige un gate GLOBAL (SITE.allowSelfReviews) que no debe duplicarse a nivel componente. El patrón canónico es: (a) helper PURO reviewSchema({items, aggregate?}) en lib/seo.ts —espejo de faqSchema()—, devuelve el bloque {aggregateRating, review[]} listo para componer en otro nodo; (b) helper INTERNO emitReviews() dentro de productSchema/serviceSchema, gateado por SITE.allowSelfReviews. Un emisor por página (B3), un gate global por sitio (B4). CAVEAT mayo 2026 → presente: Google solo pinta estrellas en SERP para algunos tipos schema (Product/Recipe/Movie/Book); LocalBusiness/Service no rinden visualmente pero siguen siendo válidos.