# Prompt: auditoría de código completa de un sitio Astro + Sanity

Plantilla reutilizable para correr un QA técnico sobre cualquier proyecto Astro
con CMS headless. Pensado para pegarse en Claude Code (o cualquier agente con
acceso al repo y a la terminal) al terminar una migración o antes de un
lanzamiento.

**Cómo usarlo:** copiar todo lo que está bajo la línea, reemplazar los valores
entre `<>` del bloque de contexto, y pegarlo. El resto no hace falta tocarlo.

Está escrito a partir de una auditoría real de 216 páginas donde aparecieron 22
problemas que ni TypeScript ni el build detectaban. Cada check de la lista
corresponde a algo que efectivamente estaba mal en un sitio que parecía
terminado — no es una lista teórica de buenas prácticas.

---

## CONTEXTO DEL PROYECTO

- Repo: `<ruta absoluta>`
- Framework: Astro `<versión>`, output `<static|server|hybrid>`
- CMS: `<Sanity | otro>`, dataset `<nombre>`
- Idiomas: `<en/es | solo uno>`. Estrategia de rutas: `<prefijo /es | dominios | ninguna>`
- Hosting previsto: `<Cloudflare Pages | Vercel | Netlify | otro>`
- Servicios de terceros que carga el sitio: `<CDN de imágenes, video, formularios, embeds, analytics>`
- Comandos: build `<pnpm build>`, typecheck `<pnpm check>`, dev `<pnpm dev>`

## OBJETIVO

Audita el código en siete ejes —bugs, accesibilidad, rendimiento, SEO,
seguridad, duplicación y deuda técnica— y entrega un reporte concreto:
qué encontraste, con qué evidencia, y qué propones.

**No apliques nada todavía.** Primero el reporte, después yo decido el alcance.

## REGLAS DE TRABAJO

1. **Mide, no supongas.** Cada hallazgo tiene que venir con el número, el
   comando que lo produjo o el `file:line`. "Podría haber un problema de
   rendimiento" no es un hallazgo; "el build tarda 6m36s con el CPU al 2%,
   porque el shell dispara 1.400 consultas idénticas" sí.
2. **Un build verde no prueba nada.** Los bugs más caros son código válido que
   produce output equivocado. Audita el HTML generado, no solo el fuente.
3. **Distingue severidad.** Separa lo que está roto ahora, lo que degrada
   métricas, y lo que es deuda técnica. No los mezcles en una sola lista.
4. **Cero cambios de contenido sin aprobación.** Si un arreglo implica tocar
   texto visible —copy, títulos, meta descriptions—, proponlo en una tabla
   "antes → después" y espera el visto bueno. El código sí se puede arreglar
   directo una vez aprobado el alcance.
5. **Si hay design system, úsalo.** No inventes variables de tipografía, color
   ni espaciado. Busca el token existente más parecido; si genuinamente no hay,
   avisa antes de crear uno.
6. **Reporta lo que decidiste NO hacer y por qué.** Un pendiente documentado con
   su motivo vale más que un arreglo apurado.

---

## FASE 0 — Inventario (antes de opinar)

```bash
find src -type f | sort            # estructura real
wc -l src/**/*.astro | sort -n     # dónde está el peso
cat astro.config.mjs package.json
```

Anota: cantidad de páginas, rutas dinámicas, componentes, y qué se genera en
build contra qué se resuelve en cliente.

Corre el typecheck y el build **cronometrando**, y guarda el `dist/` como
línea de base para comparar más adelante:

```bash
time pnpm build
cp -R dist /tmp/baseline
```

---

## EJE 1 — Bugs que el compilador no ve

### 1.1 Links y assets rotos

Los `href` y `src` son strings: nada los valida. Recorre el `dist/` y comprueba
cada destino interno contra las páginas y archivos realmente generados.

```python
# por cada .html del dist: extraer href="/..." y src="/..."
# comprobar contra el set de rutas generadas + el set de archivos
# reportar destino + cuántas páginas lo referencian
```

> En la auditoría real esto encontró el favicon apuntando a un archivo
> inexistente. Estaba en el layout base: **404 en las 215 páginas**.

### 1.2 Jerarquía de encabezados

```bash
# contar <h1> por página en el dist — debería ser exactamente 1
```

Si el contenido viene de un CMS con rich text migrado, revisa **qué estilos de
bloque mapea el renderizador**. Todo estilo no mapeado cae al renderer por
defecto: sale sin las clases del design system y, si es un `h1`, compite con el
título de la página.

