Guía práctica. Todos los snippets están probados en producción sobre sitios reales.
El informe WebAIM Million de 2026 analizó el millón de páginas de inicio más visitadas del mundo: el 95,9 % tenía fallos de accesibilidad detectables de forma automática, con una media de 56,1 errores por página. Y por primera vez en seis años el dato ha empeorado, no mejorado. Desde el 28 de junio de 2025 eso ya no es un problema estético: la European Accessibility Act es de obligado cumplimiento, y los sitios nuevos deben cumplir desde el lanzamiento (los que ya existían tienen de plazo hasta junio de 2030). En España la transpone la Ley 11/2023 en su Título I, y por otro lado el Real Decreto 193/2023 exige la norma UNE-EN 301549 a las webs de bienes y servicios a disposición del público, con un calendario propio. Las sanciones pueden llegar al millón de euros.
Aclaremos lo otro antes de empezar: la accesibilidad no es un factor de ranking. John Mueller lo dijo en 2022 con estas palabras, «No, not really», y explicó que Google no tiene forma de cuantificarla. Quien te diga lo contrario está vendiendo algo. Lo que sí ocurre es que la accesibilidad y el rastreador consumen la misma información estructural del documento, así que buena parte de esta lista es exactamente lo que ya pides en tus auditorías técnicas.
Esto no es un artículo sobre accesibilidad. Es una lista de diez fallos concretos con el snippet que los detecta y los clics que los arreglan en Elementor y en Bricks. Abre la consola del navegador (F12, pestaña Console), pega, mira el resultado. No hay nada que instalar.

Con impacto SEO directo
1. Imágenes sin alt, o con un alt que no sirve
El caso que todo el mundo audita es la imagen sin alt. El que nadie audita es el alt que se rellenó solo con el nombre del archivo al subirla: DSC_04412.jpg. Cuenta como relleno y no aporta nada.
Detectar
console.table([...document.images]
.filter(i => !i.hasAttribute('alt') || /\.(jpe?g|png|webp|svg|gif)$/i.test(i.alt))
.map(i => ({ archivo: i.currentSrc.split('/').pop(),
alt: i.hasAttribute('alt') ? i.alt : 'SIN ATRIBUTO ALT' })))Ojo con una distinción que confunde a mucha gente: alt="" es correcto en imágenes decorativas. El fallo es que el atributo no exista, no que esté vacío. Por eso el snippet no marca los vacíos.
Rompe
- Accesibilidad: criterio 1.1.1, contenido no textual.
- SEO: Google pierde el contexto de la imagen y su indexación en Google Imágenes.

