Resumen
Resolución del problema reportado por usuario (Edu) en Edge: al recargar la página, las tipografías de marca (Oswald, Plus Jakarta Sans) desaparecían brevemente y se mostraba la fuente de respaldo del sistema, generando un “baile” visual disruptivo. Root cause: los estáticos se servían con Cache-Control: max-age=60 (default de WhiteNoise sin hash), combinado con font-display: optional (ventana de ~100 ms de espera), causaba que cada recarga “en frío” mostrara la fuente de respaldo.
Fix: en producción, STORAGES["staticfiles"] usa ahora ForgivingManifestStaticFilesStorage (subclase custom que hashea nombres → WhiteNoise sirve con max-age=31536000, immutable). Tras la 1ª visita, la fuente de marca queda cacheada de forma estable e inmutable.
Beneficio colateral: cache-busting automático por hash en cada deploy para todos los estáticos.
Cambios de infraestructura
Storage de estáticos (prod/stage)
-
Archivo:
config/settings/production.py— nueva sección “Estáticos con nombres hasheados” que sobreescribeSTORAGES["staticfiles"]con backend custom. -
Backend:
core.storage.ForgivingManifestStaticFilesStorage— subclase deCompressedManifestStaticFilesStoragecon:manifest_strict = False→ tolera lookups sin entrada en el manifest (devuelve el nombre original sin error).post_process()override → degradaMissingFileErrorde archivos de terceros (p.ej. FontAwesomeall.min.csscon referencias no resolubles) a “no reescribir ese CSS” en vez de abortar el build.
-
Alcance: solo prod/stage (requieren
collectstaticpara generar el manifest). -
Dev/test: heredan
CompressedStaticFilesStoragedebase.py(sin manifest, sin hash).
Dockerfile.prod
- Cambio: se quitó
|| truedel comandocollectstatic(línea 91). - Motivo: con manifest, un
collectstaticincompleto deja elstaticfiles.jsonsin generar → cada{% static %}fallaría en runtime con 500. Fail-fast permite detectar el problema en build time (Docker). - Verificado en Docker:
collectstaticRC=0,staticfiles.jsongenerado, fuentes con hash.
Impacto en caché y recarga
| Aspecto | Antes | Después |
|---|---|---|
| Cache Control (estáticos) | max-age=60 | max-age=31536000, immutable |
| Nombre de archivo (fuentes) | plus-jakarta-sans-latin.woff2 | plus-jakarta-sans-latin.a1b2c3d4e5f6.woff2 |
| FOUT en recarga (100 ms) | ✗ Font de respaldo | ✓ Font de marca (caché) |
| CLS después de 1ª visita | N/A | 0 (font-display: optional intacto) |
Contexto: el problema de FOUT
FOUT = “Flash of Unstyled Text”. En navegadores con font-display: optional:
- Si la web font llega en < 100 ms, se usa.
- Si no llega en < 100 ms, se usa la fuente de respaldo (fallback del sistema) sin esperar más.
Antes del fix:
- Visita 1: White Noise sirve con
max-age=60→ 1ª vez la fuente está en la red, caché local vacío → FOUT → fallback del sistema. - Visita 2 (en < 60 s): fuente caché está fresca → llega en < 100 ms → se ve la fuente de marca.
- Visita 3 (> 60 s después de visita 2): caché expirada (60 s) → fuente baja de la red → FOUT nuevamente.
La caché de 60 s es arbitraria — cada usuario veía el “baile” según cuándo recargara.
Después del fix:
- Visita 1: nombre hasheado +
max-age=31536000, immutable→ fuente se descarga y se cachea por 1 año. - Visitassubsecuentes: fuente en caché local = < 100 ms → siempre se ve la fuente de marca. Sin FOUT.
- Deploy nuevo (nuevo hash): navegador trata el nombre como nuevo archivo → descarga la nueva fuente sin contaminar la caché vieja.
Entidades relacionadas
- Storage backend: [[entity—core—service—forgiving-manifest-storage]]
- Configuración de producción:
config/settings/production.py(STORAGES override). - Dockerfile:
Dockerfile.prod(collectstatic fail-fast). - Font loading: CSS con
font-display: optionalen base de maquetación (sin cambios).
Véase también
- [[entity—core—service—forgiving-manifest-storage]]
- [[concept—infra—cache-busting]]
- [[concept—ux—font-display]]
- [[decision—20260704—manifest-storage-prod]]