SimpleAsset

Asset management for ProcessWire. Resolves, groups, and renders CSS/JS assets from CDN sources or local paths with cache-busting, SRI, and inline threshold support.

Alpha — v0.1.0. This module is in early testing. The API may change before a stable release. Asset combining and minification are planned but not yet active. Feedback and bug reports are welcome.

A simple, flexible asset management system for ProcessWire that makes handling CSS and JavaScript effortless. Organize assets with groups and collections, seamlessly switch between CDNs and local files, inline critical resources, generate integrity hashes, and render optimized HTML tags - all with an intuitive notation system and clean API that feels natural in ProcessWire.

Features


  • Smart Resolution: Intuitive notation system for CDN and local assets
  • Groups & Collections: Organize assets by context (footer, analytics, critical, etc.)
  • Automatic Optimization: Inline small files, cache busting, SRI support
  • CDN Ready: Easy switching between CDNs and local files
  • Alias System: Create shortcuts for commonly used libraries
  • Flexible Rendering: Full control over HTML tag attributes
  • Simple API: Clean, consistent interface following SimpleSuite patterns
  • Advanced Access: Retrieve individual assets for custom rendering
  • Cache Busting: Automatic versioning with file hashes or timestamps
  • Security First: Built-in SRI (Subresource Integrity) hash generation
  • Inline Threshold: Automatically inline small files to reduce HTTP requests
  • Zero Required Dependencies: Works standalone with ProcessWire; optionally integrates with SimpleClient for remote fetches

Installation


Install the SimpleAsset module standalone — no other SimpleWire modules required.

Optional integration: If SimpleClient is also installed, SimpleAsset will automatically use it for all remote asset fetching (SRI hash generation, remote inlining). This means remote requests inherit SimpleClient's configured timeouts, SSL settings, and any future features. Without SimpleClient, the module falls back to cURL and then file_get_contents.

Quick Access


// Global helper — returns the AssetManager instance
$assets = asset();

// Named helper (same result)
$assets = simpleasset();

// Direct API variable
$assets = wire()->simpleasset;

Quick Start


After installation, add assets to your templates and render them in your layout:

Basic Usage

<?php
// Add assets anywhere in your templates
asset()->add(['jquery', 'bootstrap/css', 'bootstrap/js']);
asset()->add(['public/app', 'public/styles']);
?>

<!-- In your layout (e.g., _main.php) -->
<html>
<head>
    <?= asset()->css() ?>
</head>
<body>
    <!-- your content -->
    <?= asset()->js() ?>
&lt;/body&gt;
</html>

With Groups

<?php
// Add to specific groups
asset()->add('footer', ['jquery', 'public/app']);
asset()->add('analytics', ['gtag'], ['async' => true]);
?>

<html>
<head>
    <?= asset()->css() ?>
</head>
<body>
    <!-- content -->
    <?= asset()->js('footer') ?>
    <?= asset()->js('analytics') ?>
&lt;/body&gt;
</html>

Configuration


Basic Structure

The module ships with sensible defaults for common libraries (jQuery, Bootstrap, HTMX, Alpine.js, and local path aliases). The admin configuration screen controls behavioral options (destination URL, CDN base, inline threshold, cache busting, SRI, defer/async). sources, libraries, and groups are defined in code via AssetManager::getDefaults() and cannot be edited through the admin UI — customise them by extending the module or providing a config array programmatically.

// Key config structure
'sources' => [
    'cdnjs' => 'https://cdnjs.cloudflare.com/ajax/libs',
    'jsdelivr' => 'https://cdn.jsdelivr.net/npm',
    'public' => '/site/templates/public',
],
'libraries' => [
    'cdnjs/jquery' => '/jquery/3.7.1/jquery.min.js',
    'public/app' => '/app.min.js',
    'jquery' => 'cdnjs/jquery', // Alias
],

Configuration Components

ComponentExampleDescription
Sources'cdnjs' => 'https://cdnjs.com/...'Base URLs for CDN or local paths
Libraries'cdnjs/jquery' => '/jquery/3.7.1/...'Asset paths appended to source URLs
Aliases'jquery' => 'cdnjs/jquery'Shortcuts pointing to library definitions
Groups'footer' => ['defer' => true]Predefined groups with default options
Options'inline_threshold' => 2048Global asset handling settings

