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:

  1. PATH_APP/components/com_bookings/{client}/assets/css/bookings.css
  2. PATH_APP/components/com_bookings/{client}/css/bookings.css
  3. 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.