Guía de Web Components
Construyendo Web Components —
Guía Práctica
Principios, patrones y buenas prácticas contrastadas en producción, extraídas de más de 50 componentes reales. Sin dependencia de framework, centrado en el estándar, y con opiniones propias sin complejos.
Consejo: en escritorio, verás una tabla de contenidos fija a la izquierda.
01 Cuándo (y cuándo no) construir un Web Component
El error más grande que cometen los desarrolladores no es construir malos componentes — es construir
componentes innecesarios. Antes de llegar a customElements.define, hazte cuatro preguntas.
Los cuatro criterios
Un fragmento de interfaz merece ser un custom element si cumple al menos uno de estos:
- Es genuinamente reutilizable. Aparecerá en más de un lugar, en más de un proyecto, o tiene un propósito suficientemente genérico como para que otros equipos puedan usarlo sin modificarlo.
- Ofrece una presentación que no existe en HTML. Un componente
<my-rating-stars>tiene sentido. Un<my-div-with-padding>no. - Tiene un comportamiento encapsulado con significado propio. Hace algo — gestiona interacciones, mantiene estado, orquesta elementos hijos. El layout puro no es comportamiento.
- Necesita abstraer su diseño de su contexto. Puede insertarse en cualquier página o cualquier framework sin saber nada sobre la aplicación que lo aloja.
La trampa del framework
La mayor parte de la formación en frontend nos enseña cómo usar un framework, no cuándo usarlo. El modelo mental resultante es: toda interfaz es un componente, y todo componente necesita un framework. Los web components son la respuesta del estándar a esto: se ejecutan de forma nativa en cualquier navegador, no arrastran ningún runtime, e interoperan con React, Vue, Angular, Svelte — o sin ningún framework.
Muchos sitios no necesitan un framework. Una página de producto, un sitio de documentación, un portfolio — pueden construirse con HTML, CSS y un puñado de custom elements. Empieza con la plataforma. Añade abstracciones solo cuando la plataforma se quede corta.
02 Un componente, una responsabilidad
El Principio de Responsabilidad Única se aplica a los web components con especial fuerza, porque los componentes se exponen como API pública. Un componente que hace demasiado es difícil de usar, difícil de probar e imposible de razonar.
Reconocer el exceso de responsabilidades
La señal más clara de que un componente ha crecido demasiado es la longitud del archivo. Si tu archivo supera las ~300 líneas, casi seguro que está haciendo más de una cosa. Otras señales:
- Tiene más de cinco o seis atributos observados.
- Su
connectedCallbacktiene 20+ líneas de lógica de inicialización. - Encuentras ramas de renderizado condicional para estados fundamentalmente distintos.
- Su README tiene más de tres ejemplos de uso, todos significativamente diferentes.
Estrategias de división
Cuando un componente se vuelve demasiado complejo, divídelo por sus costuras naturales. Patrones habituales:
Split: container / presentational<!-- Bad: one component handles data fetching AND rendering -->
<user-profile-card user-id="42"></user-profile-card>
<!-- Better: separate concerns -->
<user-data-provider user-id="42">
<user-profile-card></user-profile-card>
</user-data-provider>Mixins para comportamiento compartido
Si varios componentes comparten un comportamiento (arrastrar y soltar, carga diferida, un patrón ARIA concreto), extráelo a un mixin en lugar de una clase base o una importación de utilidad. Los mixins mantienen la jerarquía de clases plana y el comportamiento compartido desacoplado.
mixin-focusable.jsexport const FocusableMixin = (Base) => class extends Base {
connectedCallback() {
super.connectedCallback?.();
if (!this.hasAttribute('tabindex')) {
this.setAttribute('tabindex', '0');
}
this.addEventListener('keydown', this.#handleKeydown);
}
disconnectedCallback() {
super.disconnectedCallback?.();
this.removeEventListener('keydown', this.#handleKeydown);
}
#handleKeydown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.click();
}
};
};03 Cómo nombrar tus componentes
Los nombres de etiquetas de custom elements son globales y no se pueden redefinir. Elígelos con cuidado, porque un conflicto en el registro romperá silenciosamente uno de los dos elementos en colisión.
Usa un prefijo de espacio de nombres
Los nombres de etiqueta deben contener un guion (así es como la especificación distingue los custom elements de los futuros elementos HTML). Usa el nombre de tu organización o sistema de diseño como prefijo:
<!-- Bad: likely to collide -->
<calendar></calendar>
<modal></modal>
<!-- Good: namespaced -->
<manufosela-calendar></manufosela-calendar>
<my-ds-modal></my-ds-modal>
El prefijo sirve también como mecanismo de descubrimiento: los desarrolladores hacen grep de manufosela-
para encontrar todos los componentes de una biblioteca concreta.
Sé descriptivo, no ingenioso
El nombre debe comunicar qué es el componente, no cómo está implementado.
<mf-accordion> es bueno. <mf-collapsible-details-wrapper> es demasiado
específico de implementación. <mf-thing> no dice nada.
| Patrón | Ejemplo | Veredicto |
|---|---|---|
| Sin espacio de nombres | <calendar> | Riesgo de colisión — evitar |
| Prefijo de organización | <mf-calendar> | Bien |
| Prefijo de sistema de diseño | <manufosela-calendar> | Mejor — sin ambigüedad |
| Versión en el nombre | <mf-calendar-v2> | Solo si convive necesariamente con v1 |
04 Flujo de datos: atributos hacia dentro, eventos hacia fuera
Esta es la regla de oro del diseño de API en web components. Los datos fluyen hacia abajo mediante atributos y propiedades. Los eventos fluyen hacia arriba. Rompe esta regla y estarás peleando con tu componente para siempre.
Atributos vs. propiedades — conoce la diferencia
Un atributo forma parte del marcado HTML — vive en el árbol del DOM, se serializa como cadena de texto y puede establecerse antes de que el elemento esté registrado. Una propiedad pertenece al objeto JavaScript — se asigna programáticamente y puede contener cualquier valor.
// Attribute — set in HTML or via setAttribute
<mf-badge count="5" variant="info"></mf-badge>
element.setAttribute('count', '5');
// Property — set via JS, ideal for complex data
element.items = [{ id: 1, label: 'Apple' }, { id: 2, label: 'Banana' }];
element.config = { debounce: 300, threshold: 0.5 };Usa estas heurísticas:
- Atributos: booleanos, cadenas, números, enumeraciones, IDs. Todo lo que escribirías en HTML.
- Propiedades: arrays, objetos, funciones, datos binarios. Todo lo que no puede hacer un viaje de ida y vuelta por una cadena sin perder información.
Reflejar atributos en propiedades
Expón tus atributos observados como propiedades JS mediante getters/setters para que los consumidores puedan usar cualquiera de las dos APIs:
mf-badge.jsclass MfBadge extends HTMLElement {
static observedAttributes = ['count', 'variant'];
get count() {
return Number(this.getAttribute('count') ?? 0);
}
set count(val) {
this.setAttribute('count', String(val));
}
get variant() {
return this.getAttribute('variant') ?? 'default';
}
set variant(val) {
this.setAttribute('variant', val);
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal === newVal) return;
this.#render();
}
#render() {
// ...
}
}
customElements.define('mf-badge', MfBadge);Custom events para la comunicación saliente
Cuando un componente necesita informar al exterior de que algo ha ocurrido, lanza un
CustomEvent. Usa una convención de nombres consistente: minúsculas, separado por guiones,
prefijado con el nombre del componente si hay riesgo de colisión.
// In your component — something happened
this.dispatchEvent(new CustomEvent('mf-select', {
bubbles: true, // propagate up the DOM tree
composed: true, // cross shadow DOM boundaries
detail: {
value: this.#selectedValue,
label: this.#selectedLabel,
},
}));
// Consumer side
document.querySelector('mf-combobox')
.addEventListener('mf-select', (e) => {
console.log('Selected:', e.detail.value);
});No lances eventos en respuesta a asignaciones de propiedades
Si el padre hace element.value = 'foo', el padre ya sabe
cuál es el nuevo valor. Lanzar un evento change en respuesta crea un bucle de retroalimentación
y sorprende a los consumidores. Reserva los eventos para las interacciones del usuario y las finalizaciones asíncronas internas —
cosas que el padre no pudo haber iniciado.
05 Comunicación entre componentes
Los componentes deben ser autónomos. No deberían saber nada de sus hermanos. No deberían recorrer el árbol del DOM buscando padres. La comunicación ocurre a través de eventos.
Eventos antes que jerarquía
Cuando el componente A necesita decirle algo al componente B, la tentación es escribir
parent.querySelector('mf-b').doSomething(). Resístela. En el momento en que introduces
una referencia directa, has acoplado dos componentes que deberían ser independientes.
En su lugar, lanza un evento hacia arriba y deja que la aplicación (o un contenedor coordinador) retransmita los datos hacia abajo como atributo:
// mf-search dispatches upward
searchEl.addEventListener('mf-search', (e) => {
// Application layer — the only one that knows about both components
resultsEl.setAttribute('query', e.detail.query);
resultsEl.setAttribute('loading', '');
fetchResults(e.detail.query).then((data) => {
resultsEl.removeAttribute('loading');
resultsEl.items = data;
});
});Pensamiento fenotípico
El Atomic Design organiza los componentes por tamaño: átomos, moléculas, organismos. Es un modelo mental útil, pero fomenta pensar en los componentes como un árbol genealógico (padres que contienen hijos que contienen nietos). Esto lleva al acoplamiento rígido.
Un marco más útil es el fenotípico: clasifica los componentes por sus rasgos visibles
y cómo interactúan con su entorno. Un <mf-tooltip> se define por cómo
aparece (cerca de su disparador, flotando, transitorio) y cómo
responde a su entorno (foco, hover, scroll). No le importa si su disparador es un átomo botón o una molécula de formulario compleja.
Los componentes deben ser simbióticos, no familiares. Coexisten y cooperan a través de convenciones compartidas (eventos, atributos, slots), no a través del conocimiento directo mutuo.
06 Usa el light DOM
El shadow DOM es poderoso, pero es opaco — para los motores de búsqueda, para los lectores de pantalla (en parte), y para la capa de aplicación. Úsalo para la implementación; usa slots y light DOM para el contenido.
El shadow DOM es invisible para los crawlers
El contenido dentro de un shadow root no está indexado por los motores de búsqueda (a fecha de 2026, Google puede indexarlo parcialmente, pero la situación es inconsistente). El contenido que necesita ser descubierto — encabezados, texto del cuerpo, nombres de productos, etiquetas de navegación — pertenece al light DOM.
Diseña tus componentes para que acepten HTML semántico como contenido mediante slots, y deja que el componente aplique el tratamiento visual:
mf-card.html (consumer)<!-- Light DOM content — indexable, accessible, semantic -->
<mf-card>
<h2 slot="title">Product Name</h2>
<p slot="description">A short description of this product.</p>
<img slot="image" src="/product.jpg" alt="Product photo" />
<a slot="action" href="/buy">Buy now</a>
</mf-card>mf-card.js (component)class MfCard extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host { display: block; border-radius: 0.5rem; overflow: hidden; }
.card { display: grid; gap: 1rem; padding: 1.5rem; }
/* style slots — the light DOM content inherits parent styles too */
::slotted([slot="title"]) { font-size: 1.25rem; margin: 0; }
::slotted([slot="action"]) { color: var(--primary); }
</style>
<div class="card">
<slot name="image"></slot>
<slot name="title"></slot>
<slot name="description"></slot>
<slot name="action"></slot>
</div>
`;
}
}Nunca pases JSON como atributos
Este patrón aparece en todas partes y es incorrecto en varios niveles:
<!-- DO NOT DO THIS -->
<mf-list items='[{"id":1,"label":"Apple"},{"id":2,"label":"Banana"}]'>
</mf-list>Los problemas: no es indexable, se rompe cuando el JSON contiene comillas, es inaccesible para las tecnologías asistivas, y filtra la estructura de datos al marcado.
El enfoque correcto es poner los datos en el light DOM como HTML semántico, y dejar que el componente los lea y los mejore en el momento del renderizado:
the right way<mf-list>
<li data-id="1">Apple</li>
<li data-id="2">Banana</li>
</mf-list>
<!-- component reads it at connectedCallback -->mf-list.jsconnectedCallback() {
const items = [...this.querySelectorAll('li')].map((li) => ({
id: li.dataset.id,
label: li.textContent.trim(),
}));
this.#render(items);
}El contenido ya estaba en el light DOM. Los motores de búsqueda lo indexan. Los lectores de pantalla pueden acceder a él incluso antes de que cargue JavaScript. El componente lo mejora — no lo reemplaza.
07 Gestión de errores: falla ruidosamente
Los fallos silenciosos son los peores. Si falta un atributo requerido, si una llamada a la API devuelve basura, si la configuración es inválida — dilo con claridad. Tu yo del futuro te lo agradecerá.
Lanza excepciones ante requisitos críticos
Si un componente no puede funcionar sin un atributo o propiedad concretos, lanza
un error en connectedCallback. No renderices un estado vacío roto. No registres
una advertencia que quedará enterrada en la consola. Lanza.
connectedCallback() {
const src = this.getAttribute('src');
if (!src) {
throw new Error(
`<mf-video-player> requires a "src" attribute. ` +
`Example: <mf-video-player src="/video.mp4"></mf-video-player>`
);
}
this.#init(src);
}Valida, no asumas
Cuando el valor de un atributo viene del exterior (es decir, de un consumidor), valídalo contra el conjunto de valores esperados antes de usarlo:
static #VALID_VARIANTS = new Set(['default', 'info', 'success', 'warning', 'error']);
attributeChangedCallback(name, _, newVal) {
if (name === 'variant') {
if (!MfBadge.#VALID_VARIANTS.has(newVal)) {
console.warn(
`<mf-badge>: Unknown variant "${newVal}". ` +
`Valid values: ${[...MfBadge.#VALID_VARIANTS].join(', ')}`
);
return; // don't render an invalid state
}
}
this.#render();
}Degradación elegante vs. fallos duros
Distingue entre requisitos verdaderamente críticos (ausente = roto) y mejoras opcionales (ausente = funcionalidad reducida):
- Crítico:
srcen un reproductor de medios,hrefen un componente de enlace → lanza excepción - Opcional:
varianten un badge,iconen un botón → avisa + usa valor por defecto - Mejora:
prefers-reduced-motion, modo oscuro → detecta + adapta silenciosamente
08 Accesibilidad primero
La accesibilidad no es un elemento de una lista de comprobación. Es una métrica de calidad. Un componente que no puede usarse con teclado o con un lector de pantalla es un componente roto.
Usa elementos nativos siempre que sea posible
La mayor ganancia de accesibilidad es no reimplementar lo que el navegador
ya hace. Si necesitas un diálogo, envuelve <dialog>. Si necesitas un widget de revelación,
construye sobre <details> y <summary>. El navegador te da gestión
de teclado, gestión del foco y roles ARIA gratis.
class AppModal extends HTMLElement {
#dialog;
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host { display: contents; }
dialog { border: none; border-radius: 0.75rem; padding: 0; }
dialog::backdrop { background: rgba(0,0,0,0.6); }
</style>
<dialog part="dialog">
<slot></slot>
</dialog>
`;
this.#dialog = shadow.querySelector('dialog');
}
show() { this.#dialog?.showModal(); }
close() { this.#dialog?.close(); }
}ARIA cuando el nativo se queda corto
Cuando no hay ningún elemento nativo para el patrón que estás construyendo, añade ARIA explícitamente.
Aplícalo al elemento host mediante :host o establécelo en connectedCallback:
connectedCallback() {
// Set ARIA role on the host element
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'tablist');
}
// Manage focus
this.setAttribute('tabindex', '0');
this.addEventListener('keydown', this.#handleKeyNav);
}CSS custom properties para la tematización
Usa var() con fallbacks razonables para que tu componente participe en el sistema de diseño de la página
sin requerir configuración:
:host {
display: block;
/* The host page can override these; fallbacks ensure the component works standalone */
--_bg: var(--mf-surface, #1c1b1b);
--_text: var(--mf-on-surface, #f4f5f2);
--_accent: var(--mf-primary, #00e0b3);
--_radius: var(--mf-radius, 0.5rem);
background: var(--_bg);
color: var(--_text);
border-radius: var(--_radius);
}Responsivo y sensible al movimiento
Los componentes deben funcionar en cualquier tamaño de ventana. Si tienes animaciones, respeta
prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
:host, :host * {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
@media (prefers-color-scheme: dark) {
:host {
--_bg: var(--mf-surface, #131313);
}
}Imágenes: SVG antes que Base64
El SVG en línea escala nítidamente a cualquier resolución, no requiere petición HTTP adicional, admite
animación CSS y mantiene el tamaño del archivo pequeño. Las imágenes codificadas en Base64 no tienen ninguna de esas cualidades.
Usa SVG para iconos y gráficos simples. Usa <img> con alt descriptivo
para fotografías.
09 Rendimiento
Algo que el ecosistema de frameworks no enfatiza suficiente: no necesitas una librería para construir web components. El navegador ya incluye todo lo que necesitas.
Sin framework necesario
Los web components son parte de la especificación HTML. HTMLElement, customElements,
attachShadow, MutationObserver, IntersectionObserver
— todas son APIs estándar del navegador, sin dependencias, sin coste de runtime.
// This ships as ~2KB minified + gzipped
// No React, no Vue, no Angular, no Lit (though Lit is great too)
class MfTooltip extends HTMLElement {
static observedAttributes = ['text', 'placement'];
connectedCallback() {
this.attachShadow({ mode: 'open' }).innerHTML = `
<style>/* ... */</style>
<slot></slot>
<span class="tooltip" part="tooltip"></span>
`;
this.#setup();
}
// ...
}El primer pintado importa
Define tus componentes de forma que eviten el cambio de diseño (layout shift). Establece display en
:host a un valor no en línea para que el elemento ocupe espacio antes de que
se ejecute su JavaScript. Usa :not(:defined) para ocultar componentes no configurados
de forma elegante:
/* In your component styles */
:host { display: block; }
/* In the host page CSS — hide undefined components to prevent FOUC */
mf-card:not(:defined),
mf-modal:not(:defined) {
visibility: hidden;
}Carga diferida donde corresponda
Los componentes que están por debajo del pliegue o que se muestran condicionalmente no necesitan estar en la
ruta crítica. Usa import() dinámico junto con IntersectionObserver:
// Only load the component definition when it enters the viewport
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
import('./mf-heavy-chart.js');
observer.unobserve(entry.target);
}
}
});
document.querySelectorAll('mf-heavy-chart')
.forEach((el) => observer.observe(el));Minimiza las dependencias externas
Cada dependencia que añades es una dependencia que tus consumidores tienen que descargar. Pregúntate por cada una: ¿realmente necesito una librería, o hay una API del navegador que hace lo mismo? Hoy en día, la respuesta es "API del navegador" más a menudo que nunca.
| Tarea | Librería | Alternativa nativa |
|---|---|---|
| Animaciones | GSAP | Web Animations API / transiciones CSS |
| Detección de intersección | jQuery waypoints | IntersectionObserver |
| Detección de redimensionado | resize-sensor | ResizeObserver |
| Mutación del DOM | — | MutationObserver |
| Clonado profundo | lodash.cloneDeep | structuredClone() |
| IDs únicos | uuid | crypto.randomUUID() |
10 Documentación
Un componente sin documentar es un componente roto. Si alguien no puede entender cómo usarlo en cinco minutos, no lo usará — lo reescribirá.
La plantilla de README
Todo paquete de componente debería tener un README con esta estructura:
Secciones recomendadas del README
- Título — nombre del componente, descripción en una línea, badge de npm, enlace a demo
- Instalación — comando npm, snippet CDN, sentencia de importación
- Uso — ejemplo mínimo funcional en HTML y JS
- Atributos — tabla: nombre, tipo, valor por defecto, descripción
- Propiedades — tabla: nombre, tipo, descripción
- Eventos — tabla: nombre, forma del detail, cuándo se lanza
- CSS Custom Properties — tabla: nombre, valor por defecto, propósito
- Slots — tabla: nombre, descripción
- Notas — compatibilidad con navegadores, problemas conocidos, notas de migración
custom-elements.json (el manifiesto)
El Custom Elements Manifest es un archivo JSON legible por máquinas que describe la API de tu componente. Herramientas como Storybook, extensiones de VS Code y generadores de documentación lo consumen automáticamente.
# Generate from JSDoc comments using the analyser
npx @custom-elements-manifest/analyzer analyze --globs "src/**/*.js"Anota tu componente con JSDoc para que el analizador pueda extraer el manifiesto:
documented component/**
* A badge component for displaying short status labels.
*
* @element mf-badge
*
* @attr {string} [variant=default] - Visual variant: default | info | success | warning | error
* @attr {number} [count] - Numeric count to display
*
* @cssprop [--mf-badge-bg] - Badge background color
* @cssprop [--mf-badge-radius=9999px] - Border radius
*
* @fires mf-dismiss - Fired when the dismiss button is clicked
*/
class MfBadge extends HTMLElement { /* ... */ }11 Mantenibilidad
El mejor componente es el que sigue teniendo sentido seis meses después, cuando otro desarrollador abre el archivo a las 9 de la noche para corregir un bug en producción.
Versionado semántico
Sigue semver de forma estricta.
Añadir un nuevo atributo opcional es un minor. Renombrar uno existente es un
major (es un cambio que rompe compatibilidad). Corregir un bug es un patch.
Mantén un CHANGELOG.md.
Privado por defecto
Usa campos privados de clase de JavaScript (prefijo #) para métodos internos y estado.
Todo lo que no forme parte de la API pública debería ser privado. Esto mantiene la superficie documentada
mínima y evita que los consumidores dependan accidentalmente de detalles de implementación.
class MfAccordion extends HTMLElement {
// Private state — not accessible from outside
#isOpen = false;
#panel = null;
// Private methods
#toggle() {
this.#isOpen = !this.#isOpen;
this.#panel?.setAttribute('aria-hidden', String(!this.#isOpen));
}
// Public API — documented and stable
open() { this.#isOpen = true; this.#render(); }
close() { this.#isOpen = false; this.#render(); }
toggle() { this.#toggle(); }
get open() { return this.#isOpen; }
}Tests: cobertura del 80% o más
Los web components son testables con las APIs estándar del DOM. Usa
Vitest con
happy-dom o
@open-wc/testing
para un entorno de pruebas en un navegador real:
import { fixture, html, expect } from '@open-wc/testing';
import '../src/mf-badge.js';
describe('mf-badge', () => {
it('reflects the count attribute', async () => {
const el = await fixture(html`<mf-badge count="5"></mf-badge>`);
expect(el.count).to.equal(5);
});
it('fires mf-dismiss when the dismiss button is clicked', async () => {
const el = await fixture(html`<mf-badge dismissible>Label</mf-badge>`);
let fired = false;
el.addEventListener('mf-dismiss', () => { fired = true; });
el.shadowRoot.querySelector('[part="dismiss"]').click();
expect(fired).to.be.true;
});
});JavaScript moderno — siempre
Escribe contra el estándar ECMAScript actual. Campos privados de clase, optional chaining,
nullish coalescing, structuredClone, crypto.randomUUID, top-level await —
todo esto está disponible en los navegadores evergreen. El código orientado a IE11 o entornos
heredados queda fuera del alcance de esta guía.
// Modern, clean, idiomatic
class MfSelect extends HTMLElement {
#options = [];
#selected = null;
get value() { return this.#selected?.value ?? null; }
#findOption = (val) =>
this.#options.find(({ value }) => value === val) ?? null;
select(val) {
this.#selected = this.#findOption(val);
this.dispatchEvent(new CustomEvent('mf-change', {
bubbles: true,
composed: true,
detail: { value: this.value },
}));
}
}Lista de comprobación rápida
- Campos privados (
#) para todo el estado interno y los métodos - JSDoc en cada atributo público, propiedad, evento, CSS custom property y slot
static observedAttributessolo lista los atributos que el componente usa realmentedisconnectedCallbackelimina todos los event listeners añadidos enconnectedCallback- Tests para el camino feliz, el camino de error y cada evento público
CHANGELOG.mdactualizado en cada releasecustom-elements.jsongenerado y commiteado- Demo en vivo enlazada desde el README