Asset Notation


Notation System

SimpleAsset uses an intuitive notation to reference assets:

NotationWhat It DoesExample Output
'jquery'Uses alias from configResolves to cdnjs/jquery
'cdnjs/jquery'Specific source/libraryhttps://cdnjs.com/.../jquery.min.js
'public/app'Local file reference/site/templates/public/app.min.js
'https://example.com/file.js'Direct URLUsed as-is

Resolution Process

  1. Check if it's a full URL → use directly (no further resolution)
  2. Check if it's an alias → resolve to the target library notation
  3. Parse source/library notation
  4. Look up in sources and libraries config
  5. Combine source base URL + library path to produce final URL

Adding Assets


Simple Add

// Add to default 'root' group
asset()->add(['jquery', 'bootstrap/css']);

Add to Named Group

// Add to specific group
asset()->add('footer', ['jquery', 'public/app']);

Add with Options

// Add with asset-specific options
asset()->add('analytics', ['gtag'], ['async' => true]);

// Override group defaults
asset()->add('footer', ['special.js'], ['defer' => false]);

Multiple Notations

// Mix and match notation styles
asset()->add([
    'jquery',                              // Alias
    'cdnjs/bootstrap/js',                  // Full notation
    'public/app',                          // Local file
    'https://example.com/custom.js'        // Direct URL
]);

Rendering Assets


Basic Rendering

// Render all CSS
<?= asset()->css() ?>

// Render all JavaScript
<?= asset()->js() ?>

Group-Specific Rendering

// Render specific group
<?= asset()->js('footer') ?>

// Render multiple groups
<?= asset()->js(['head', 'analytics']) ?>

With Additional Attributes

// Add attributes to all rendered tags
<?= asset()->css('print', ['media' => 'print']) ?>
<?= asset()->js('analytics', ['data-tracking' => 'enabled']) ?>

Output Examples

<!-- CSS output -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="/site/templates/public/styles.min.css?v=1704729600">

<!-- JS output -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="/site/templates/public/app.min.js?v=1704729600"></script>

<!-- With defer (from group defaults) -->
<script src="/site/templates/public/app.min.js" defer></script>

<!-- Inline (small files or inline option) -->
<style>
/* actual CSS content */
</style>

Groups


Predefined Groups

Define groups in configuration with default options:

'groups' => [
    'critical' => ['inline' => true],
    'head' => ['position' => 'head'],
    'footer' => ['position' => 'footer', 'defer' => true],
    'analytics' => ['async' => true],
]

Using Groups

// Assets inherit group options
asset()->add('footer', ['app.js']);
// Automatically gets defer=true

asset()->add('critical', ['above-fold.css']);
// Automatically gets inline=true

asset()->add('analytics', ['gtag']);
// Automatically gets async=true

Overriding Group Defaults

// Override specific asset options
asset()->add('footer', ['special.js'], ['defer' => false]);

Collections


Define Collections

Collections are reusable bundles of assets:

// Define a collection
asset()->collection('bootstrap', [
    'bootstrap/css',
    'bootstrap/js'
]);

asset()->collection('frontend-base', [
    'public/normalize',
    'public/base',
    'public/utilities'
]);

Use Collections

// Use in templates - adds all items
asset()->add(['jquery', 'bootstrap', 'frontend-base']);

// In groups
asset()->add('footer', ['bootstrap']);

Advanced Usage


Find Individual Assets

// Get specific asset for custom rendering
$react = asset()->find('footer/react');

if ($react) {
    echo $react->url();
    // https://cdnjs.cloudflare.com/ajax/libs/react/19.1.1/react.min.js
}

Custom Tag Generation

$react = asset()->find('footer/react');

// With async and defer
echo $react->tag(['async', 'defer']);
// <script src="..." async defer></script>

// With key-value attributes
echo $react->tag(['crossorigin' => 'anonymous']);
// <script src="..." crossorigin="anonymous"></script>

