Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 20 de 45

Ficheros estáticos

El CSS, las imágenes y el JavaScript, por qué no se ponen con una ruta a mano, y el día que el sitio se queda sin estilos.

⏱ 14 min de lectura

Las páginas que llevas son HTML pelado. Ponerles estilos parece lo más tonto del mundo: un <link rel="stylesheet" href="/css/estilo.css"> y listo. Y sin embargo Django tiene una aplicación entera, un ajuste, una etiqueta y una orden solo para esto.

El motivo es que en desarrollo y en producción esos ficheros no se sirven igual, ni desde el mismo sitio, ni con la misma dirección. Django resuelve el desarrollo automáticamente y prepara el terreno para lo otro, y si escribes las rutas a mano la mudanza duele. Esta lección explica ese reparto ahora, para que el bloque F sea aburrido.

Dónde van#

Igual que las plantillas, y por el mismo motivo:

text
contactos/
  static/
    contactos/
      estilo.css
      logotipo.png

La carpeta se repite otra vez a propósito. Todos los ficheros estáticos del proyecto acaban mezclados en una sola carpeta, así que sin ese espacio de nombres el estilo.css de una aplicación pisa el de otra. Lo mismo que la lección 16, con la misma consecuencia: gana el primero en INSTALLED_APPS y el otro desaparece sin avisar.

Y funciona sin configurar nada por lo mismo: django.contrib.staticfiles viene en INSTALLED_APPS y busca una carpeta static/ dentro de cada aplicación.

{% static %}#

plantillas/base.html
{% load static %}
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <title>{% block titulo %}Agenda{% endblock %}</title>
  <link rel="stylesheet" href="{% static 'contactos/estilo.css' %}">
</head>
Salida en pantalla
<link rel="stylesheet" href="/static/contactos/estilo.css">

Lo que devuelve es lo mismo que habrías escrito a mano, y esa es justo la pregunta: para qué.

Porque esa dirección cambia. El día que el sitio sirva los estáticos desde /assets/, desde un subdominio o desde una red de distribución, cambia el ajuste y las cien plantillas siguen valiendo. Y porque en producción los nombres de los ficheros llevan un trozo añadido, como se ve más abajo, y esa etiqueta es la que sabe cuál toca.

El {% load static %} va en cada plantilla que lo use. En la base también, aunque la hereden todas: el load no se hereda, como se vio en la lección 19.

Los tres ajustes que se parecen#

Aquí es donde se lía casi todo el mundo, porque los tres nombres se parecen y hacen cosas distintas.

AjusteQué es
STATIC_URLEl prefijo de las direcciones. Lo único que viene puesto
STATICFILES_DIRSCarpetas de estáticos que no son de ninguna aplicación
STATIC_ROOTDónde se copian todos juntos para producción. Vacío en desarrollo
agenda/settings.py
STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / 'estaticos']
STATIC_ROOT = BASE_DIR / 'estaticos_recogidos'

El reparto, en una frase cada uno. STATICFILES_DIRS es a los estáticos lo que DIRS a las plantillas: la carpeta del proyecto para lo común, el CSS del sitio entero. STATIC_ROOT es un destino, no un origen: nadie pone ficheros ahí, los pone collectstatic, y la carpeta va al .gitignore.

Confundir STATIC_ROOT con STATICFILES_DIRS y poner los ficheros dentro del primero es el error clásico, y el síntoma es que collectstatic se queja de que el origen y el destino son el mismo sitio.

collectstatic#

En desarrollo, Django busca los estáticos en la carpeta de cada aplicación, una por una, cuando se los piden. Eso es cómodo y lento, y un servidor de verdad no quiere hacerlo: quiere una carpeta con todo dentro y servirla sin preguntar.

consola
python manage.py collectstatic
Salida en pantalla
132 static files copied to '/tmp/m2/estaticos_recogidos'.

Ciento treinta y dos, y solo has escrito uno. Los demás son del panel de administración, que también tiene los suyos. Y ahí está la razón de ser de la orden: recoge los estáticos de todas las aplicaciones instaladas, las tuyas y las ajenas, en un único sitio.

Se ejecuta al desplegar, nunca en desarrollo, y el resultado no se guarda en el control de versiones porque se puede regenerar.

El día que el sitio se queda sin estilos#

Este momento le llega a todo el mundo y conviene reconocerlo de lejos. Con DEBUG = False:

Salida en pantalla
css con DEBUG=False: 404
pagina:              200

La página carga, funciona, y está en blanco y negro. runserver deja de servir los estáticos cuando DEBUG es False, y lo hace a propósito: ese servidor no es para producción, como recuerda el aviso de la lección 5, y servir ficheros con él sería ineficiente e inseguro.

Lo que sirve los estáticos en producción es otra cosa: el servidor web de delante, un WhiteNoise dentro de la aplicación, o un servicio aparte. Es la lección 43.

Mientras tanto, la conclusión práctica es que DEBUG = False en el portátil no sirve para probar el sitio, solo para probar cosas concretas como las páginas de error. Y si pruebas eso, no te alarmes por los estilos.

El hash de los nombres#

Falta una pieza, y es la que explica del todo por qué no se escriben las rutas a mano. Los navegadores guardan el CSS en memoria durante días, así que un cambio en los estilos no le llega a quien ya visitó el sitio.

La solución es que el nombre del fichero cambie cuando cambie su contenido:

agenda/settings.py
STORAGES = {
    'default': {'BACKEND': 'django.core.files.storage.FileSystemStorage'},
    'staticfiles': {'BACKEND': 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'},
}
Salida en pantalla
132 static files copied to '...', 132 post-processed.

estilo.css
estilo.9dcfc804e470.css
staticfiles.json

Cada fichero aparece dos veces: con su nombre y con un resumen de su contenido dentro. El staticfiles.json es el índice que relaciona uno con otro, y {% static %} lo consulta para escribir el nombre con hash en la página.

Con eso se puede decirle al navegador que guarde esos ficheros un año entero, porque si el contenido cambia, cambia el nombre, y el navegador pide uno que no tiene. Es la única forma de tener a la vez caché agresiva y despliegues que se ven al momento.

Y es la respuesta definitiva a la pregunta del principio: con la ruta escrita a mano, esto no funciona.