> Doce artículos tenían hasta **siete `<h1>`**, uno de ellos duplicando el
> título. Además 17 `h4` y 2 tablas salían con la tipografía default del
> navegador.

### 1.3 Fuentes de verdad duplicadas

Busca diccionarios, mapas de rutas o listas de configuración que existan en más
de un archivo. Presta especial atención a los que tienen un comentario del tipo
"esto debe reflejar X": **ese comentario es la evidencia de que ya se
desincronizaron o de que lo van a hacer.**

> Un mapa de rutas EN→ES vivía en `astro.config.mjs` y en `lib/sanity.ts`, con
> el comentario puesto. Al sumar el blog se actualizó uno solo: **92 de 214 URLs
> del sitemap salieron sin `hreflang`.**

### 1.4 Contenido del CMS inyectado sin escapar

Revisa cada `set:html`. Los valores que se interpolan dentro de atributos
(`alt`, `href`, `title`) deben escaparse: `&`, `"`, `<`, `>`.

Si hay SVG inline procesados por código, verifica que no se rompan las
referencias internas (`fill="url(#gradiente)"`, `clip-path`). Un `id` renombrado
o borrado deja el elemento negro o invisible, sin ningún error.

---

## EJE 2 — Rendimiento

### 2.1 Amplificación de consultas al CMS

**El check de mayor retorno en un sitio estático.** Cuenta cuántas veces se
consulta el CMS por página renderizada:

```bash
grep -rn "await get\|sanity.fetch" src/components src/layouts
```

Todo lo que esté en layout, navbar, footer o secciones compartidas se ejecuta
**una vez por página**. En un sitio de 200 páginas, seis consultas en el shell
son 1.200 requests devolviendo lo mismo.

La señal inequívoca es el build lento con el CPU bajo: no compila, espera.

```bash
time pnpm build   # si el %cpu es de un dígito, es red
```

Arreglo, para consultas sin parámetros:

```ts
function once<T>(fn: () => Promise<T>): () => Promise<T> {
  let cached: Promise<T> | undefined;
  return () => (cached ??= fn());   // la PROMESA, no el resultado
}
```

Cachear la promesa y no el valor hace que dos renders en paralelo compartan el
request en vuelo. El caché vive en el módulo: cada build arranca limpio, así que
no hay riesgo de servir contenido viejo.

> Resultado real: **6m36s → 1m38s**. De ~1.850 ms a 4 ms por página.

### 2.2 Imágenes sin dimensiones

```bash
# contar <img> en el dist; cuántos tienen width Y height
```

Sin dimensiones hay layout shift en cada imagen. Para las locales, mide el
archivo. Para las del CMS, muchas codifican el tamaño en el identificador del
asset —Sanity lo hace: `image-<hash>-1440x833-jpg`—, así que se puede derivar
sin consultas extra:

```ts
const m = ref?.match(/-(\d+)x(\d+)-[a-z]+$/i);
// alto = ancho_pedido * (h / w)
```

### 2.3 Peso y formato de los assets

```bash
du -sh public/* | sort -h
# ¿quedan JPG/PNG donde el resto del sitio ya usa AVIF/WebP?
```

Convierte manteniendo la resolución: el ahorro viene del códec, y así no hay
riesgo de que algo se vea distinto.

### 2.4 Assets sin referencia

Comparar los archivos de `public/` contra los referenciados en `src/`, CSS y JS.
Los exports de constructores visuales dejan variantes responsive que nunca se
usan si el markup no tiene `srcset`.

> 48 archivos huérfanos, **2,55 MB**. `public/images` bajó de 7,9 a 3,2 MB.

### 2.5 Terceros en el `<head>`

Por cada script externo: ¿versión fijada? ¿bloquea el render? ¿tiene SRI?

Cuidado especial con **pares core + plugin en versiones distintas** y con las
URLs sin versión (`unpkg.com/<paquete>` sirve siempre la última: puede cambiar
en producción sin que nadie toque el repo).

> Estaban GSAP core 3.15.0 y su plugin ScrollTrigger 3.14.2 —combinación que la
> propia librería no soporta— más un paquete sin pinear, los tres bloqueando el
> primer render.

---

## EJE 3 — Accesibilidad

### 3.1 Controles que no son controles

```bash
grep -rn "addEventListener(\"click\"" public/js src
```

Por cada handler de click: ¿el elemento es un `<button>` o `<a>`? Si es un
`<div>` o `<span>`, no se puede usar con teclado y el lector de pantalla no lo
anuncia.

Necesita `role="button"`, `tabindex="0"`, manejo de `Enter` y `Espacio`, y —si
abre o cierra algo— `aria-expanded` + `aria-controls`.