// With SRI hash — note: sri() fetches the remote file to compute the hash
echo $react->tag([
    'integrity' => $react->sri(),
    'crossorigin' => 'anonymous'
]);
// <script src="..." integrity="sha384-..." crossorigin="anonymous"></script>

Asset Information

$asset = asset()->find('public/app');

// Check existence
if ($asset && $asset->exists()) {
    echo $asset->url();        // Get URL
    echo $asset->type();       // 'js' or 'css'
    echo $asset->size();       // File size in bytes
    echo $asset->path();       // File path (local only)
    echo $asset->isLocal();    // true/false
}

Inline Content

// Get file contents
$critical = asset()->find('critical/styles');

if ($critical) {
    echo '<style>' . $critical->inline() . '</style>';
}

// Or use automatic inlining
asset()->add('critical', ['styles'], ['inline' => true]);

Remote assets: calling inline() or sri() on a remote CDN asset makes an outbound HTTP request to fetch the file content. The request uses SimpleClient when installed, otherwise cURL, otherwise file_get_contents (requires allow_url_fopen). Avoid calling these in tight loops or on assets that are not already cached locally.

Configuration Options


Global Options

'options' => [
    'minify' => true,           // Minify combined files (future)
    'combine' => true,          // Combine files (future)
    'inline_threshold' => 2048, // Inline files smaller than bytes
    'cache_buster' => 'auto',   // 'auto', 'timestamp', or false
    'sri' => false,             // Generate integrity hashes
    'defer' => false,           // Add defer to scripts
    'async' => false,           // Add async to scripts
]

Cache Busting

SettingBehaviorExample
'auto'File mtime for local, URL hash for remote — stable across requestsapp.js?v=1704729600
'timestamp'Current Unix timestamp — changes on every request, preventing browser caching; use only during developmentapp.js?v=1704820321
falseDisabled — no query string appendedapp.js

Inline Threshold

Files smaller than the threshold are automatically inlined:

'inline_threshold' => 2048, // 2KB

// Small file (1KB) - inlined
<style>
/* actual CSS content */
</style>

// Large file (10KB) - linked
<link rel="stylesheet" href="https://raw.githubusercontent.com/wirecodex/SimpleAsset/main/styles.css">

Common Patterns


Basic Website

// _init.php
asset()->add(['public/normalize', 'public/base']);

if ($user->isLoggedin()) {
    asset()->add('head', ['admin/toolbar'], ['inline' => true]);
}

// _main.php
<head>
    <?= asset()->css() ?>
    <?= asset()->css('critical', ['inline' => true]) ?>
</head>
<body>
    <!-- content -->
    <?= asset()->js('footer') ?>
&lt;/body&gt;

Template-Specific Assets

// Product page
if ($page->template == 'product') {
    asset()->add('footer', [
        'jquery',
        'public/product-gallery',
        'public/product-zoom'
    ]);
}

// Checkout page
if ($page->template == 'checkout') {
    asset()->add('footer', [
        'public/stripe',
        'public/checkout'
    ]);
}

Analytics & Marketing

// Define analytics collection
asset()->collection('analytics', [
    'gtag',
    'facebook-pixel',
    'hotjar'
]);

// Add with async
asset()->add('analytics', ['analytics'], ['async' => true]);

// Render
<?= asset()->js('analytics') ?>
// All scripts load async

Critical CSS Pattern

// Above-fold styles - inline
asset()->add('critical', ['public/critical'], ['inline' => true]);

// Full styles - deferred
asset()->add(['public/styles']);

<head>
    <?= asset()->css('critical') ?>
    <!-- Inlined for instant render -->

    <?= asset()->css() ?>
    <!-- Linked, loads async -->
</head>

CDN with Local Fallback

// Primary CDN
asset()->add(['jsdelivr/jquery']);

// Check and add fallback in template
$jquery = asset()->find('jsdelivr/jquery');
if (!$jquery || !$jquery->exists()) {
    asset()->add(['public/jquery-fallback']);
}

API Reference


Global Functions

simpleasset(): \SimpleWire\Asset\AssetManager
asset(): \SimpleWire\Asset\AssetManager

Main Methods

MethodParametersReturns
add()string|array, array, arrayself
find()stringAsset|null
css()string|array, arraystring
js()string|array, arraystring
collection()string, arrayself
clear()-self

Asset Methods

MethodReturnsDescription
url()stringGet asset URL
tag()stringGenerate HTML tag
sri()string|nullGet SRI hash (null if content unavailable)
inline()string|nullGet file contents (null if unavailable)
exists()boolCheck if exists
type()stringGet type (css/js)
isLocal()boolCheck if local
size()int|nullGet file size (null for remote or unreadable assets)

Best Practices


1. Use Aliases for Common Libraries

// Good - easy to switch CDN providers
'jquery' => 'cdnjs/jquery',
asset()->add(['jquery']);

// Bad - hard-coded CDN
asset()->add(['https://cdnjs.com/.../jquery.min.js']);

2. Organize with Groups

// Good - organized and semantic
asset()->add('footer', ['jquery', 'app']);
asset()->add('analytics', ['gtag']);

// Less organized
asset()->add(['jquery', 'app', 'gtag']);

3. Use Collections for Repeated Patterns

// Good - reusable
asset()->collection('bootstrap-full', [
    'bootstrap/css',
    'bootstrap/js',
    'jquery'
]);

// Bad - repetitive
asset()->add(['bootstrap/css', 'bootstrap/js', 'jquery']);
asset()->add(['bootstrap/css', 'bootstrap/js', 'jquery']);

4. Leverage Inline Threshold

// Good - small critical CSS inlines automatically
'inline_threshold' => 2048,
asset()->add('critical', ['above-fold']);

// Renders as <style>...</style> if < 2KB

5. Use Cache Busting

// Good - automatic versioning
'cache_buster' => 'auto',

// Generates: app.js?v=1704729600

Troubleshooting


Assets Not Loading

  • Check if asset notation exists in configuration
  • Verify source URLs are correct
  • Ensure local file paths are valid
  • Use $asset->exists() to test

Debug Asset Resolution

$asset = asset()->find('public/app');

if ($asset) {
    bd($asset->resolved()); // Debug resolved info
    bd($asset->exists());   // Check existence
    bd($asset->url());      // Final URL
} else {
    bd('Asset not found');
}

SRI Hashes or Remote Inline Content Returning Null

inline() and sri() on remote assets perform an HTTP fetch. If they return null:

  • Install SimpleClient for the most reliable remote fetch behaviour
  • Check that the server allows outbound connections on port 443
  • Verify cURL is available: phpinfo() or function_exists('curl_init')
  • As a last resort, ensure allow_url_fopen is enabled in php.ini
  • Enable debug mode and check the ProcessWire error log for SimpleAsset: entries

Configuration Not Working

  • Clear ProcessWire cache: Setup → Clear Cache
  • Refresh modules: Modules → Refresh
  • Enable debug mode: $config->debug = true;

Roadmap


  • Asset combining (multiple files → one file) — AssetCombiner is stubbed, logic not yet active
  • JS/CSS minification — option exists in admin but has no effect until combiner is complete
  • Cache manifest system
  • Automatic CDN upload support
  • Conditional loading helpers
  • Responsive image asset handling
  • Asset preloading hints
  • Integration with SimpleRender

License


This module is released under the MIT License.

More modules by WireCodex

  • SimpleRouter

    URL routing with pattern matching and caching for ProcessWire.
  • SimpleClient

    Fluent cURL-based HTTP client with retry, file download, and concurrent pool support.
  • SimpleRequest

    HTTP request abstraction with input handling, validation, and content negotiation.
  • SimpleResponse

    HTTP response builder with HTMX support, redirects, and content negotiation.
  • SimpleQuery

    GraphQL-like query engine for ProcessWire pages with caching, rate limiting, and write support.
  • SimpleQueue

    Background job queue with priority, delayed execution, retry, and LazyCron-based processing.
  • SimpleAsset

    Asset management for ProcessWire. Resolves, groups, and renders CSS/JS assets from CDN sources or local paths with cache-busting, SRI, and inline threshold support.

All modules by WireCodex

Install and use modules at your own risk. Always have a site and database backup before installing new modules.