Identidad
Módulo: core/storage.py (nuevo en v1.45.3)
Tipo: Django storage backend, subclase de whitenoise.storage.CompressedManifestStaticFilesStorage
Propósito: servir estáticos con nombres hasheados (app.a1b2c3d4e5f6.js) para activar caché de 1 año + cache-busting automático en deploy, tolerando errores de post-procesado de CSS de terceros.
Firma pública
class ForgivingManifestStaticFilesStorage(CompressedManifestStaticFilesStorage):
manifest_strict = False
def post_process(self, paths, dry_run=False, **options):
# Yields (name, hashed_name, processed) tuples;
# degrada MissingFileError a False (no reescribir CSS) en vez de abortar
Atributos clave:
manifest_strict = False— lookups de estáticos sin entrada en el manifest devuelven el nombre original, no error.post_process()— override que envuelve el post-procesado del padre en un try-except implícito (viaisinstance), capturandoMissingFileErrory convirtiéndolo aprocessed=False.
Cómo funciona
Phase 1: Build-time (Dockerfile.prod, collectstatic)
- Django ejecuta
manage.py collectstatic --noinput. - Todas las rutas en
STATIC_ROOT(/app/staticfiles/) se copian. - Para cada archivo CSS: se reescribe cada
url()interna con su nombre hasheado (p.ej.url(../fonts/oswald.woff2)→url(../fonts/oswald.a1b2c3d4e5f6.woff2)). - Si una
url()no resoluble aparece (p.ej. FontAwesomeall.min.cssreferenciafa-v4compatibility.woff2ausente):- Antes (sin override):
MissingFileError→collectstaticaborta RC ≠ 0 → Docker falla → no se generastaticfiles.json. - Ahora (con override): captura el error, devuelve
processed=False(ese CSS se sirve sin reescribir las URLs, pero el build continúa) →staticfiles.jsongenerado, resto de estáticos OK.
- Antes (sin override):
Phase 2: Runtime (WhiteNoise)
- Django inicia; WhiteNoise se intercepta en la cadena WSGI.
- Request a
/static/...llega a WhiteNoise con el archivo hasheado (p.ej./static/fonts/oswald.a1b2c3d4e5f6.woff2). - WhiteNoise detecta el nombre inmutable (tiene hash) → sirve con
Cache-Control: max-age=31536000, immutable. - Navegador cachea por 1 año. Cambio en el deploy = nuevo hash = nuevo nombre = nueva descarga (old name sigue en caché vieja, no afecta).
Phase 3: Fallback (manifest_strict = False)
- Si un
{% static "foo.js" %}en un template intenta un lookup sin entrada en el manifest (p.ej. archivo descargado en runtime):- Antes (strict=True):
KeyError. - Ahora (strict=False): devuelve
"foo.js"(nombre original), se sirve sin hash, cae al caché corto de WhiteNoise (~24h).
- Antes (strict=True):
Uso en settings
Producción / Staging (config/settings/production.py)
STORAGES = {
**STORAGES,
"staticfiles": {"BACKEND": "core.storage.ForgivingManifestStaticFilesStorage"},
}
Este override aplica solo en producción (porque production.py se importa condicionalmente; ver config/wsgi.py).
Desarrollo / Testing
Hereda CompressedStaticFilesStorage de config/settings/base.py — sin manifest, sin hash, no requiere collectstatic previo.
Tolerancia de errores: el caso FontAwesome
Archivo: templates/... incluye {% static "vendor/fontawesome/all.min.css" %} (comprimido, es el bundle completo de FontAwesome).
Referencia interna: el CSS contiene:
@font-face {
font-family: "Font Awesome 6 Pro";
src: url("../webfonts/fa-v4compatibility.woff2") format("woff2");
...
}
Status en el árbol:
all.min.css✓ existe (se copia encollectstatic).fa-v4compatibility.woff2✗ no existe (es opcional, para compatibilidad con código viejo; usamossubset.css+ woff2 de marca).
Sin el override:
> python manage.py collectstatic --noinput
Processing static files...
ERROR: core/storage.py - MissingFileError: ... fa-v4compatibility.woff2 not found
collectstatic aborted (RC ≠ 0)
[Docker build FAIL]
Con el override (processed = False):
> python manage.py collectstatic --noinput
Processing static files...
vendor/fontawesome/all.min.css: could not rewrite urls (missing fa-v4compatibility.woff2), serving as-is
... [resto de estáticos procesados normalmente] ...
Generated staticfiles.json ✓
[Docker build OK]
Impacto en caché y deploy
| Escenario | Antes | Después |
|---|---|---|
Deploy nuevo (collectstatic normal) | Todos los estáticos siguen en caché 60 s | Estáticos con nuevo hash → navegador descarga nuevo, viejo queda en caché vieja |
| Usuario recarga en < 60 s después de deploy anterior | Ve la versión vieja (caché) | Sigue viendo vieja (manifest obsoleto en navegador) → recarga fría descarga nueva |
| Estático corrupto / cambio de contenido | Necesita usuario forzar refresco (Ctrl+Shift+R) | Automático con nuevo hash |
Conclusión: ForgivingManifestStaticFilesStorage es la pieza clave del cache-busting automático por hash. Sin ella, el manifest no se genera y WhiteNoise sigue sirviendo con max-age=60.
Dependencias
- Django: 4.2+ (storage backend API estable).
- WhiteNoise: 6.4+ (entiende
max-age=31536000, immutableen nombres con hash). - Django Compressor (opcional pero típico): compatible.
Testing
- Build-time:
Dockerfile.prod→collectstatic RC=0+staticfiles.jsonpresente. - Runtime:
python manage.py collectstaticen dev no aborta;{% static %}en templates resuelve a nombres hasheados. - Navegador: dev tools → Network → fuentes con nombre hash,
Cache-Control: max-age=31536000, immutable.
Véase también
- [[feature—infra—fix-flash-tipografia-cache]]
- [[concept—infra—cache-busting]]
- [[concept—django—staticfiles-manifest]]
- [[entity—core—model—organization]]
Referenciado desde
- Add Rack creaba dos racks — doble instancia de módulo por manifest hasheado (v1.77.3)
- Cache stale de base.js/alpine-components.js por versionado manual olvidado (v1.73.1)
- El service worker fantasma: la limpieza pasaba una sola vez, ahora es incondicional (v1.73.2)
- Fix de flash de tipografía con caché inmutable (v1.45.3)