En Elementor. El alt se hereda de la biblioteca de medios, no del widget. Arréglalo en Medios > Biblioteca, campo Texto alternativo, y se propaga a todas las apariciones. Es el arreglo con mejor relación esfuerzo/impacto de toda la lista.
En Bricks. Igual: el valor sale de la biblioteca de medios de WordPress. En el elemento Image puedes sobrescribirlo con el control de texto alternativo cuando esa instancia concreta necesita otra descripción.
2. Enlaces sin texto accesible
El enlace que solo contiene un icono, la imagen enlazada sin alt, la tarjeta con enlace envolvente. Para un lector de pantalla es un enlace anónimo. Para Google es un enlace interno con el anchor text vacío.
Detectar
const nombre = a => { const c = a.cloneNode(true);
c.querySelectorAll('style,script').forEach(n => n.remove());
return (c.textContent || '').replace(/\s+/g, ' ').trim() || a.getAttribute('aria-label')
|| a.querySelector('img[alt]:not([alt=""])')?.alt || a.title || '' };
console.table([...document.querySelectorAll('a[href]')].filter(a => !nombre(a))
.map(a => ({ href: a.getAttribute('href'), html: a.innerHTML.trim().slice(0, 60) })))El matiz que te interesa a ti y no a un consultor de accesibilidad: aria-label sustituye al texto visible del enlace, no lo complementa. Si pones aria-label="Más info" en un enlace cuyo texto era «Guía de accesibilidad para WordPress», acabas de tirar a la basura un anchor text bueno. Usa aria-label solo donde no hay texto, nunca encima de un texto que ya funciona.
Rompe
- Accesibilidad: criterio 2.4.4, propósito del enlace.
- SEO: enlace interno sin señal de anclaje.
En Elementor. Con Pro, Avanzado > Atributos y la sintaxis clave|valor, por ejemplo aria-label|Ver el carrito. Sin Pro no tienes ese campo: la salida es que el enlace lleve una imagen con alt real, o añadir texto oculto con una clase de tipo screen reader.
En Bricks. El control de enlace de cada elemento incluye campos para aria-label y title, pensados justo para cuando el texto del enlace está vacío o no es claro. No necesitas atributos personalizados.
3. El mismo anchor text apuntando a sitios distintos
Los «Leer más» y «Ver producto» en serie. Con el rotor de enlaces de un lector de pantalla es una lista de treinta entradas idénticas. Para Google son treinta enlaces internos que no dicen nada de su destino.
No busques texto genérico: busca el mismo texto con destinos distintos, que es la señal real.
Detectar
const m = {};
document.querySelectorAll('a[href]').forEach(a => { const k = a.textContent.replace(/\s+/g,' ').trim().toLowerCase();
if (k && k.length < 30) (m[k] = m[k] || new Set()).add(a.href) });
console.table(Object.entries(m).filter(([, s]) => s.size > 1)
.map(([texto, s]) => ({ texto, destinos: s.size })).sort((a, b) => b.destinos - a.destinos))Un menú que repite «Inicio» apuntando siempre a la misma URL no sale. Sale lo que de verdad es un problema.
Rompe
- Accesibilidad: criterio 2.4.4, propósito del enlace.
- SEO: señal de anclaje interno desperdiciada, y multiplicada, porque esto vive en plantillas de archivo con decenas de repeticiones por página.
En Elementor. En el widget Posts o Loop Grid, el botón «Leer más» es un campo de texto único para todo el bucle, así que no puedes personalizarlo por entrada desde ahí. Las dos salidas reales: quitar el botón y dejar que el enlace sea el propio título de la entrada, que ya es descriptivo, o pasar a un Loop Item con plantilla propia y usar la etiqueta dinámica del título dentro del aria-label.
En Bricks. El bucle de consulta con plantilla de elemento permite meter etiquetas dinámicas en el aria-label del enlace, así que puedes generar «Leer más sobre {post_title}» de forma automática. Es el arreglo limpio.
4. Jerarquía de encabezados rota
Sin h1, con tres h1, con saltos de h2 a h4, o con encabezados elegidos por tamaño visual. Este ya lo auditas, pero probablemente solo en las entradas y no en las plantillas.
Detectar
let p = 0;
console.table([...document.querySelectorAll('h1,h2,h3,h4,h5,h6')].map(h => {
const n = +h.tagName[1], salto = p && n > p + 1 ? 'SALTO desde H' + p : '';
p = n; return { nivel: h.tagName, texto: h.textContent.trim().slice(0, 45), problema: salto } }))
console.log('H1 en la página:', document.querySelectorAll('h1').length, '(debe ser 1)')
Cuando probé esto para escribir el artículo, la portada de un blog de WordPress muy conocido en español devolvió cero h1. Pruébalo en la home de tu cliente antes de dar por hecho que está bien.
Rompe
- Accesibilidad: criterio 1.3.1, información y relaciones.
- SEO: estructura del documento.
En Elementor. El widget Heading tiene la etiqueta HTML en Contenido > Etiqueta HTML, separada del tamaño visual. Un h2 que se ve pequeño es perfectamente válido: cambia la etiqueta y ajusta el tamaño en la pestaña Estilo. En contenedores y secciones, Diseño > Etiqueta HTML te deja poner header, main, nav o footer, que son landmarks gratis.
En Bricks. Cada elemento de texto tiene su selector de etiqueta en los ajustes del elemento, y el Container permite elegir la etiqueta semántica desde Style > Attributes. Mismo principio: la etiqueta es estructura, el tamaño es estilo.
Con impacto SEO indirecto
5. El idioma del documento
Una línea. El caso típico es la plantilla comprada que se quedó con lang="en-US" en un sitio entero en español, lo que hace que un lector de pantalla lea el contenido con fonética inglesa.
Detectar
console.log('lang =', document.documentElement.lang || 'AUSENTE')Rompe
- Accesibilidad: criterio 3.1.1, idioma de la página.
- SEO: identificación de idioma y coherencia con tus hreflang.
En Elementor y en Bricks. No es del maquetador: sale de la configuración de WordPress en Ajustes > Generales > Idioma del sitio. Si ahí está bien y el HTML sigue mal, lo está pisando el tema o un plugin multiidioma.
6. Cosas clicables que no son ni enlace ni botón
El div con onclick que hace de botón. No recibe foco, no responde a Enter, y si debía ser un enlace, para el rastreador sencillamente no existe. Los maquetadores generan esto con facilidad.
Detectar
const c = [...document.querySelectorAll('div,span,li')].filter(e =>
(e.onclick || getComputedStyle(e).cursor === 'pointer') && !e.closest('a,button,summary,label')
&& !e.getAttribute('role') && e.tabIndex < 0 && !e.querySelector('a,button,input,select'));
console.table(c.filter(e => !c.some(o => o !== e && e.contains(o)))
.map(e => ({ tag: e.tagName, clase: (e.className + '').slice(0, 40),
texto: e.textContent.trim().slice(0, 30) || '(sin texto)' })))Este es el único de la lista que devuelve candidatos, no errores confirmados: cursor: pointer también se usa por decoración. Revisa la lista, no la des por buena.
Rompe
- Accesibilidad: criterios 2.1.1 (teclado) y 4.1.2 (nombre, función, valor).
- SEO: si tenía que ser un enlace, no hay enlace que rastrear.
En Elementor. Suele venir de una caja entera hecha clicable con un enlace de contenedor o con JavaScript propio. La solución es que el enlace real esté en un elemento de texto dentro de la tarjeta, y ampliar su área activa con CSS mediante un pseudoelemento que cubra la tarjeta.
En Bricks. Misma técnica. Bricks permite enlazar el contenedor completo, lo que genera un enlace envolvente sin texto: comprueba con el snippet 2 que ese enlace tiene nombre, o dáselo con el campo aria-label del control de enlace.
7. Contenido que solo existe después de un clic
Pestañas, acordeones y botones de «cargar más». Aquí hay que ser preciso, porque circula mucha desinformación: el contenido oculto con display: none sí está en el HTML y Google lo ve. El problema real es el contenido que no está en el HTML y se inyecta por JavaScript al hacer clic.
Detectar
console.table([...document.querySelectorAll('[class*=tab],[class*=accordion],[class*=panel]')]
.filter(e => getComputedStyle(e).display === 'none' && e.textContent.trim().length > 100)
.map(e => ({ clase: (e.className + '').slice(0, 40),
palabras: e.textContent.trim().split(/\s+/).length })))Lo que sale aquí está en el HTML y es indexable. Para encontrar lo que no está, la comprobación es otra y no necesita snippet: abre el código fuente con Ctrl+U y busca un fragmento de ese texto. Si no aparece en el fuente, no está en el HTML inicial.
Rompe
- Accesibilidad: criterio 2.1.1, si el componente no se opera con teclado.
- SEO: solo afecta al contenido inyectado por JavaScript, que puede no llegar al índice.
En Elementor. Los widgets de pestañas y acordeón renderizan todo el contenido en el HTML, así que por indexación estás cubierto. Lo que hay que probar es el teclado: Tab hasta la pestaña, Enter para abrirla, y comprobar que el foco va donde debe.
En Bricks. Igual con los elementos anidables de pestañas y acordeón. La zona de riesgo son los sliders anidados dentro de bucles de consulta, donde han aparecido problemas de ARIA reportados en su propio foro.
Sin impacto SEO, y van igual
Los tres que quedan no te dan absolutamente nada en posicionamiento. Van en la lista porque son exactamente los que acaban en demanda, y porque el 8 y el 10 te tocan la conversión.
8. Contraste por debajo de 4,5:1
Detectar
const L = c => { const [r,g,b] = c.match(/\d+/g).map(v => { v/=255;
return v <= .03928 ? v/12.92 : ((v+.055)/1.055)**2.4 }); return .2126*r + .7152*g + .0722*b };
const BG = e => { for (; e; e = e.parentElement) { const c = getComputedStyle(e).backgroundColor;
if (c && !/rgba\(0, 0, 0, 0\)|transparent/.test(c)) return c } return 'rgb(255, 255, 255)' };
console.table([...document.querySelectorAll('p,a,li,h1,h2,h3,h4,button,label,span')]
.filter(e => e.offsetParent && [...e.childNodes].some(n => n.nodeType === 3 && n.textContent.trim()))
.map(e => { const s = getComputedStyle(e), f = BG(e),
r = (Math.max(L(s.color), L(f)) + .05) / (Math.min(L(s.color), L(f)) + .05);
return { texto: e.textContent.trim().slice(0, 30), ratio: +r.toFixed(2), color: s.color, fondo: f } })
.filter(x => x.ratio < 4.5))Calcula el ratio con la fórmula de WCAG sobre los estilos computados, subiendo por el árbol hasta encontrar un fondo opaco. Es la comprobación más útil de la lista y también la menos fiable, así que conviene saber por dónde falla antes de enseñarle los resultados a nadie.
Solo lee background-color. Si el fondo real es una imagen o un degradado, se salta esa capa y sigue subiendo hasta el primer color opaco que encuentre, que puede ser el de la sección entera. Ahí no te dice «no lo sé»: te devuelve un ratio calculado contra un fondo que no es el que se ve, y suele salir catastrófico. Los 1,00:1 son casi siempre esto.
Ignora todo lo que lleve position: fixed, porque su offsetParent es null y el filtro de visibilidad lo descarta. Es decir, no mira las cabeceras ni las barras pegajosas, que es justo donde más se repite el gris flojo.
Y espera colores en rgb(). Con oklch(), color(display-p3 …) o porcentajes no lanza ningún error: devuelve un número sin sentido. Hoy Chrome serializa casi todo como rgb(), pero en un sitio moderno con color-mix() puede pasar.
Trátalo como una lista de sospechosos, no como un veredicto: verifica en el Contrast Checker de WebAIM lo que vayas a reportar.

El sospechoso habitual son los botones de compartir en redes con el color corporativo de cada marca sobre blanco. En una de las pruebas para este artículo, WhatsApp daba 1,98:1 y Telegram 2,77:1.
Rompe
- Accesibilidad: criterio 1.4.3. Mínimo 4,5:1 en texto normal y 3:1 en texto grande y elementos de interfaz.
- SEO: ninguno.
En Elementor. Ajustes del sitio > Colores globales. Es el punto único de arreglo: cambias ahí el gris de los textos secundarios y se propaga a todo el sitio. Si el tema no usa colores globales, este es el momento de migrarlo.
En Bricks. Paletas de color y variables globales en los ajustes del tema, con el mismo efecto. Cambias el token, no cuarenta elementos.
9. Foco anulado en el CSS
El outline: none sin sustituto. Quien navega con teclado deja de saber dónde está.
Detectar
[...document.styleSheets].forEach(s => { try {
[...s.cssRules].forEach(r => { if (r.selectorText?.includes(':focus')
&& /outline\s*:\s*(none|0)/.test(r.style.cssText) && !r.style.boxShadow)
console.warn(r.selectorText, '→', r.style.cssText) }) } catch (e) {} })El try/catch está porque las hojas de estilo servidas desde otro dominio lanzan excepción al leer sus reglas. Esas no se pueden inspeccionar así, y conviene saberlo.
Después del snippet, la prueba de verdad: pulsa Tab diez veces y mira si en todo momento ves dónde estás.
Rompe
- Accesibilidad: criterio 2.4.7, foco visible.
- SEO: ninguno.
En Elementor. No hay ajuste para esto: se corrige con CSS, en Ajustes del sitio > CSS personalizado con Pro, o en el CSS adicional del personalizador.
a:focus-visible, button:focus-visible, input:focus-visible {
outline: 3px solid #0069DE;
outline-offset: 2px;
}
En Bricks. Aquí sí hay ajuste, y es de las mejores cosas que tiene: Settings > Theme Styles > Typography > Focus Outline. Lo defines una vez para todo el sitio sin escribir CSS.
10. Formularios con placeholder en lugar de label
El placeholder desaparece en cuanto el usuario escribe, no lo anuncian todos los lectores de pantalla, y su contraste suele suspender. No es una etiqueta.
Detectar
console.table([...document.querySelectorAll(
'input:not([type=hidden]):not([type=submit]):not([type=button]),select,textarea')]
.filter(f => !f.labels?.length && !f.getAttribute('aria-label') && !f.getAttribute('aria-labelledby'))
.map(f => ({ tipo: f.type || f.tagName, name: f.name || '-', placeholder: f.placeholder || 'nada' })))
Rompe
- Accesibilidad: criterios 1.3.1, 3.3.2 y 4.1.2.
- SEO: ninguno. Conversión: mucho, y en el formulario de contacto o el checkout, que es donde duele.
En Elementor. En el widget Form, cada campo tiene su etiqueta y un interruptor para mostrarla. Actívalo. Si el diseño exige no verla, la opción correcta no es quitar la etiqueta, sino dejarla y ocultarla visualmente con CSS.
En Bricks. El elemento Form permite añadir o quitar etiquetas de campo, y cuando las ocultas Bricks genera el aria-label a partir de la etiqueta definida en el constructor. Compruébalo con el snippet en lugar de darlo por supuesto.
El auditor completo, en un solo bloque
Ejecuta las diez comprobaciones de una pasada y devuelve un resumen con el recuento de cada una y el detalle desplegable. Pégalo en la consola de cualquier página, incluida la de tu cliente en producción: solo lee, no modifica nada.
(() => {
const q=(s)=>[...document.querySelectorAll(s)];
const nombre=a=>{const c=a.cloneNode(true);c.querySelectorAll('style,script').forEach(n=>n.remove());
return (c.textContent||'').replace(/\s+/g,' ').trim()||a.getAttribute('aria-label')||a.querySelector('img[alt]:not([alt=""])')?.alt||a.title||''};
const L=c=>{const[r,g,b]=c.match(/\d+/g).map(v=>{v/=255;return v<=.03928?v/12.92:((v+.055)/1.055)**2.4});return .2126*r+.7152*g+.0722*b};
const BG=e=>{for(;e;e=e.parentElement){const c=getComputedStyle(e).backgroundColor;
if(c&&!/rgba\(0, 0, 0, 0\)|transparent/.test(c))return c}return 'rgb(255, 255, 255)'};
const anchors={};q('a[href]').forEach(a=>{const k=nombre(a).toLowerCase();if(k&&k.length<30)(anchors[k]=anchors[k]||new Set()).add(a.href)});
let prev=0;
const R={
'1. Imágenes sin alt útil':q('img').filter(i=>!i.hasAttribute('alt')||/\.(jpe?g|png|webp|svg|gif)$/i.test(i.alt))
.map(i=>({elemento:i.currentSrc.split('/').pop(),detalle:i.hasAttribute('alt')?i.alt:'SIN ATRIBUTO ALT'})),
'2. Enlaces sin texto accesible':q('a[href]').filter(a=>!nombre(a))
.map(a=>({elemento:a.getAttribute('href'),detalle:a.innerHTML.trim().slice(0,60)})),
'3. Mismo anchor a destinos distintos':Object.entries(anchors).filter(([,s])=>s.size>1)
.map(([t,s])=>({elemento:t,detalle:s.size+' destinos'})),
'4. Jerarquía de encabezados':(()=>{const o=q('h1,h2,h3,h4,h5,h6').map(h=>{const n=+h.tagName[1],
s=prev&&n>prev+1?'salto desde H'+prev:'';prev=n;return{elemento:h.tagName,detalle:s}}).filter(x=>x.detalle);
const n1=q('h1').length;if(n1!==1)o.unshift({elemento:'H1',detalle:n1+' en la página (debe haber 1)'});return o})(),
'5. Idioma del documento':document.documentElement.lang?[]:[{elemento:'<html>',detalle:'sin atributo lang'}],
'6. Clicables que no son enlace ni botón':(()=>{const c=q('div,span,li').filter(e=>(e.onclick||getComputedStyle(e).cursor==='pointer')
&&!e.closest('a,button,summary,label')&&!e.getAttribute('role')&&e.tabIndex<0&&!e.querySelector('a,button,input,select'));
return c.filter(e=>!c.some(o=>o!==e&&e.contains(o))).map(e=>({elemento:e.tagName+'.'+(e.className+'').split(' ')[0],
detalle:e.textContent.trim().slice(0,30)||'(sin texto)'}))})(),
'7. Contenido oculto con display:none':q('[class*=tab],[class*=accordion],[class*=panel]')
.filter(e=>getComputedStyle(e).display==='none'&&e.textContent.trim().length>100)
.map(e=>({elemento:(e.className+'').slice(0,35),detalle:e.textContent.trim().split(/\s+/).length+' palabras'})),
'8. Contraste por debajo de 4.5:1':q('p,a,li,h1,h2,h3,h4,button,label,span')
.filter(e=>e.offsetParent&&[...e.childNodes].some(n=>n.nodeType===3&&n.textContent.trim()))
.map(e=>{const s=getComputedStyle(e),f=BG(e),r=(Math.max(L(s.color),L(f))+.05)/(Math.min(L(s.color),L(f))+.05);
return{elemento:e.textContent.trim().slice(0,30),detalle:r.toFixed(2)+':1 '+s.color+' sobre '+f,_r:r}})
.filter(x=>x._r<4.5).map(({_r,...x})=>x),
'9. Foco anulado en CSS':(()=>{const o=[];[...document.styleSheets].forEach(s=>{try{[...s.cssRules].forEach(r=>{
if(r.selectorText?.includes(':focus')&&/outline\s*:\s*(none|0)/.test(r.style.cssText)&&!r.style.boxShadow)
o.push({elemento:r.selectorText.slice(0,50),detalle:r.style.cssText.slice(0,40)})})}catch(e){}});return o})(),
'10. Campos de formulario sin label':q('input:not([type=hidden]):not([type=submit]):not([type=button]),select,textarea')
.filter(f=>!f.labels?.length&&!f.getAttribute('aria-label')&&!f.getAttribute('aria-labelledby'))
.map(f=>({elemento:(f.type||f.tagName)+' name='+(f.name||'?'),detalle:f.placeholder?'placeholder: '+f.placeholder:'sin nada'}))
};
console.group('%cAuditoría rápida de accesibilidad','font-size:14px;font-weight:bold');
console.table(Object.entries(R).map(([k,v])=>({comprobación:k,fallos:v.length})));
Object.entries(R).forEach(([k,v])=>{if(v.length){console.groupCollapsed(k+' — '+v.length);console.table(v);console.groupEnd()}});
console.groupEnd();
})()
Guárdalo como snippet en DevTools > Sources > Snippets y lo tendrás a un clic en cualquier pestaña.
Antes de cerrar la auditoría
Esto detecta lo que se puede detectar leyendo el DOM, que es aproximadamente el mismo 30-40 % que cubren axe, WAVE o Lighthouse. Es la parte barata. La otra parte no la hace ninguna herramienta y son quince minutos:

