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.
- 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
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.
// Global helper — returns the AssetManager instance
$assets = asset();
// Named helper (same result)
$assets = simpleasset();
// Direct API variable
$assets = wire()->simpleasset;After installation, add assets to your templates and render them in your layout:
<?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() ?>
</body>
</html><?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') ?>
</body>
</html>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
],| Component | Example | Description |
|---|---|---|
| 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' => 2048 |
Global asset handling settings |
SimpleAsset uses an intuitive notation to reference assets:
| Notation | What It Does | Example Output |
|---|---|---|
'jquery' |
Uses alias from config | Resolves to cdnjs/jquery |
'cdnjs/jquery' |
Specific source/library | https://cdnjs.com/.../jquery.min.js |
'public/app' |
Local file reference | /site/templates/public/app.min.js |
'https://example.com/file.js' |
Direct URL | Used as-is |
- Check if it's a full URL → use directly (no further resolution)
- Check if it's an alias → resolve to the target library notation
- Parse
source/librarynotation - Look up in
sourcesandlibrariesconfig - Combine source base URL + library path to produce final URL
// Add to default 'root' group
asset()->add(['jquery', 'bootstrap/css']);// Add to specific group
asset()->add('footer', ['jquery', 'public/app']);// Add with asset-specific options
asset()->add('analytics', ['gtag'], ['async' => true]);
// Override group defaults
asset()->add('footer', ['special.js'], ['defer' => false]);// 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
]);// Render all CSS
<?= asset()->css() ?>
// Render all JavaScript
<?= asset()->js() ?>// Render specific group
<?= asset()->js('footer') ?>
// Render multiple groups
<?= asset()->js(['head', 'analytics']) ?>// Add attributes to all rendered tags
<?= asset()->css('print', ['media' => 'print']) ?>
<?= asset()->js('analytics', ['data-tracking' => 'enabled']) ?><!-- 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>Define groups in configuration with default options:
'groups' => [
'critical' => ['inline' => true],
'head' => ['position' => 'head'],
'footer' => ['position' => 'footer', 'defer' => true],
'analytics' => ['async' => true],
]// 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// Override specific asset options
asset()->add('footer', ['special.js'], ['defer' => false]);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 in templates - adds all items
asset()->add(['jquery', 'bootstrap', 'frontend-base']);
// In groups
asset()->add('footer', ['bootstrap']);// 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
}$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 = 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
}// 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()orsri()on a remote CDN asset makes an outbound HTTP request to fetch the file content. The request uses SimpleClient when installed, otherwise cURL, otherwisefile_get_contents(requiresallow_url_fopen). Avoid calling these in tight loops or on assets that are not already cached locally.
'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
]| Setting | Behavior | Example |
|---|---|---|
'auto' |
File mtime for local, URL hash for remote — stable across requests | app.js?v=1704729600 |
'timestamp' |
Current Unix timestamp — changes on every request, preventing browser caching; use only during development | app.js?v=1704820321 |
false |
Disabled — no query string appended | app.js |
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="styles.css">// _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') ?>
</body>// 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'
]);
}// 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// 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>// 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']);
}simpleasset(): \SimpleWire\Asset\AssetManager
asset(): \SimpleWire\Asset\AssetManager| Method | Parameters | Returns |
|---|---|---|
add() |
string|array, array, array | self |
find() |
string | Asset|null |
css() |
string|array, array | string |
js() |
string|array, array | string |
collection() |
string, array | self |
clear() |
- | self |
| Method | Returns | Description |
|---|---|---|
url() |
string | Get asset URL |
tag() |
string | Generate HTML tag |
sri() |
string|null | Get SRI hash (null if content unavailable) |
inline() |
string|null | Get file contents (null if unavailable) |
exists() |
bool | Check if exists |
type() |
string | Get type (css/js) |
isLocal() |
bool | Check if local |
size() |
int|null | Get file size (null for remote or unreadable assets) |
// Good - easy to switch CDN providers
'jquery' => 'cdnjs/jquery',
asset()->add(['jquery']);
// Bad - hard-coded CDN
asset()->add(['https://cdnjs.com/.../jquery.min.js']);// Good - organized and semantic
asset()->add('footer', ['jquery', 'app']);
asset()->add('analytics', ['gtag']);
// Less organized
asset()->add(['jquery', 'app', 'gtag']);// 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']);// Good - small critical CSS inlines automatically
'inline_threshold' => 2048,
asset()->add('critical', ['above-fold']);
// Renders as <style>...</style> if < 2KB// Good - automatic versioning
'cache_buster' => 'auto',
// Generates: app.js?v=1704729600- Check if asset notation exists in configuration
- Verify source URLs are correct
- Ensure local file paths are valid
- Use
$asset->exists()to test
$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');
}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()orfunction_exists('curl_init') - As a last resort, ensure
allow_url_fopenis enabled inphp.ini - Enable debug mode and check the ProcessWire error log for
SimpleAsset:entries
- Clear ProcessWire cache: Setup → Clear Cache
- Refresh modules: Modules → Refresh
- Enable debug mode:
$config->debug = true;
- Asset combining (multiple files → one file) —
AssetCombineris 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
This module is released under the MIT License.