Nette Assets: Konečně jednotné API pro vše od obrázků po Vite
Představuji Nette Assets – knihovnu, která kompletně transformuje práci se statickými soubory v Nette aplikacích. Vyzkoušenou a otestovanou na desítkách projektů.

Kolikrát jste psali kód jako tento?
<link rel="stylesheet" href="{$baseUrl}/css/style.css?v=7">
<img src="{$baseUrl}/images/logo.png?v=1699123456" width="200" height="100" alt="Logo">
Tedy ručně jste konstruovali URL, přidávali versioning parametry, zjišťovali a doplňovali správné rozměry obrázků a tak dále? To je minulost.
Během let vývoje webových aplikací jsem hledal řešení, které bude velmi jednoduché na použití, ale zároveň neomezeně otevřené pro rozšíření. Něco, co budu moci používat na všech svých webech – od jednoduchých prezentaček až po složité e-shopy. Po testování různých přístupů a osvědčení si řady principů na reálných projektech nakonec vzniklo Nette Assets.
S Nette Assets se stejný kód změní na:
{asset 'css/style.css'}
<img n:asset="images/logo.png" alt="Logo">
A to je vše! Knihovna automaticky:
✅ Detekuje rozměry obrázků a vloží je do HTML
✅ Přidá verzovací parametry podle času modifikace souboru
✅ Má nativní podporu Vite s Hot Module Replacement
Žádná konfigurace!
Pojďme začít tím nejjednodušším scénářem: máte složku
www/assets/
plnou obrázků, CSS a JS souborů. Chcete je vkládat
do stránek se správným verzováním a automatickými rozměry. Pak nemusíte
vůbec nic konfigurovat, stačí nainstalovat Nette Assets:
composer require nette/assets
… a můžete začít používat všechny nové Latte značky a budou okamžitě fungovat:
{asset 'css/style.css'}
<img n:asset="images/logo.png">
Značku {asset}
používám hlavně pro CSS a skripty, kdežto
u obrázků mám raději n:asset
, protože chci mít v šabloně
explicitně viditelné HTML elementy jako <img>
– je to
však otázka osobních preferencí.
Šablona vygeneruje podobný kód:
<link rel="stylesheet" href="https://example.cz/assets/css/style.css?v=1670133401">
<img src="https://example.cz/assets/images/logo.png?v=1699123456" width="200" height="100">
Proč to verzování? Prohlížeče si ukládají
statické soubory do cache a když soubor změníte, prohlížeč může stále
používat starou verzi. Tzv cache busting řeší tento problém přidáním
parametru jako ?v=1699123456
, který se změní při každé
úpravě souboru a nutí prohlížeč stáhnout novou verzi. Verzování lze
vypnout v konfiguraci.
Trošku si zakonfigurujeme
Preferujete jiný název složky než assets
? V takovém
případě musíme přidat konfiguraci do common.neon
, nicméně
stačí pouhé tři řádky:
assets:
mapping:
default: static # soubory v /www/static/
Někdy dávám assety na samostatnou subdoménu, jako třeba tady na webu Nette. Konfigurace pak vypadá takto:
assets:
mapping:
default:
path: %rootDir%/www.files # soubory v /www.files/
url: https://files.nette.org
Na blogu jsem chtěl mít pořádek a rozdělit si assety logicky do tří kategorií: design webu (logo, ikony, styly), obrázky v článcích a namluvené verze článků. Každá kategorie má svou vlastní složku:
assets:
mapping:
default: assets # běžné soubory /www/assets/
content: media/images # obrázky článků v /www/media/images/
voice: media/audio # audio soubory v /www/media/audio/
Pro přístup k různým kategoriím používáme dvojtečkovou notaci
kategorie:soubor
. Pokud kategorii nespecifikujete, použije se
výchozí (default
). Takže v šabloně pak mám:
{* Obrázky k článku *}
<img n:asset="content:hero-photo.jpg" alt="Hero image">
{* Audio verze článku *}
<audio n:asset="voice:123.mp3" controls></audio>
V reálných šablonách pochopitelně pracujeme s dynamickými daty.
Článek máte obvykle v proměnné, třeba $post
, takže nemohu
napsat voice:123.mp3
, prostě napíšu:
<audio n:asset="voice:{$post->id}.mp3" controls></audio>
Abych nemusel v šabloně přemýšlet nad audio formátem, můžu do konfigurace přidat automatické doplňování koncovky:
assets:
mapping:
voice:
path: media/audio
extension: mp3 # automaticky doplní .mp3
Pak stačí psát:
<audio n:asset="voice:{$post->id}" controls></audio>
Když později začnu používat kvalitnější formát M4A, bude stačit jen upravit konfiguraci. A protože nemá smysl převádět staré nahrávky, zadám pole možných koncovek – systém je zkusí v daném pořadí a použije první, pro kterou existuje soubor:
assets:
mapping:
audio:
path: media/audio
extension: [m4a, mp3] # zkusí nejdřív M4A, pak MP3
A co když soubor prostě není?
Nojo, ne každý článek má namluvenou verzi. Musím ověřit, jestli
soubor vůbec existuje, a pokud ne, tak element <audio>
nevykreslovat.
Tohle celé vyřeší jediný znak! Otazník v názvu
atributu n:asset?
{* Zobrazí audio player jen pokud existuje namluvená verze *}
<audio n:asset?="voice:{$post->id}" controls></audio>
Stejně tak existuje otázníková varianta značky {asset?}
.
A pokud potřebujete asset obalit do vlastní HTML struktury nebo přidat
podmíněnou logiku vykreslování, můžete použít funkci
asset()
nebo její „otazníkovou“ variantu
tryAsset()
:
{var $voice = tryAsset("voice:{$post->id}")}
<div n:if="$voice" class="voice-version">
<p>🎧 Tento článek si můžete také poslechnout</p>
<audio n:asset=$voice controls></audio>
</div>
Funkce tryAsset()
vrátí objekt assetu nebo null
,
pokud soubor neexistuje. Funkce asset()
vrátí vždy objekt assetu
nebo vyhodí výjimku. Dají se tak s nimi dělat různé kejkle, užitečný
je třeba tento trik s fallbackem, když chcete zobrazit náhradní
obrázek:
{var $avatar = tryAsset("avatar:{$user->id}") ?? asset('avatar:default')}
<img n:asset=$avatar alt="Avatar">
A co vlastně za objekty tyto funkce vlastně vrací? O nich si teď povíme.
Práce s assety v PHP kódu
S assety se pracuje nejen v šablonách, ale i v PHP kódu. K přístupu
k nim slouží třída Registry
, která je hlavním rozhraním
celé knihovny. Tuto službu si necháme předat pomocí dependency
injection:
class ArticlePresenter extends Presenter
{
public function __construct(
private Nette\Assets\Registry $assets
) {}
}
Registry poskytuje metody getAsset()
a
tryGetAsset()
pro získání asset objektů. Každý objekt
reprezentuje nějaký resource. Nemusí jít o fyzický soubor – může být
třeba dynamicky generovaný. Asset nese informace o URL a metadatech.
V namespace Nette\Assets
najdete tyto třídy:
- ImageAsset – obrázky (width, height)
- ScriptAsset – JavaScript soubory (type pro moduly)
- StyleAsset – CSS soubory (media queries)
- AudioAsset / VideoAsset – média (duration, dimensions)
- FontAsset – fonty (správné CORS hlavičky)
- EntryAsset – entry pointy pro Vite
- GenericAsset – ostatní soubory (základní URL a metadata)
Každý asset má readonly vlastnosti specifické pro svůj typ:
// ImageAsset pro obrázky
$image = $this->assets->getAsset('photo.jpg');
echo $image->url; // '/assets/photo.jpg?v=1699123456'
echo $image->width; // 1920
echo $image->height; // 1080
echo $image->mimeType; // 'image/jpeg'
// AudioAsset pro audio soubory
$audio = $this->assets->getAsset('song.mp3');
echo $audio->duration; // délka v sekundách
// ScriptAsset pro JavaScript
$script = $this->assets->getAsset('app.js');
echo $script->type; // 'module' nebo null
Vlastnosti jako rozměry obrázků nebo délka audio souborů se načítají lazy (až když je poprvé použijete), takže systém zůstává rychlý i s tisíci soubory.
First-class Vite integrace
Nette Assets mají nativní podporu pro Vite. Což je moderní nástroj, který řeší nutnost kompilovat (buildovat) frontend kód, protože:
- se píše v jiných jazycích (TypeScript, Sass)
- transpiluje se pro starší prohlížeče
- spojuje se do jednoho souboru
- minifikuje se pro menší velikost
Aby nebylo potřeba ve vývojovém režimu při každé změně celý proces opakovat, servíruje Vite soubory přímo bez spojování a minifikace. Každá změna se tak projeví okamžitě. A jako bonus to dokáže dokonce bez refreshování stránky – upravíte CSS nebo JavaScript a změna se projeví v prohlížeči během milisekund, aniž byste ztratili data ve formulářích nebo stav aplikace. Tomu se říká Hot Module Replacement.
Pro produkci pak Vite vytvoří klasické optimalizované buildy – spojí, zminifikuje, rozdělí do chunks a přidá verzované názvy pro cache busting.
V současném světě JavaScriptu je Vite de facto standard pro moderní frontend vývoj. Proto mi dávalo smysl zahrnout podporu přímo do Nette Assets. Díky otevřené architektuře si ale můžete snadno dopsat mapper pro jakýkoliv jiný bundler.
Možná si říkáte: „Vite adaptéry pro Nette už existují, v čem je tohle jiné?“ Ano, existují, a děkuji za to jejich autorům. Rozdíl je v tom, že Nette Assets je celý systém pro správu všech statických souborů, a Vite je jednou ze součástí, ale velmi elegantně integrovanou.
Do konfigurace common.neon
stačí přidat jen dvě slova:
type: vite
.
assets:
mapping:
default:
type: vite
path: assets
Vite je známé tím, že funguje i bez konfigurace nebo se velmi jednoduše konfiguruje. To ale není úplně pravda, když chcete Vite integrovat s backendem – tam se konfigurace stává složitější. Proto jsem vytvořil @nette/vite-plugin, který celou integraci dělá zase úplně jednoduchou.
V konfiguračním souboru vite.config.ts
stačí aktivovat
plugin a uvést cestu ke vstupním
bodům:
import { defineConfig } from 'vite';
import nette from '@nette/vite-plugin';
export default defineConfig({
plugins: [
nette({
entry: 'app.js', // vstupní bod
}),
],
});
A v šabloně layoutu vstupní bod načteme:
<!doctype html>
<head>
{asset 'app.js'}
</head>
A to je vše! Systém se automaticky postará o ostatní:
- V development módu načítá soubory z Vite dev serveru s Hot Module Replacement
- V produkci používá zkompilované, optimalizované soubory
Žádná konfigurace, žádné podmínky v šablonách. Prostě to funguje.
Síla vlastních mapperů: Reálné příklady
Mapper je komponenta, která ví, kde najít soubory a jak vytvořit URL pro jejich načtení. Můžete mít více mapperů pro různé účely – lokální soubory, CDN, cloud storage nebo build nástroje. Systém Nette Assets je kompletně otevřený pro vytváření vlastních mapperů pro cokoliv.
Mapper pro produktové obrázky
Na eshopu potřebujete obrázky produktů v různých velikostech. Většinou už máte službu, která se stará o resize a optimalizaci obrázků. Stačí kolem ní postavit mapper pro Nette Assets.
Metoda getAsset()
vašeho mapperu musí vracet příslušný typ
assetu. V našem případě ImageAsset
. Pokud konstruktoru zadáme
navíc parametr file
s cestou k lokálnímu souboru, automaticky
načte rozměry obrázku (width, height) a MIME typ. Pokud požadovaný soubor
neexistuje, vyhodíme výjimku AssetNotFoundException
:
class ProductImageMapper implements Mapper
{
public function getAsset(string $reference, array $options = []): Asset
{
// $reference je ID produktu
$product = $this->database->getProduct((int) $reference);
if (!$product) {
throw new Nette\Assets\AssetNotFoundException("Product $reference not found");
}
$size = $options['size'] ?? 'medium'; // small, medium, large
// Využijeme existující službu pro práci s obrázky
return new Nette\Assets\ImageAsset(
url: $this->imageService->getProductUrl($product, $size),
file: $this->imageService->getProductFile($product, $size)
);
}
}
Registrace v konfiguraci:
assets:
mapping:
product: App\ProductImageMapper()
Použití v šablonách:
{* Různé velikosti produktu *}
<img n:asset="product:{$product->id}, size: small" alt="Product thumbnail">
<img n:asset="product:{$product->id}, size: large" alt="Product detail">
Automatické generování OG images
OG (Open Graph) obrázky se zobrazují při sdílení na sociálních sítích. Místo ručního vytváření můžete nechat systém generovat je automaticky podle typu obsahu.
V tomto případě parametr $reference
určuje kontext
obrázku. Pro statické stránky (např. homepage
,
about
) se použijí předpřipravené soubory. Pro články se
obrázek generuje dynamicky na základě názvu článku. Mapper nejprve
zkontroluje cache – pokud už obrázek existuje, vrátí jej. Jinak jej
vygeneruje a uloží do cache pro příští použití:
class OgImageMapper implements Mapper
{
public function getAsset(string $reference, array $options = []): Asset
{
// Pro články generujeme dynamicky podle názvu
if ($reference === 'article') {
$title = $options['title'] ?? throw new LogicException('Missing option title for article');
$filename = '/generated/' . md5("article-{$title}") . '.png';
$path = $this->staticDir . $filename;
if (!file_exists($path)) {
file_put_contents($path, $this->ogGenerator->createArticleImage($title));
}
} else { // Pro statické stránky použijeme předpřipravené soubory
$filename = "/{$reference}.png";
$path = $this->staticDir . $filename;
if (!file_exists($path)) {
throw new Nette\Assets\AssetNotFoundException("Static OG image '$reference' not found");
}
}
return new Nette\Assets\ImageAsset($this->baseUrl . $filename, file: $path);
}
}
V šabloně pak přidáme do hlavičky:
{* Pro statickou stránku *}
{var $ogImage = asset('og:homepage')}
{* Pro článek s dynamickým generováním *}
{var $ogImage = asset('og:article', title: $article->title)}
<meta property="og:image" content={$ogImage}>
<meta property="og:image:width" content={$ogImage->width}>
<meta property="og:image:height" content={$ogImage->height}>
Díky tomuto přístupu máte automaticky vygenerované OG obrázky pro každý článek, které se vytvoří jen jednou a pak se cachují pro další použití.
Nette Assets: elegantnější správa assetů
Nette Assets představují elegantní systém, který se stará téměř o vše automaticky.
- Eliminuje boilerplate kód – žádné více ručního konstruování URL
- Podporuje moderní workflow – Vite, TypeScript, code splitting
- Je flexibilní – vlastní mappery, různé storage systémy
- Funguje hned – žádná složitá konfigurace
Kompletní popis najdete v dokumentaci a kód na GitHubu.
Už nikdy nebudete psát
<img src="/images/photo.jpg?v=123456">
ručně!
Další čtení
- Nette Vite – použití Nette s Vite pro rychlý lokální vývoj
- Nette Utils: generátory výkonu a efektivity
- Jedna řádka v konfiguraci zrychlí vaši Nette aplikaci. Jak je to možné?
- Ako predávať adresáre projektu registrovaným službám
- Latte: jeden řádek a lokalizuje za vás
- Architektura, která roste s vaším projektem
Komentáře
Super. To tam chybelo jako sul, diky.
Jeste to ma mouchy u vite dev to nacte build ne a musel jsem prepsat kompletne celej vite.config
import { defineConfig } from ‚vite‘;
export default defineConfig({
build: {
manifest: true,
outDir: ‚./www/assets‘,
emptyOutDir: true,
rollupOptions: {
input: {
main: ‚/assets/main.js‘,
},
output: {
entryFileNames: ‚[name]-[hash].js‘,
chunkFileNames: ‚[name]-[hash].js‘,
assetFileNames: ‚[name]-[hash][extname]‘,
}
},
},
})
protoze ten config cos tam mel me vyhazoval chybu kvuli index.html
navic to kopirovalo assety do ../www
Velmi podobnou službu dělal https://github.com/…istry/images . Ten se tedy především soustředil na výrobu miniatur a obsluhu formulářů a k tomu mimochodem dělal i tohle mapování souborů – pomocí n:img=„prostor, id“ .
Ale tenhle Nette Assets je mnohem dál. Díky
Chcete-li odeslat komentář, přihlaste se