Recorre la página entera con Tab sin tocar el ratón, e intenta completar el formulario o el checkout. Haz zoom al 200 % y mira qué se rompe. Y enciende un lector de pantalla real: VoiceOver ya está instalado en tu Mac (Cmd + F5), y NVDA es gratuito en Windows.
Si lo primero lo tienes automatizado y lo segundo lo haces de verdad, ya estás por delante del 95,9 % de la web.
Dónde seguir mirando
El estándar
- WCAG 2.2 — la norma, en su versión completa. Densa, pero es la fuente.
- Referencia rápida de WCAG 2.2 — la misma norma criterio a criterio, filtrable por nivel A/AA/AAA. Esta es la que vas a usar de verdad.
- WAI del W3C en español — material introductorio traducido.
- EN 301 549 v3.2.1 — la norma europea armonizada, la versión vigente. Incorpora WCAG 2.1 AA; la v4.1.1 con 2.2 llega durante 2026.
Lo legal
- Directiva (UE) 2019/882 — el texto de la European Accessibility Act.
- Real Decreto 193/2023 — condiciones de accesibilidad de bienes y servicios en España.
Herramientas gratuitas
- WAVE — pega una URL y te la marca sobre la propia página. La más rápida para enseñársela a un cliente.
- axe DevTools — extensión de Chrome y Firefox. El motor que hay debajo de media industria.
- Contrast Checker de WebAIM — dos colores, un número. Para verificar lo que te saque el snippet 8.
- NVDA — lector de pantalla gratuito para Windows. En Mac ya tienes VoiceOver con
Cmd + F5. - WebAIM Million — el informe anual del que sale el dato del principio.
Quién escribe esto
Soy Salva. Hago SEO local para negocios de servicios —reformas, instaladores, fontaneros, mudanzas— y trabajo casi siempre sobre WordPress con Elementor y Bricks. De ahí sale esta lista: no son los diez fallos de un manual, son los diez que me encuentro una y otra vez en webs de clientes reales.
Si pasas el auditor por una web y te sale algo que no sabes por dónde coger, escríbeme y lo vemos.

Desarrollador web reconvertido en consultor de SEO local en Sant Boi de Llobregat, especializado en negocios de servicios que se desplazan al cliente, como reformas, instaladores, fontaneros o mudanzas.