Developers
Assets
A component does not write <link> and <script> tags. It asks for a file by
name and the platform works out the path, appends a cache-busting version,
lets the active template override it, and adds it to the document once no
matter how many layouts ask. That is the whole reason to use the helpers
rather than writing the tag: a hand-written path is a path that breaks the
first time the component is deployed under app/.
Stylesheets, scripts, and images belong to a client, not to a component as a
whole — the administrator interface and the public site rarely want the same
CSS. Each client directory therefore has its own assets tree:
app/components/com_bookings/
site/assets/
css/bookings.css
js/bookings.js
img/instrument-placeholder.png
admin/assets/
js/bookings.js
css, js, and img are the directories the helpers know about by name.
Anything else — fonts, less, scss — is yours to organise, and is
addressed with an explicit path.
Attaching them
css(), js(), and img() are available on every controller, through the
AssetAware
trait, and in every view, through the
view helpers. They chain, and
the usual call sits at the top of a layout:
$this->css()
->js();
Both arguments have defaults, and both defaults are what you usually want:
| Argument | Default |
|---|---|
| the asset name | the component name without com_, so com_bookings looks for bookings.css |
| the extension | the component currently running |
So $this->css() in a com_bookings view attaches bookings.css from
com_bookings's own assets. $this->css('print') attaches print.css from
the same place. $this->css('tags', 'com_tags') reaches into another
component. The file extension is optional — css('print') and
css('print.css') are the same call.
Name the component's main stylesheet after the component and the no-argument
call works everywhere. Name it style.css and every layout has to remember to
say so.
Passing 'system' as the extension reads from the platform's own assets in
core/assets.
img() is the odd one: it attaches nothing, and returns a URL.
<img src="<?php echo $this->img('helpful.png'); ?>" alt="" />
A file that does not exist is not an error either. css() and js() both
test $asset->exists() first and do nothing at all when the candidate
list runs out: no tag is written, nothing is logged, and there is no 404 in
the browser's console to find. A page that renders unstyled with no
<link> in its source is almost always a misspelled asset name or a file
in site/ that the administrator client was asking for.
How a file is found
The name and extension are turned into a list of candidate paths by
Hubzero\Document\Asset\File,
and the first that exists wins. For a component, with {client} being site
or admin:
PATH_APP/components/com_bookings/{client}/assets/css/bookings.cssPATH_APP/components/com_bookings/{client}/css/bookings.css- the same two under
PATH_CORE
Every app path is tried before every core path, so a hub can replace a
single stylesheet by putting its own copy in app/components/. The assets
directory is checked before the bare one, which is why the recommended layout
is worth following: it works with no configuration.
A leading ./ in the name drops the css/js/img directory, so
css('./custom') looks for {client}/assets/custom.css. A leading / makes
the path relative to PATH_ROOT instead. A name beginning http, //, or
:// is treated as external and passed through untouched.
Template overrides
Before serving what it found, the asset checks the active template for an
override at {template path}/html/{extension}/{file} — for example
app/templates/hubzero/html/com_bookings/bookings.css. If that file exists it is used
instead. This is the same mechanism templates use to override layouts, and it
lets a template restyle a component without a copy of the component.
Cache busting
link() appends the file's modification time as a query string:
/app/components/com_bookings/site/assets/css/bookings.css?v=1583172290
The URL changes whenever the file does, so browsers pick up an edit without being told to clear anything.
Inline styles and scripts
The same helpers take raw source rather than a file name, and add it as a declaration:
$this->css('#content .instrument { margin-top: 0; }')
->js('jQuery(document).ready(function ($) { /* ... */ });');
Detection is by content, not by a flag. A stylesheet argument containing {
or @ is treated as CSS source; a script argument containing ( or ; is
treated as JavaScript source. An empty extension name also forces a
declaration.
Declarations are emitted as inline <style> and <script> blocks in the
document head, which a strict content security policy rejects. Prefer a file:
put behaviour in a .js file under assets/js and drive it from data-
attributes on the markup, rather than writing script into the page.
Rewritten and checked against 2.4-main @ 348f0057c2 on 2026-09-10.