Nette Assets

¿Cansado de gestionar a mano los archivos estáticos de sus aplicaciones web? Olvídese de escribir rutas a mano, de lidiar con la invalidación de la caché o de preocuparse por el versionado de los archivos. Nette Assets transforma la forma de trabajar con imágenes, hojas de estilo, scripts y otros recursos estáticos.

  • El versionado inteligente asegura que los navegadores carguen siempre los archivos más recientes
  • Detección automática de los tipos de archivo y de sus dimensiones
  • Integración perfecta con Latte mediante etiquetas intuitivas
  • Arquitectura flexible que soporta sistemas de archivos, CDN y Vite
  • Carga diferida para un rendimiento óptimo

¿Por qué Nette Assets?

Trabajar con archivos estáticos suele significar código repetitivo y propenso a errores. Construye las URL a mano, añade parámetros de versión para invalidar la caché y trata cada tipo de archivo de forma distinta. Eso lleva a un código como este:

<img src="/images/logo.png?v=1699123456" width="200" height="100" alt="Logo">
<link rel="stylesheet" href="/css/style.css?v=2">

Con Nette Assets toda esa complejidad desaparece:

{* Todo automatizado: URL, versionado, dimensiones *}
<img n:asset="images/logo.png">
<link n:asset="css/style.css">

{* O simplemente *}
{asset 'css/style.css'}

¡Eso es todo! La biblioteca automáticamente:

  • Añade los parámetros de versión según la fecha de modificación del archivo
  • Detecta las dimensiones de las imágenes y las incluye en el HTML
  • Genera el elemento HTML correcto para cada tipo de archivo
  • Se ocupa tanto del entorno de desarrollo como del de producción

Instalación

Instale Nette Assets con Composer:

composer require nette/assets

Requiere PHP 8.1 o superior y funciona perfectamente con Nette Framework, pero también se puede usar por separado.

Primeros pasos

Nette Assets funciona nada más instalarlo, sin ninguna configuración. Coloque sus archivos estáticos en el directorio www/assets/ y empiece a usarlos:

{* Muestra una imagen con las dimensiones automáticas *}
{asset 'logo.png'}

{* Incluye una hoja de estilo con versionado *}
{asset 'style.css'}

{* Carga un script *}
{asset 'app.js'}

Para tener más control sobre el HTML generado, use el atributo n:asset o la función asset().

Cómo funciona

Nette Assets se construye en torno a tres conceptos básicos que lo hacen potente y a la vez sencillo de usar:

Assets: sus archivos, con inteligencia

Un asset representa cualquier archivo estático de su aplicación. Cada archivo se convierte en un objeto con propiedades de solo lectura útiles:

$image = $assets->getAsset('photo.jpg');
echo $image->url;      // '/assets/photo.jpg?v=1699123456'
echo $image->file;     // '/var/www/assets/photo.jpg' (ruta local, o null)
echo $image->width;    // 1920
echo $image->height;   // 1080
echo $image->mimeType; // 'image/jpeg'

Los distintos tipos de archivo proporcionan propiedades distintas:

  • Imágenes: ancho, alto, texto alternativo, carga diferida
  • Scripts: tipo de módulo, hashes de integridad, crossorigin
  • Hojas de estilo: media queries, integridad
  • Audio y vídeo: duración, dimensiones (solo el vídeo)
  • Fuentes: precarga correcta con CORS

La biblioteca detecta automáticamente los tipos de archivo y crea la clase de asset adecuada.

Mappers: de dónde vienen los archivos

Un mapper sabe cómo encontrar los archivos y crear las URL para ellos. Puede tener varios mappers para distintos fines: archivos locales, CDN, almacenamiento en la nube o herramientas de compilación (cada uno de ellos tiene un nombre). El FilesystemMapper integrado se ocupa de los archivos locales, mientras que ViteMapper se integra con las herramientas de compilación modernas.

Los mappers se definen en la configuración.

Registry: su interfaz principal

El registry gestiona todos los mappers y proporciona la API principal:

// Inyecte el registry en su servicio
public function __construct(
	private Nette\Assets\Registry $assets
) {}
// Obtiene assets de distintos mappers
$logo = $this->assets->getAsset('images:logo.png'); // mapper 'images'
$app = $this->assets->getAsset('app:main.js'); // mapper 'app'
$style = $this->assets->getAsset('style.css'); // usa el mapper predeterminado

