Asset Management
ZephyrPHP provides a powerful, production-ready asset management system for handling CSS, JavaScript, images, and other static files with automatic versioning, CDN support, SRI hashing, asset collections, and build tool integration.
Overview
The asset system offers:
- Automatic Cache Busting - 5 versioning strategies: timestamp, hash, manifest, global, or none
- CDN Integration - Seamless CDN URL prefixing with configurable enable/disable
- Asset Collections - Define reusable bundles of CSS/JS with priorities
- Queuing System - Enqueue assets from anywhere, render them in layouts
- Subresource Integrity - Automatic SRI hash generation for security
- Build Tool Support - Native Vite and Webpack manifest.json integration
- Performance Hints - Preload, prefetch, preconnect, and fetchpriority support
- Auto-Minification - Automatically serve .min.css/.min.js in production
- Data URI Embedding - Embed small assets as base64 (configurable size limit)
- Lazy Loading - Automatic lazy loading for images with customizable attributes
Configuration
Asset management is configured in config/assets.php. All settings can be overridden via environment variables in .env.
Complete Configuration Reference
Create or edit config/assets.php in your project root:
<?php
return [
// Version strategy: 'timestamp', 'hash', 'manifest', 'global', 'none'
'version_strategy' => env('ASSET_VERSION_STRATEGY', 'timestamp'),
// Global version (used when version_strategy = 'global')
'global_version' => env('ASSET_VERSION', '1.0.0'),
// Environment: 'development', 'staging', 'production'
'environment' => env('ENV', 'development'),
// CDN base URL (null for local serving)
'cdn_url' => env('ASSET_CDN_URL', null),
// Enable/disable CDN (useful for staging environments)
'cdn_enabled' => env('ASSET_CDN_ENABLED', true),
// Auto-replace .css/.js with .min.css/.min.js in production
'minify' => env('ASSET_MINIFY', false),
// Build manifest path (relative to public directory)
'manifest' => env('ASSET_MANIFEST', 'build/manifest.json'),
// Enable Subresource Integrity (SRI) hashes
'integrity' => env('ASSET_INTEGRITY', false),
// Public directory path (auto-detected if null)
'public_path' => null,
// Asset URL prefix (default: 'assets')
'assets_prefix' => 'assets',
// Asset collections (bundles)
'collections' => [
'core' => [
['path' => 'assets/css/reset.css', 'priority' => 1],
['path' => 'assets/css/styles.css', 'priority' => 5],
['path' => 'assets/js/app.js', 'priority' => 10, 'head' => false],
],
'docs' => [
['path' => 'assets/css/styles.css', 'priority' => 1],
['path' => 'assets/css/docs.css', 'priority' => 2],
['path' => 'assets/js/docs.js', 'priority' => 10, 'head' => false],
],
],
// Preload resources (rendered before other assets)
'preload' => [
['path' => 'assets/fonts/inter.woff2', 'as' => 'font'],
['path' => 'assets/css/critical.css', 'as' => 'style'],
],
// Preconnect URLs (establish early connections)
'preconnect' => [
['url' => 'https://fonts.googleapis.com', 'crossorigin' => false],
['url' => 'https://fonts.gstatic.com', 'crossorigin' => true],
],
];
Environment Variables
Configure assets in your .env file:
# Versioning strategy
ASSET_VERSION_STRATEGY=timestamp # Options: timestamp, hash, manifest, global, none
ASSET_VERSION=1.0.0 # Used when strategy is 'global'
# CDN configuration
ASSET_CDN_URL=https://cdn.example.com
ASSET_CDN_ENABLED=true
# Production optimizations
ASSET_MINIFY=true # Auto-use .min.css/.min.js files
ASSET_INTEGRITY=true # Enable SRI hashes
# Build tool manifest
ASSET_MANIFEST=build/manifest.json # Path to Vite/Webpack manifest
Basic Usage
asset() - Generate Asset URLs
Generate a versioned URL for any asset file. Works in both PHP and Twig templates.
// Basic usage
$url = asset('assets/css/styles.css');
// Returns: /assets/css/styles.css?v=1737741234
// With options
$url = asset('assets/images/logo.png', ['absolute' => true]);
// Returns: https://example.com/assets/images/logo.png?v=1737741234
<link rel="stylesheet" href="{{ asset('assets/css/styles.css') }}">
<img src="{{ asset('assets/images/logo.png') }}" alt="Logo">
css() / stylesheet() - Generate CSS Tags
Generate complete <link rel="stylesheet"> tags with automatic versioning, SRI hashes, and CDN URLs.
// Basic stylesheet link
{{ css('assets/css/styles.css') }}
// Output: <link rel="stylesheet" href="/assets/css/styles.css?v=1737741234">
// With custom attributes
{{ css('assets/css/print.css', {media: 'print'}) }}
// Output: <link rel="stylesheet" href="/assets/css/print.css?v=..." media="print">
// Alias function (same functionality)
{{ stylesheet('assets/css/theme.css') }}
// With SRI and CDN (when configured)
{{ css('assets/css/app.css') }}
// Output: <link rel="stylesheet" href="https://cdn.example.com/assets/css/app.css?v=..."
// integrity="sha384-..." crossorigin="anonymous">
js() / script() - Generate JavaScript Tags
Generate <script> tags with automatic versioning, defer/async support, and module type handling.
// Basic script tag
{{ js('assets/js/app.js') }}
// Output: <script src="/assets/js/app.js?v=1737741234"></script>
// With defer (recommended for non-blocking)
{{ js('assets/js/app.js', {defer: true}) }}
// Output: <script src="/assets/js/app.js?v=..." defer></script>
// With async
{{ js('assets/js/analytics.js', {async: true}) }}
// ES6 module with async
{{ script('assets/js/module.js', {type: 'module', async: true}) }}
// Alias function (same functionality)
{{ script('assets/js/utils.js', {defer: true}) }}
// Multiple attributes
{{ js('assets/js/critical.js', {
defer: true,
id: 'critical-script',
'data-theme': 'dark'
}) }}
image() / img() - Generate Image Tags
Generate <img> tags with automatic lazy loading, dimension detection, and modern performance attributes.
// Basic image (lazy loading enabled by default)
{{ image('assets/images/logo.png', 'Company Logo') }}
// Output: <img src="/assets/images/logo.png?v=..." alt="Company Logo" loading="lazy">
// Disable lazy loading for above-the-fold images
{{ img('assets/images/hero.jpg', 'Hero Image', {
loading: 'eager',
fetchpriority: 'high'
}) }}
// For critical LCP images in viewport
// With custom attributes and dimensions
{{ img('assets/images/photo.jpg', 'Photo', {
class: 'rounded shadow-lg',
width: 800,
height: 600,
loading: 'lazy'
}) }}
// Responsive image with srcset
{{ image('assets/images/product.jpg', 'Product', {
srcset: asset('assets/images/product@2x.jpg') ~ ' 2x',
class: 'product-image'
}) }}
// Alias function (same functionality)
{{ img('assets/images/icon.svg', 'Icon', {width: 24, height: 24}) }}
Use loading="eager" and fetchpriority="high" for your Largest Contentful Paint (LCP) image—typically your hero image above the fold. Keep loading="lazy" (default) for all other images.
Asset Queuing System
The queuing system lets you enqueue assets from anywhere in your application (controllers, page templates, components) and render them all at once in your layout. This prevents duplicate includes and gives you fine-grained control over load order.
How It Works
{# pages/blog/post.twig #}
{% do enqueue_css('assets/css/blog.css') %}
{% do enqueue_js('assets/js/blog.js') %}
{# Content here... #}
{# pages/layouts/app.twig #}
<!DOCTYPE html>
<html>
<head>
{{ csp_meta() }}
{# Render preload hints first #}
{{ render_preloads() }}
{# Render all queued CSS #}
{{ render_css() }}
{# Render head scripts (if any) #}
{{ render_js_head() }}
</head>
<body>
{% block content %}{% endblock %}
{# Render body scripts before closing tag #}
{{ render_js() }}
</body>
</html>
Queuing Functions
| Function | Description | Signature |
|---|---|---|
enqueue_css() |
Queue a CSS file | enqueue_css(path, attrs, priority) |
enqueue_js() |
Queue a JavaScript file | enqueue_js(path, attrs, inHead, priority) |
render_css() |
Output all queued CSS | render_css() |
render_js() |
Output body scripts | render_js() |
render_js_head() |
Output head scripts | render_js_head() |
render_preloads() |
Output preload links | render_preloads() |
Priority Control
Control the order in which assets are loaded using priority values. Lower numbers load first.
{# Priority 1 - loads first #}
{% do enqueue_css('assets/css/reset.css', {}, 1) %}
{# Priority 5 - loads second #}
{% do enqueue_css('assets/css/base.css', {}, 5) %}
{# Priority 10 - loads third #}
{% do enqueue_css('assets/css/components.css', {}, 10) %}
{# Priority 20 - loads last #}
{% do enqueue_css('assets/css/page-specific.css', {}, 20) %}
Head vs Body Scripts
JavaScript can be rendered in the <head> or before the closing </body> tag.
{# Load in (third parameter = true) #}
{% do enqueue_js('assets/js/critical.js', {defer: true}, true, 1) %}
{# Load before (third parameter = false or omitted) - default #}
{% do enqueue_js('assets/js/app.js', {defer: true}, false, 10) %}
{# Shorthand for body script #}
{% do enqueue_js('assets/js/utils.js') %}
<head>
{{ render_js_head() }} {# Renders: critical.js #}
</head>
<body>
{# Page content #}
{{ render_js() }} {# Renders: app.js, utils.js #}
</body>
For optimal performance, load most JavaScript before </body> with the defer attribute. Only put critical, above-the-fold scripts in the <head>.
Asset Collections
Asset collections (also called bundles) let you define groups of related CSS and JavaScript files that are commonly used together. Define them once in configuration, then load them with a single function call.
Defining Collections
Define collections in config/assets.php:
<?php
return [
'collections' => [
// Core assets for all pages
'core' => [
['path' => 'assets/css/reset.css', 'priority' => 1],
['path' => 'assets/css/styles.css', 'priority' => 5],
['path' => 'assets/js/app.js', 'priority' => 10, 'head' => false],
],
// Documentation pages
'docs' => [
['path' => 'assets/css/styles.css', 'priority' => 1],
['path' => 'assets/css/docs.css', 'priority' => 2],
['path' => 'assets/css/syntax-highlight.css', 'priority' => 3],
['path' => 'assets/js/docs.js', 'priority' => 10, 'head' => false],
],
// Admin panel
'admin' => [
['path' => 'assets/css/styles.css', 'priority' => 1],
['path' => 'assets/css/admin.css', 'priority' => 2],
['path' => 'assets/js/admin.js', 'priority' => 10, 'head' => false],
['path' => 'assets/js/datatables.js', 'priority' => 15, 'head' => false],
],
// Blog pages
'blog' => [
['path' => 'assets/css/blog.css', 'priority' => 10],
['path' => 'assets/js/blog.js', 'priority' => 10, 'head' => false],
],
],
];
Collection Options
Each asset in a collection can have these options:
| Option | Type | Default | Description |
|---|---|---|---|
path |
string | Required | Path to the asset file |
priority |
int | 10 | Load priority (lower = earlier) |
head |
bool | false | For JS: load in <head> (true) or before </body> (false) |
attrs |
array | [] | Additional HTML attributes (defer, async, media, etc.) |
Loading Collections
Load collections in your templates or controllers:
{# Load a single collection #}
{% do load_assets('core') %}
{# Load multiple collections #}
{% do load_assets('core') %}
{% do load_assets('docs') %}
{# Conditional loading #}
{% if has_collection('admin') %}
{% do load_assets('admin') %}
{% endif %}
{# Then render in layout #}
{{ render_css() }}
{{ render_js() }}
use ZephyrPHP\Asset\Asset;
class DocsController extends Controller
{
public function show(string $page): string
{
// Load the docs collection
Asset::loadCollection('docs');
return $this->render('docs/pages/' . $page);
}
}
Real-World Example
This is how the ZephyrPHP website uses collections:
{# Load core assets for all pages #}
{% do load_assets('core') %}
{# Load documentation assets #}
{% do load_assets('docs') %}
<head>
{{ render_preloads() }}
{{ render_css() }}
{{ render_js_head() }}
</head>
<body>
{% block content %}{% endblock %}
{{ render_js() }}
</body>
Use collections to organize assets by page type or feature. This makes it easy to maintain consistent asset loading across similar pages and prevents forgetting to include required dependencies.
Versioning Strategies
Cache busting ensures browsers always load the latest version of your assets. ZephyrPHP supports 5 versioning strategies.
Available Strategies
| Strategy | Description | Output Example | Best For |
|---|---|---|---|
timestamp |
Uses file modification time (Unix timestamp) | ?v=1737741234 |
Development, staging |
hash |
Uses MD5 hash of file content (first 8 chars) | ?v=a3f4d2e9 |
Production (most reliable) |
manifest |
Reads from Vite/Webpack manifest.json | /build/app-a3f4d2e9.js |
Modern build tools |
global |
Single version string for all assets | ?v=1.0.0 |
Manual versioning, releases |
none |
No version parameter added | /assets/app.js |
Custom CDN caching, testing |
Configuring Versioning
<?php
return [
// Choose your strategy
'version_strategy' => env('ASSET_VERSION_STRATEGY', 'timestamp'),
// Required for 'global' strategy
'global_version' => env('ASSET_VERSION', '1.0.0'),
// Required for 'manifest' strategy
'manifest' => env('ASSET_MANIFEST', 'build/manifest.json'),
];
# Development: use timestamps
ASSET_VERSION_STRATEGY=timestamp
# Production: use content hashes
ASSET_VERSION_STRATEGY=hash
# Build tools: use manifest
ASSET_VERSION_STRATEGY=manifest
ASSET_MANIFEST=build/.vite/manifest.json
# Manual versioning: use global
ASSET_VERSION_STRATEGY=global
ASSET_VERSION=2.1.0
Strategy Comparison
timestamp (Recommended for Development)
- ✅ Fast - no file reading or hashing required
- ✅ Automatic - updates when you modify files
- ✅ Simple - no build tools needed
- ❌ Less reliable - depends on file system modification times
- ❌ Not for distributed systems - timestamps vary across servers
hash (Recommended for Production)
- ✅ Most reliable - based on actual file content
- ✅ Cache-friendly - identical files have identical hashes
- ✅ Works everywhere - no build tool required
- ❌ Slower - requires reading and hashing files (uses caching)
manifest (Recommended for Build Tools)
- ✅ Build tool integration - reads Vite/Webpack output
- ✅ No query strings - uses hashed filenames
- ✅ Fast - single JSON file read
- ❌ Requires build step - not for simple projects
global
- ✅ Simple - one version for everything
- ✅ Manual control - update when you deploy
- ❌ Inefficient - changes all asset URLs even if only one file changed
none
- ✅ Clean URLs - no query parameters
- ❌ No cache busting - requires CDN configuration
CDN Configuration
Serve your assets from a Content Delivery Network (CDN) for faster global delivery. ZephyrPHP automatically prefixes all asset URLs with your configured CDN URL.
Setting Up a CDN
# Production CDN
ASSET_CDN_URL=https://cdn.example.com
ASSET_CDN_ENABLED=true
# Staging (disable CDN for testing)
# ASSET_CDN_URL=https://cdn.example.com
# ASSET_CDN_ENABLED=false
<?php
return [
// CDN base URL (null = local serving)
'cdn_url' => env('ASSET_CDN_URL', null),
// Enable/disable CDN
'cdn_enabled' => env('ASSET_CDN_ENABLED', true),
];
How CDN URLs Work
{{ asset('assets/css/styles.css') }}
{# Output: /assets/css/styles.css?v=1737741234 #}
{{ asset('assets/css/styles.css') }}
{# Output: https://cdn.example.com/assets/css/styles.css?v=1737741234 #}
CDN Template Functions
{# Get CDN URL #}
{% if cdn_url() %}
{# CDN is configured #}
{{ preconnect(cdn_url(), true) }}
{% endif %}
{# Check if CDN is enabled #}
{% if is_production() and cdn_url() %}
<!-- Assets will be served from CDN -->
{% endif %}
Popular CDN Providers
- Cloudflare CDN - Free tier, easy setup, global network
- Amazon CloudFront - AWS-integrated, pay-as-you-go
- Fastly - Real-time purging, enterprise features
- KeyCDN - Affordable, simple pricing
- BunnyCDN - Low-cost, high-performance
When using a CDN, enable ASSET_INTEGRITY=true to add Subresource Integrity (SRI) hashes for security, and use preconnect() to establish early connections.
Performance Optimization
Improve page load performance with resource hints: preload, prefetch, and preconnect.
Preloading Critical Assets
Preload tells the browser to download critical resources as soon as possible, before they're discovered in HTML.
{# Preload a critical CSS file #}
{{ preload('assets/css/critical.css', 'style') }}
{# Output: <link rel="preload" href="/assets/css/critical.css?v=..." as="style"> #}
{# Preload a web font (with crossorigin) #}
{{ preload_font('assets/fonts/inter.woff2') }}
{# Output: <link rel="preload" href="..." as="font" type="font/woff2" crossorigin> #}
{# Preload the LCP image #}
{{ preload('assets/images/hero.webp', 'image') }}
{# Preload with fetchpriority #}
{{ preload('assets/js/critical.js', 'script', {fetchpriority: 'high'}) }}
{# Queue preloads for later rendering #}
{% do enqueue_preload('assets/fonts/custom.woff2', 'font') %}
{% do enqueue_preload('assets/css/above-fold.css', 'style') %}
{# Then render all preloads in #}
{{ render_preloads() }}
What to Preload
- ✅ Above-the-fold CSS
- ✅ Custom web fonts used in visible content
- ✅ Hero/LCP images
- ✅ Critical JavaScript for interactivity
- ❌ Don't preload everything - only what's critical for initial render
Prefetching Future Assets
Prefetch tells the browser to download resources needed for future navigation during idle time.
{# Prefetch next page's JavaScript #}
{{ prefetch('assets/js/dashboard.js', 'script') }}
{# Prefetch next page's stylesheet #}
{{ prefetch('assets/css/dashboard.css', 'style') }}
{# Prefetch an image for the next page #}
{{ prefetch('assets/images/dashboard-bg.jpg', 'image') }}
When to Prefetch
- ✅ Assets for the next likely navigation (e.g., login → dashboard)
- ✅ Assets for hover-revealed content
- ✅ Images for the next carousel slide
- ❌ Don't prefetch assets for unlikely navigation paths
Preconnecting to External Domains
Preconnect establishes early connections (DNS, TCP, TLS) to external domains to reduce latency.
{# Preconnect to your CDN #}
{{ preconnect('https://cdn.example.com', true) }}
{# Preconnect to Google Fonts #}
{{ preconnect('https://fonts.googleapis.com') }}
{{ preconnect('https://fonts.gstatic.com', true) }}
{# Preconnect with configured CDN #}
{% if cdn_url() %}
{{ preconnect(cdn_url(), true) }}
{% endif %}
{# Configure preconnects in config/assets.php #}
{# These will be rendered automatically with render_preloads() #}
<?php
return [
'preconnect' => [
['url' => 'https://fonts.googleapis.com', 'crossorigin' => false],
['url' => 'https://fonts.gstatic.com', 'crossorigin' => true],
['url' => 'https://analytics.example.com', 'crossorigin' => false],
],
];
When to Preconnect
- ✅ Your CDN domain
- ✅ Google Fonts or other font providers
- ✅ Analytics services
- ✅ Third-party APIs you'll call on page load
- ❌ Don't preconnect to more than 3-4 domains - diminishing returns
Inline Assets
Add inline CSS or JavaScript directly in the HTML, useful for critical styles or configuration.
{# Add inline CSS (rendered in ) #}
{% do inline_css('.hero { background: linear-gradient(135deg, #667eea, #764ba2); }') %}
{% do inline_css('body { font-family: Inter, sans-serif; }') %}
{# Render all inline CSS #}
{{ render_css() }}
{# Outputs both external tags and