Ir al contenido
Código en GitHub (se abre en una pestaña nueva)

Documentación · Referencia · El formato de un proyecto

9. Ficheros, CSS y JavaScript

Lo que se copia tal cual, y el CSS y el JavaScript de cada página.

Versión 0.16

publico/ se copia tal cual a la raíz de la salida, incluidos los ficheros que empiezan por punto, como un .htaccess, salvo los que dejan el sistema o el control de versiones: .DS_Store, .gitkeep, Thumbs.db, desktop.ini y cualquier carpeta .git, que no se copian y no avisan (decisión 138). La lista es corta y cerrada. En contenido/ pasa igual con Thumbs.db y desktop.ini; lo demás que empieza por punto ya se ignora (§4). Sin pipeline: el CSS y el JS llegan ya escritos y, si hace falta, minificados fuera del motor.

Lo que no se copia. Ni de publico/ ni de contenido/ se copia un fichero PHP: uno cuyo nombre tenga como extensión .php, de .php3 a .php8, .pht, .phtml o .phar, en mayúsculas o en minúsculas, y aunque no sea la última (foto.php.jpg: un servidor con AddHandler la ejecuta igual). El build se detiene con un error que nombra el fichero. Un sitio de Ehundu es estático y ese fichero no lo ha escrito el motor: lo normal es que sea un resto de otro gestor de contenidos, o algo que alguien dejó en una carpeta de imágenes, y publicarlo lo pondría a ejecutarse en el servidor. Hoy no hay forma de permitirlo.

Las imágenes no se procesan durante el build. Si hacen falta varios tamaños, los genera otra herramienta (por ejemplo, un editor al subir la imagen, §11.6) y deja los ficheros en publico/, con el nombre que dice la sección imagenes de sitio.yml (§2). El motor solo los lee: variantes() (§7.3), el atajo imagen y las figuras del Markdown (§8) escriben el srcset con lo que encuentran.

9.1 CSS y JavaScript de cada página

El CSS y el JavaScript de una página se incrustan en ella, reunidos en un <style> y un <script>. Es el equivalente del plugin de bundle de Eleventy: cada plantilla declara lo que necesita donde lo usa, y todo se escribe en un solo sitio.

  • {{ css('css/cabecera.css') }}, en cualquier plantilla, parcial o atajo, declara uno o varios ficheros de publico/ que necesita la página. No escribe nada donde está.
  • {{ css() }}, sin argumentos, marca dónde va todo el CSS de la página: <style>{{ css() }}</style> en el layout base. Se rellena cuando la página está terminada, así que cuentan también los parciales que van después, como el pie.
  • El campo css del front matter añade los ficheros de la página. Se suma a lo largo de la cascada (§3): un _datos.yml puede añadir el CSS de una sección entera.

Cada fichero sale una sola vez por página, en el orden en que se declara al construirla, de arriba abajo; los del campo css van al final, para que puedan sobrescribir a los demás, y uno ya declarado no cambia de sitio (si el layout declara css/a.css y el front matter lo repite, sigue donde lo puso el layout). Se insertan con los saltos de línea normalizados (§1), unidos por un salto de línea. Un fichero que no existe, o que no es un .css de dentro de publico/, se avisa y se salta; uno que no es UTF-8 detiene el build. Si una página declara CSS y ninguna plantilla escribe {{ css() }} (ni {{ css_fichero() }}, más abajo), se avisa.

El JavaScript funciona igual, con js('…'), <script>{{ js() }}</script> y el campo js.

{# plantillas/base.twig #}
<head>
  {{ css('css/reset.css', 'css/estilos.css') }}
  <style>{{ css() }}</style>
</head>
<body>
  {% include 'parciales/cabecera.twig' %}  {# declara css/cabecera.css #}
  {% block cuerpo %}{% endblock %}
  <script>{{ js() }}</script>
</body>

Como fichero, en lugar de incrustados. Incrustar es lo mejor con poco CSS: la página llega con todo y no hace falta otra petición. Con mucho, repite el mismo CSS en cada página y el navegador no puede guardarlo. css_fichero() y js_fichero() hacen lo mismo que css() y js() con lo que la página ha declarado (los mismos ficheros, en el mismo orden, una vez cada uno), pero en lugar de su contenido escriben la etiqueta que enlaza un fichero que contiene ese contenido, el paquete de esa página (el CSS o el JavaScript que declara, reunido en uno):

  • {{ css_fichero() }} escribe <link rel="stylesheet" href="/css/3fa9c1d2ab.css">.
  • {{ js_fichero() }} escribe <script src="/js/7c01e4b9d3.js" defer></script>.

Las dos funciones solo marcan el sitio, como css() y js() sin argumentos: los ficheros se declaran con css('…') y js('…'), y si se los das a css_fichero() se avisa y se ignoran.

El fichero lo escribe el motor en salida/css/ o salida/js/, y su nombre es la huella del contenido: los 10 primeros caracteres hexadecimales de su SHA-256. Las páginas que declaran lo mismo comparten el mismo fichero, y si cambia cualquiera de los ficheros de publico/ que lo forman, cambia el nombre: ni el navegador ni una caché intermedia pueden servir uno viejo. Los ficheros que dejan de usarse desaparecen de salida/ como cualquier otro (§10). Si una página no declara nada, o no se puede leer nada de lo que declara, no se escribe ninguna etiqueta. Si dos cosas van al mismo fichero, por ejemplo un publico/css/3fa9c1d2ab.css, el build se detiene (§10).

Como el nombre cambia con el contenido, el fichero se puede servir con una caducidad de un año. En Apache, con un .htaccess en publico/:

<FilesMatch "^[0-9a-f]{10}\.(css|js)$">
  Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

Una plantilla puede usar la forma que le convenga: css() para un CSS corto que quiere en la propia página, css_fichero() para uno largo. Lo declarado es lo mismo en las dos. Dentro de un CSS que se enlaza, las direcciones de url(…) tienen que empezar por /, como ya pasa en uno incrustado: las relativas se resolverían desde donde esté el fichero o la página, y un paquete reúne ficheros de carpetas distintas. En la plantilla de un PDF (§10.3) solo vale css().

{# plantillas/base.twig #}
<head>
  {{ css('css/base.css') }}
  {{ css_fichero() }}
</head>
<body>
  {% block cuerpo %}{% endblock %}
  {{ js_fichero() }}
</body>

Ver este documento en GitHub (se abre en una pestaña nueva)