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:

config/assets.php
<?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:

.env
# 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.

PHP
// 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
Twig
<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.

Usage
// 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.

Usage
// 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.

Usage
// 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}) }}
Performance Tip

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

Step 1: Enqueue Assets (in page templates or controllers)
{# pages/blog/post.twig #}
{% do enqueue_css('assets/css/blog.css') %}
{% do enqueue_js('assets/js/blog.js') %}

{# Content here... #}
Step 2: Render Assets (in layout template)
{# 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.

Example
{# 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.

Usage
{# 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') %}
Layout Rendering
<head>
    {{ render_js_head() }}  {# Renders: critical.js #}
</head>
<body>
    {# Page content #}

    {{ render_js() }}       {# Renders: app.js, utils.js #}
</body>
Best Practice

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:

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:

Twig Templates
{# 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() }}
PHP Controllers
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:

pages/layouts/app.twig (Main Layout)
{# 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>
Pro Tip

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

config/assets.php
<?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'),
];
.env
# 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

.env
# 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
config/assets.php
<?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

Without CDN (Local)
{{ asset('assets/css/styles.css') }}
{# Output: /assets/css/styles.css?v=1737741234 #}
With CDN Enabled
{{ asset('assets/css/styles.css') }}
{# Output: https://cdn.example.com/assets/css/styles.css?v=1737741234 #}

CDN Template Functions

Usage
{# 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 %}
  • 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
Performance Tip

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.

Usage
{# 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.

Usage
{# 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.

Usage
{# 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() #}
config/assets.php
<?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.

Inline CSS
{# 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