> Los acordeones de FAQ eran `<div>` pelados en **ocho páginas**, incluida la
> home. El mismo proyecto ya tenía el patrón correcto en el megamenú: estaba
> resuelto una vez y no se replicó. **Busca siempre si el patrón correcto ya
> existe en el repo antes de escribirlo de nuevo.**

### 3.2 Foco visible

Si se agregan controles con `tabindex`, hace falta `:focus-visible` propio: el
outline por defecto es invisible sobre fondos oscuros.

### 3.3 Etiquetas y agrupaciones

- `<img>` sin `alt`
- `<label>` sin `for`, o etiquetando un grupo en vez de un control
  (un grupo de checkboxes va con `<fieldset>` + `<legend>`)
- Campos de formulario sin nombre accesible

---

## EJE 4 — SEO

```bash
# sobre el dist: por página, largo de <title> y de meta description,
# canonical, hreflang, y cantidad de h1
```

- **`<title>` > 60 caracteres** y **description > 160**: Google trunca. Revisa
  que el recorte no se coma el término por el que compite la página.
- **Descripciones que caen a otro campo.** Si el template hace
  `metaDescription ?? heroDescription`, las páginas sin meta propia arrastran
  copy largo de la página. Compruébalo sobre el HTML, no sobre el CMS.
- **Sitemap**: ¿todas las URLs tienen su par de idioma? ¿el canonical es único?
- **Texto visible contra texto SEO**: si el CMS tiene un campo de título SEO
  aparte, úsalo — nunca acortes el título que lee la gente por una razón de
  buscadores.

> 80 títulos y 85 descripciones fuera de límite. Los servicios no tenían meta
> propia y caían a la descripción del hero, que es copy de la página.

---

## EJE 5 — Seguridad

### 5.1 Secretos

```bash
git ls-files | grep -iE "\.env$|\.env\."      # solo .example debería aparecer
git grep -lIE "sk[-_]|api[_-]?key\s*="        # revisar falsos positivos
```

Verifica también qué variables terminan en el bundle del cliente: solo las que
el framework marca como públicas, y que ninguna de esas sea sensible de verdad.

### 5.2 Cabeceras

`X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options`,
`Permissions-Policy`. Son de respuesta: no cambian el HTML ni el render.

**CSP: publícala en `Report-Only` primero.** Si el sitio carga de varios
orígenes y la lista queda incompleta, los recursos fallan **en silencio** —la
imagen no aparece, el formulario no envía— y te enteras por un cliente. Dos
semanas de tráfico real, revisar el reporte, y recién ahí modo bloqueo.

**HSTS: no lo actives antes del dominio definitivo.** Una vez que un navegador
cachea el header, fuerza HTTPS todo el `max-age` y **no se revierte desde el
servidor**. Déjalo comentado con el motivo escrito.

### 5.3 Assets de pago servidos por CDN

Si hay video, audio o descargas en un CDN que factura por ancho de banda,
comprueba si están abiertos:

```bash
curl -sI "<url del asset>"      # ¿200 sin Referer ni token?
```

**Una CSP no protege esto**: solo aplica dentro de tus páginas, no impide que
alguien use la URL fuera de tu sitio.

Lo que sí protege, en orden de esfuerzo:

1. **Lista blanca de referrers** + bloqueo de acceso directo. Inmediato y
   gratis. Detalles que cuestan tiempo si no se saben: la comparación suele ser
   por hostname **exacto e incluyendo el puerto**, y `localhost` no se acepta
   como referrer válido, así que activarlo **rompe el desarrollo local** hasta
   que apuntes un hostname real a `127.0.0.1` y lo agregues a la lista.
2. **Tope mensual de ancho de banda.** Es el techo de gasto real —el
   auto-recharge de la facturación es lo contrario: recarga saldo, no limita.
   Calcúlalo con la tarifa más cara de tus mercados. Complémentalo con límites
   por IP, que frenan a un abusador sin apagar el servicio para todos.
3. **URLs firmadas con token.** La protección fuerte. En un sitio estático hay
   que decidir dónde se firman: en el build con expiración larga, o on-demand
   en un edge function.

Antes de optimizar el peso "para no gastar", **mide cuánto es**. Los CDN suelen
mandar `cache-control` de semanas, así que el desarrollo local descarga cada
archivo una vez, no una vez por recarga.

---

## EJE 6 — Duplicación

### 6.1 Páginas espejo por idioma

