Skip to content

wirecodex/SimpleAsset

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SimpleAsset

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() ?>
</body>
</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') ?>
</body>
</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

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

Asset Notation

Notation System

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

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

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

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="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') ?>
</body>

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

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

Asset Methods

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)

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.

About

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.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages