Registering Assets in Djebel: One Call, No Enqueue Ceremony
Putting a stylesheet on a page used to be four jobs pretending to be one. Hook the page pipeline. Find </head> yourself. Escape your own URL. Remember not to emit it twice. Every plugin wrote the same code, slightly differently, and the differences only showed up on somebody else's site.
Dj_App_Assets reduces that to a single call:
Dj_App_Assets::register([ 'plugin' => 'my-plugin', 'file' => '/assets/main.js', ]);
That resolves the plugin's directory, builds the public URL, stamps a cache-busting version from the file's own modification time, works out that it is JavaScript, decides the footer is where JavaScript belongs, and renders the tag when the page is built.
The Key Tells It What You Meant
You do not pass a type. The key you use is the type:
Dj_App_Assets::register([ 'style' => '.notice { color: #b00; }', ]); // inline CSS → head
Dj_App_Assets::register([ 'js' => 'window.cfg = { ready: 1 };', ]); // inline JS → footer
Dj_App_Assets::register([ 'url' => 'https://cdn.example.com/x.css', ]); // external → head
Dj_App_Assets::register([ 'file' => '/assets/app.js', 'plugin' => 'my-plugin', ]);
Dj_App_Assets::register([ 'file' => '/assets/site.css', 'theme' => 'my-theme', ]);
plugin and theme name whose directory the file is relative to — the same API serves both, so a theme registers its stylesheet exactly the way a plugin registers its script. With neither, the file is taken as the site's own and rooted at the content directory.
CSS goes to the head because a page that paints before its stylesheet arrives flashes. JavaScript goes to the footer because a script in the head blocks the parser. Those are defaults, not rules — every one of them is overridable, and content (or buffer, or data) takes markup whose type is sniffed from a leading <script or <style.
Content that already carries its own tag is emitted verbatim. Bare content gets wrapped. You never have to decide which you are holding.
Where It Goes
The default is right often enough that most calls say nothing. When it is not, say so:
Dj_App_Assets::register([
'plugin' => 'my-plugin',
'file' => '/assets/early.js',
'in_head' => 1,
]);
in_head, head, in_footer and footer are shorthand; target takes the constants directly and also reaches TARGET_BODY_START, which rides a seam the framework already injects after the opening <body> tag. An explicit target outranks the shorthand. Asking for the head and the footer in one call throws — that is a contradiction, and quietly picking one is how you end up debugging the half of the page your asset is not on.
Attributes On The Tag
Anything the tag itself needs goes in attrs — a plain map, where true renders the attribute bare:
Dj_App_Assets::register([
'plugin' => 'my-plugin',
'file' => '/assets/main.js',
'attrs' => [ 'defer' => true, 'crossorigin' => 'anonymous', ],
]);
<script src="https://example.com/dj-content/plugins/my-plugin/assets/main.js?v=1787314638" defer crossorigin="anonymous"></script>
That is where defer, async, nomodule and a stylesheet's media live. Values are escaped on the way out, and href and src go through the URL escaper specifically — so an attribute map cannot smuggle a javascript: value into a tag. attribs is accepted as the same key, because half of us type it that way.
Register Whenever You Like — There Is No Enqueue Window
This is the part that surprises people arriving from WordPress. There, registering an asset means finding the right action first: wp_enqueue_scripts on the front end, admin_enqueue_scripts in the admin, login_enqueue_scripts on the login screen. Hook the wrong one and nothing appears. Ask after the relevant wp_head or wp_footer has already printed and nothing appears either — silently, because by then the document has moved on.
Djebel has no such window. Plugins and themes register whenever they happen to know:
- in a plugin's bootstrap, before anything has rendered
- from a theme's
header.phporindex.php, mid-render - inside a shortcode, at the moment it discovers it needs a stylesheet
- from a listener three hooks deep, in code that has no idea where in the page it is
All of it lands. The queue is read when the page is assembled, and anything registered before that point is in.
That falls out of where the rendering happens rather than being a feature bolted on: Djebel writes assets into the complete document at the very end of the page pipeline, after content has already been generated. Everything that ran to produce that content ran before the queue was read — so a shortcode that works out halfway through rendering that it needs a stylesheet simply asks for one, and does not have to have known at boot.
The practical difference is that you never write the awkward version: no stashing a flag during render to enqueue from a later hook, no registering an asset unconditionally on every page because you could not tell early enough whether you would need it.
Cache Busting You Do Not Have To Think About
A file that resolves on disk gets ?v=<filemtime> automatically:
<script src="https://example.com/dj-content/plugins/my-plugin/assets/main.js?v=1787314638"></script>
The URL comes out absolute — built from the site's own URL, so it is correct on a site living in a subdirectory and on one that is not, without the plugin knowing which it is.
Change the file, the number changes, browsers fetch it. Nothing to bump, nothing to remember at release time.
If you already know the version — a build id, a release tag — hand it over and the file is never stat'ed for one:
Dj_App_Assets::register([
'plugin' => 'my-plugin',
'file' => '/assets/main.js',
'version' => '2.4.1',
]);
v and ver work too.
Assets From Anywhere, Even Where There Is No URL
Plugins do not all live in the same place. Some sit in dj-content/plugins and are served over the web. Others live in the private tree, outside the document root, where no URL can reach them at all.
You should not have to know which one you are, and you do not:
Dj_App_Assets::register([ 'plugin' => 'my-plugin', 'file' => '/assets/main.js', ]);
If the file is web-reachable, that renders a <script src>. If it is not, the file's contents are read and inlined instead, and a line goes to the log saying why. The same call works from either place, and a plugin can be installed anywhere without its author thinking about deployment layout.
Order: Registration, Priority, Prerequisites
Assets render in the order they were registered. That is usually what you meant, and it costs nothing — with no priorities in play, there is no sorting step at all.
When registration order is not enough, priority works exactly like it does for hooks — lower runs earlier, the default is the framework's own DEFAULT_PRIORITY, and two assets at the same priority keep their registration order so plugins never have to coordinate.
And when the requirement is not a preference but a fact — this script cannot run before that one — say so directly:
Dj_App_Assets::register([ 'plugin' => 'my-plugin', 'file' => '/assets/vendor/jquery.js', 'id' => 'jquery', ]);
Dj_App_Assets::register([
'plugin' => 'my-plugin',
'file' => '/assets/app.js',
'prereq' => 'jquery',
]);
prereq is a hard constraint and it wins over priority, because a priority is an opinion about ordering and a prerequisite is a requirement. Register the dependent one first if you like — it still renders second.
Several prerequisites, written whichever way reads better:
'prereq' => [ 'jquery', 'my-plugin-config', ],
'prereq' => 'jquery, my-plugin-config',
Two things worth knowing. A prerequisite naming something nobody registered is treated as satisfied rather than blocking — a missing name should not silently drop a working asset off the page. And if two assets end up requiring each other, everything still renders and the cycle is logged with both ids, because a registration mistake should not cost you the page.
Removing and Replacing
register() hands back a result carrying the asset's id, so you can hold on to it:
$res_obj = Dj_App_Assets::register([ 'plugin' => 'my-plugin', 'file' => '/assets/main.js', ]);
$asset_id = $res_obj->id;
Dj_App_Assets::deregister($asset_id);
You do not have to keep it, though. Naming the asset the same way you added it finds it just as well:
Dj_App_Assets::deregister([ 'plugin' => 'my-plugin', 'file' => '/assets/main.js', ]);
Pass an explicit id when you want a stable handle other code can target — registering that id again replaces the entry where it already sits, so a site can swap a plugin's script for its own without touching the plugin.
Hooks, Both Ends
Everything above is a default that a site can change without editing anybody's plugin:
| Hook | What it is for |
|---|---|
app.core.assets.filter.add_params |
Rewrite the request before it is understood — point a file at a CDN, force a placement |
app.core.assets.filter.item |
The resolved asset, just before it queues. Return empty to veto it entirely |
app.core.assets.action.added |
It queued. For logging and auditing |
app.core.assets.filter.queue |
The whole queue for one placement, after ordering |
app.core.assets.filter.tag_html |
Each rendered tag — stamp a CSP nonce, add integrity, rewrite somebody else's attributes |
app.core.assets.filter.html |
The assembled block, right before it is written |
Which means a content-security-policy plugin is one listener on tag_html, and "this site never loads that asset" is one listener on item returning nothing.
The split is worth keeping straight: attrs is how a plugin states what its own tag needs, and tag_html is how a site imposes something on a tag it does not own.
The Short Version
// A file, from a plugin, wherever that plugin lives
Dj_App_Assets::register([ 'plugin' => 'my-plugin', 'file' => '/assets/main.css', ]);
// Inline, in the head, before anything paints
Dj_App_Assets::register([ 'style' => '.hero { opacity: 0; }', 'in_head' => 1, ]);
// After a dependency, whatever order they were registered in
Dj_App_Assets::register([ 'js' => 'App.start();', 'prereq' => 'jquery', ]);
One call. The key says what it is, the context says where it lives, and the page decides when. No enqueue window, no manual URL building, no cache-busting ritual, and nothing to remember about where your plugin happens to be installed.