El registry elige automáticamente el mapper correcto y cachea los resultados por rendimiento.

Trabajar con assets en PHP

El Registry proporciona dos métodos para obtener los assets:

// Lanza Nette\Assets\AssetNotFoundException si el archivo no existe
$logo = $assets->getAsset('logo.png');

// Devuelve null si el archivo no existe
$banner = $assets->tryGetAsset('banner.jpg');
if ($banner) {
	echo $banner->url;
}

Indicar el mapper

Puede elegir explícitamente qué mapper usar:

// Usa el mapper predeterminado
$file = $assets->getAsset('document.pdf');

// Usa un mapper concreto con prefijo
$image = $assets->getAsset('images:photo.jpg');

// Usa un mapper concreto con la sintaxis de array
$script = $assets->getAsset(['scripts', 'app.js']);

Propiedades y tipos de los assets

Cada tipo de asset proporciona las propiedades de solo lectura pertinentes:

// Propiedades de una imagen
$image = $assets->getAsset('photo.jpg');
echo $image->width;     // 1920
echo $image->height;    // 1080
echo $image->mimeType;  // 'image/jpeg'

// Propiedades de un script
$script = $assets->getAsset('app.js');
echo $script->type;     // null ('module' en los puntos de entrada de Vite)

// Propiedades de un audio
$audio = $assets->getAsset('song.mp3');
echo $audio->duration;  // duración en segundos

// Todos los assets se pueden convertir a cadena (devuelve la URL)
$url = (string) $assets->getAsset('document.pdf');

Las propiedades como las dimensiones o la duración se cargan de forma diferida, solo al acceder a ellas, lo que mantiene la biblioteca rápida.

Para un análisis estático preciso, instale la extensión nette/phpstan-rules. PHPStan conoce entonces el tipo concreto de cada asset, así que getAsset('photo.jpg') se entiende como ImageAsset y acceder a ->width no provoca ningún error.

Usar los assets en las plantillas de Latte

Nette Assets proporciona una integración intuitiva con Latte mediante etiquetas y funciones.

{asset}

La etiqueta {asset} renderiza elementos HTML completos:

{* Renderiza: <img src="/assets/hero.jpg?v=123" width="1920" height="1080"> *}
{asset 'hero.jpg'}

{* Renderiza: <script src="/assets/app.js?v=456"></script> *}
{asset 'app.js'}

{* Renderiza: <link rel="stylesheet" href="/assets/style.css?v=789"> *}
{asset 'style.css'}

La etiqueta automáticamente:

  • Detecta el tipo de asset y genera el HTML adecuado
  • Incluye el versionado para invalidar la caché
  • Añade las dimensiones de las imágenes
  • Establece los atributos correctos (type, media, etc.)

Cuando se usa dentro de atributos HTML o dentro de los elementos <style> y <script>, imprime solo la URL:

<div style="background-image: url({asset 'bg.jpg'})">
<img srcset="{asset 'logo@2x.png'} 2x">

n:asset

Para tener pleno control sobre los atributos HTML:

{* El atributo n:asset rellena src, las dimensiones, etc. *}
<img n:asset="product.jpg" alt="Product" class="rounded">

{* Funciona con cualquier elemento pertinente *}
<script n:asset="analytics.js" defer></script>
<link n:asset="print.css" media="print">
<audio n:asset="podcast.mp3" controls></audio>

Use variables y mappers:

{* Las variables funcionan con naturalidad *}
<img n:asset="$product->image">

{* Indique el mapper con llaves *}
<img n:asset="images:{$product->image}">

{* Indique el mapper con la notación de array *}
<img n:asset="[images, $product->image]">

n:asset también funciona en <a>, donde rellena href, y en <link>, donde crea una pista de precarga:

<a n:asset="hero.jpg">Download image</a>

Tenga en cuenta que las variantes <a> y <link> funcionan solo con assets renderizables (una imagen, un script, etc.), nunca con un GenericAsset como un PDF.

En las imágenes basta con indicar solo el width (o solo el height) y la otra dimensión se calcula automáticamente para conservar la relación de aspecto:

{* height se completa a partir de la relación de aspecto *}
<img n:asset="product.jpg" width="200">

asset()

Para la máxima flexibilidad, use la función asset():

{var $logo = asset('logo.png')}
<img src={$logo} width={$logo->width} height={$logo->height}>

{* O directamente *}
<img src={asset('logo.png')} alt="Logo">

Assets opcionales

Trate con elegancia los assets que faltan usando {asset?}, n:asset? y tryAsset():

{* Etiqueta opcional: no renderiza nada si falta el asset *}
{asset? 'optional-banner.jpg'}

{* Atributo opcional: se salta si falta el asset *}
<img n:asset?="user-avatar.jpg" alt="Avatar" class="avatar">

{* Con alternativa *}
{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')}
<img n:asset=$avatar alt="Avatar">

{preload}

Mejore el rendimiento de carga de la página:

{* En su sección <head> *}
{preload 'critical.css'}
{preload 'important-font.woff2'}
{preload 'hero-image.jpg'}

Genera los enlaces de precarga adecuados:

<link rel="preload" href="/assets/critical.css?v=123" as="style">
<link rel="preload" href="/assets/important-font.woff2?v=456" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/hero-image.jpg?v=789" as="image" type="image/jpeg">

Cuando su respuesta establece una cabecera Content-Security-Policy con un nonce, Nette añade automáticamente el atributo nonce correspondiente a cada elemento <script>, <link> y <style> generado, para que no los bloquee la política de seguridad del navegador.

Funciones avanzadas

Detección automática de la extensión

Trate varios formatos automáticamente:

assets:
	mapping:
		images:
			path: img
			extension: [webp, jpg, png]  # Prueba en este orden

Ahora puede pedirlo sin extensión:

{* Encuentra logo.webp, logo.jpg o logo.png automáticamente *}
{asset 'images:logo'}

Perfecto para la mejora progresiva con formatos modernos.

Versionado inteligente

Los archivos se versionan automáticamente según su fecha de modificación:

{asset 'style.css'}
{* Salida: <link rel="stylesheet" href="/assets/style.css?v=1699123456"> *}

Cuando actualiza el archivo, la marca de tiempo cambia, lo que fuerza el refresco de la caché del navegador.

Controle el versionado por asset:

// Desactiva el versionado de un asset concreto
$asset = $assets->getAsset('style.css', ['version' => false]);
{* En Latte *}
{asset 'style.css', version: false}

La misma sintaxis referencia, clave: valor pasa las opciones también a n:asset y a {preload}:

<img n:asset="photo.jpg, version: false">
{preload 'style.css', version: false}

Assets de fuentes

Las fuentes reciben un trato especial con el CORS correcto:

{* Precarga correcta con crossorigin *}
{preload 'fonts:OpenSans-Regular.woff2'}

{* Uso en CSS *}
<style>
@font-face {
	font-family: 'Open Sans';
	src: url('{asset 'fonts:OpenSans-Regular.woff2'}') format('woff2');
	font-display: swap;
}
</style>

Mappers propios

Cree mappers propios para necesidades especiales, como el almacenamiento en la nube o la generación dinámica:

use Nette\Assets\Mapper;
use Nette\Assets\Asset;
use Nette\Assets\Helpers;

class CloudStorageMapper implements Mapper
{
	public function __construct(
		private CloudClient $client,
		private string $bucket,
	) {}

	public function getAsset(string $reference, array $options = []): Asset
	{
		if (!$this->client->exists($this->bucket, $reference)) {
			throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found");
		}

		$url = $this->client->getPublicUrl($this->bucket, $reference);
		return Helpers::createAssetFromUrl($url);
	}
}

Regístrelo en la configuración:

assets:
	mapping:
		cloud: CloudStorageMapper(@cloudClient, 'my-bucket')

Úselo como cualquier otro mapper:

{asset 'cloud:user-uploads/photo.jpg'}

El método Helpers::createAssetFromUrl() crea automáticamente el tipo de asset correcto según la extensión del archivo.

Los tipos de asset que implementan la interfaz Nette\Assets\HtmlRenderable (imágenes, scripts, estilos, etc.) los puede renderizar {asset} como un elemento HTML completo. Los demás tipos de archivo se convierten en un GenericAsset (por ejemplo un PDF), que no se puede renderizar como elemento HTML pero sigue proporcionando una URL (y otros metadatos). Intentar renderizar un asset así como elemento HTML lanza Nette\InvalidArgumentException; su URL sí la puede usar dentro de un atributo.

Lecturas adicionales

versión: 1.x