```bash
# por cada par: total de líneas y cuántas difieren
diff <(sed 's/[[:space:]]*$//' EN) <(sed 's/[[:space:]]*$//' ES) | grep -c "^<"
```

Si el 80% es idéntico, no son dos páginas: es una copiada. **Todo arreglo hay
que hacerlo dos veces y basta olvidar uno para que un idioma quede atrás.**

El arreglo es un componente que recibe `locale` y dos rutas mínimas. Prioriza
por instancias generadas: una plantilla de detalle que produce 30 páginas rinde
más que un hub que produce una.

> 13 pares, **8.422 líneas en 26 archivos**, ~3.400 de duplicación pura.

### 6.2 Markup repetido a mano

```bash
grep -rc "<clase-sospechosa" src | grep -v ":0"
```

Elementos decorativos copiados con estilos inline son el caso típico.

> 125 `<div>` de estrellas con su `rgba()` y sus píxeles inline, repartidos en
> 11 archivos. Doce elementos únicos repetidos diez veces.

### 6.3 Tokens del design system escritos a mano

```bash
# variables CSS USADAS menos variables DEFINIDAS = huérfanas
```

Busca también valores literales que ya existen como token.

> `var(--color--menta, #3bbfad)` donde esa variable **no está definida en ningún
> archivo**: siempre gana el fallback. Es un color hardcodeado disfrazado de
> token, y el disfraz es el problema —un hex suelto se nota en una revisión,
> esto no.

---

## EJE 7 — El método de verificación

**Esta es la parte que más rinde y la que casi nadie hace.**

Si un refactor no debería cambiar el resultado, compruébalo: guarda el `dist/`
antes, vuelve a compilar, y compara. No los archivos crudos —el formato cambia por
motivos irrelevantes— sino **el texto visible con los tags eliminados y cada
`href`/`src`**, normalizando de antemano las diferencias que sí son buscadas.

```bash
for f in $(cd dist && find . -name "*.html"); do
  diff <(normalizar "/tmp/baseline/$f") <(normalizar "dist/$f") || echo "DIFIERE: $f"
done
```

El objetivo es **0 páginas con diferencias**. Cualquier resultado distinto es un
hallazgo, no un ruido a ignorar.

> En un refactor de 8.000 líneas con `astro check` en 0 errores y el build
> generando las 216 páginas, esto encontró cuatro bugs:
>
> 1. **Links en español apuntando a rutas en inglés.** El extractor comparaba
>    nodos de texto; un `href` dentro de una expresión JS no lo es.
> 2. **Un componente convertido en string.** `<CtaSection />` guardado como
>    texto e inyectado con `set:html`: el navegador ve un elemento desconocido y
>    la sección **desaparece de la página** sin ningún error.
> 3. **Una meta description pisada.** Dos textos que empezaban igual generaron
>    la misma clave de diccionario y el segundo sobrescribió al primero.
> 4. **Nueve páginas con el frontmatter sin traducir.** La alineación comparaba
>    cantidad de líneas y los comentarios del archivo en inglés eran más largos:
>    al no coincidir, descartaba y se quedaba con el inglés.
>
> De paso destapó dos bugs que **ya existían antes del refactor** y que nadie
> había visto.

**Cuando encuentres un bug así, arregla también el modo de fallo.** El caso
puntual y la validación que impide que vuelva a pasar callado:

```python
if esqueleto_en != esqueleto_es:
    sys.exit("ERROR: queda una diferencia sin capturar")
```

Una herramienta que nunca se rompe y a veces emite lo incorrecto es peor que una
que aborta. **Fallar ruidosamente le gana a funcionar callado.**

---

## FORMATO DEL REPORTE

Entrega esto, en este orden:

1. **Veredicto general.** Qué está bien y no hay que tocar. Con números.
2. **Tabla de hallazgos por severidad**: qué está roto ahora, qué degrada
   métricas, qué es deuda técnica.
3. **Por cada hallazgo**: qué es, la evidencia (comando, número o `file:line`),
   el impacto concreto, y el arreglo propuesto.
4. **Cambios de contenido**, aparte y en tabla "antes → después". No los
   apliques sin aprobación.
5. **Lo que recomiendas NO hacer todavía**, con el motivo. Especialmente CSP,
   HSTS y cualquier cosa que dependa de tráfico real.
6. **Plan por tandas**, ordenado por impacto sobre esfuerzo, para que se pueda
   revisar y mergear por partes en vez de todo junto.

Al aplicar: una tanda por commit, con el porqué en el mensaje —no solo el qué—,
y `pnpm check` + `pnpm build` + la comparación del `dist/` verdes antes de cada
uno.
