Theme Assets

3 vues Markdown

Where a theme's stylesheets, scripts, images and fonts live, and why only two of the four get a version query.

Overview

Everything the browser fetches from a theme sits under its assets/ directory, addressed by one function. Nothing is registered, bundled or compiled: the file is where you put it.

Stylesheets and scripts come back with a cache busting query built from the file's modification time. Fonts and images come back clean, deliberately.

Structure

Inside assets/

The layout is a convention: the function takes any path under assets/. Counts are from the shipped WStyle theme.

templates/website/WStyle/assets
assets/
├── css/                    50 stylesheets: default.css, theme.css, then one per surface
│   └── libs/               5 third party bundles: bootstrap-icons, fontawesome, fonts, prism, wcp-table
├── js/                     55 scripts: default.js, money.js, then one per surface
│   └── libs/               6 third party bundles: tom-select, intl-tel-input, jspdf, ...
├── images/                 27 entries, grouped: hero/, logo/, banks/, avatars/, addons/
├── videos/                 anything heavier than an image
├── favicon.svg             addressed like any other asset
└── component-showcase.html a live catalogue of the theme's own primitives

default.css and default.js load on every page, so every visitor pays for them. A stylesheet named after a surface is linked from that view and by nothing else.

component-showcase.html is the theme's own component catalogue. Open it in a browser before writing new markup.

Step by Step

1. Put the File in Place

  1. Drop the file under assets/, in css/, js/ or images/.
  2. Name a surface file after its surface: css/balance.css, js/balance.js.
  3. Third party bundles go unmodified in css/libs/ or js/libs/, so upgrading one is a directory swap.

Nothing watches the directory: the file is reachable but unreferenced.

  1. Site wide files belong in the layout's head, once.
  2. A surface's own stylesheet goes in that view's {block name=head}.
  3. A surface's own script goes in {block name=scripts}, never in head. Core scripts print before that block, so a head script runs too early and fails silently.
  4. Write the path relative to assets/. The function adds the rest.

Reload and read the markup. A link ending in ?v=1753974812 is your file, found on disk. A link with no query is the diagnostic below.

3. Reference It from PHP

  1. In hooks.php, or any other PHP, call the same function on the active theme.
  2. Inject markup through a layout hook point instead of editing the layout. An optional stylesheet ships without a second head.

The injected tag now appears wherever the hook point fires, with the same version query.

4. Add a Font

  1. Put the woff2 files and their @font-face stylesheet together under css/libs/fonts/.
  2. Inside that stylesheet, reference the font files relatively. Browsers resolve url() against the stylesheet, not the page.
  3. Link the stylesheet with the normal tag, and preload the font file the first paint needs.
  4. If the font already ships with a hashed query in its stylesheet, repeat that exact query on the preload.

The network panel shows one request per font file. Two requests mean the preload and the stylesheet disagree.

Reference

The Addressing Function

coremio/classes/Theme.php
public function assetUrl(string $path = ''): string;

// $path  relative to the theme's assets/ directory. A leading slash is trimmed,
//        so 'css/default.css' and '/css/default.css' are the same request.
//        An empty string returns the assets directory itself.
//
// returns  {APP_URI}/templates/website/{Theme}/assets/{$path}
//          plus ?v={mtime} when the extension is css or js AND the file exists on disk.
ArgumentReturned URLVersion query
'css/default.css'.../assets/css/default.css?v=1753974812Yes, the file's modification time
'js/home.js'.../assets/js/home.js?v=1753902114Yes
'images/hero/banner.webp'.../assets/images/hero/banner.webpNo, deliberately
'css/libs/fonts/x.woff2'.../assets/css/libs/fonts/x.woff2No, deliberately
'css/typo.css' (no such file).../assets/css/typo.cssNone. The URL is still returned, and 404s
''.../assets/None

A link with no version query says the file is not on disk. The function stats it only to read a modification time.

Calling It from a View

the same function, three call sites
<!-- Smarty: one named parameter, 'path' -->
<link rel="stylesheet" href="{asset path='css/balance.css'}">
<script src="{asset path='js/balance.js'}" defer></script>

<!-- Twig: one positional argument -->
<link rel="stylesheet" href="{{ asset('css/balance.css') }}">

<!-- Plain PHP theme, and any PHP outside a view -->
<link rel="stylesheet" href="<?= Theme::active()->assetUrl('css/balance.css') ?>">
{asset path='...'} Smarty. One named parameter, and only one. Mistype it and the tag falls back to an empty path: the link points at the bare assets directory instead of failing.
asset('...') Twig. Positional, and present in the sandbox's function allowlist. Same resolution, same string.
Theme::active()->assetUrl() PHP. What both tags call underneath. Use it from hooks.php and from any markup built outside a template.
$tadress Every page also receives the theme directory's URL as a plain variable. It is the theme root, not assets/, and carries no version query.

Example

Site Wide Assets, Once, in the Layout

The head of a shipped layout, cut to the asset lines. Order matters: the icon font is preloaded before the stylesheet that declares it.

templates/website/WStyle/layouts/default.tpl
<link rel="icon" type="image/svg+xml" href="{asset path='favicon.svg'}">

<!-- The query is the hash already present in bootstrap-icons.min.css's own src.
     Get it wrong and the browser fetches the font twice. -->
<link rel="preload" href="{asset path='css/libs/bootstrap-icons/fonts/bootstrap-icons.woff2'}?e34853135f9e39acf64315236852cd5a"
      as="font" type="font/woff2" crossorigin>

<link rel="stylesheet" href="{asset path='css/libs/bootstrap-icons/bootstrap-icons.min.css'}">
<link rel="stylesheet" href="{asset path='css/libs/fonts/urbanist.css'}">
<link rel="stylesheet" href="{asset path='css/theme.css'}">
<link rel="stylesheet" href="{asset path='css/default.css'}">
<link rel="stylesheet" href="{asset path='css/default-dark.css'}">

<!-- Loaded only where it is needed, decided by a variable, not by a second layout. -->
{if $is_client_area}<link rel="stylesheet" href="{asset path='css/client-nav.css'}">{/if}

<!-- Core scripts, before the page's own block. -->
<script src="{asset path='js/bootstrap.bundle.min.js'}" defer></script>
<script src="{asset path='js/money.js'}" defer></script>
<script src="{asset path='js/default.js'}" defer></script>

Surface Assets, in the View That Needs Them

A real view's two asset blocks. Stylesheets go in head, scripts in scripts. The library the page script needs is loaded in the same block, above it.

templates/website/WStyle/views/account/balance.tpl
{extends file='layouts/default.tpl'}

{block name=head}
    <link rel="stylesheet" href="{asset path='css/account-settings.css'}">
    <link rel="stylesheet" href="{asset path='css/libs/wcp-table/table.css'}">
    <link rel="stylesheet" href="{asset path='css/balance.css'}">
{/block}

{block name=scripts}
    {* The library first, then the page script that uses it: same block, document order. *}
    <script src="{asset path='js/libs/wcp-table/table.js'}" defer></script>
    <script src="{asset path='js/balance.js'}" defer></script>
{/block}

{block name=content}
    {* ... *}
{/block}
templates/website/Acme/hooks.php
// A hook point takes a string and prints it as is, so build the tag here and
// leave the layout alone. Same function, same version query.
Hook::add("ui:client.head.css", 1, function (): string {
    $href = Theme::active()->assetUrl('css/extra.css');

    return '<link rel="stylesheet" href="' . $href . '">';
});

Pitfalls

No version query means the file is not there

A URL is versioned only when the function finds the file on disk. A link that comes back as css/balance.css with nothing after it is a wrong path, and it is about to 404.

Fonts and images are unversioned on purpose. Do not "fix" it

A font is requested twice: once by your preload tag, once by the url() inside the font stylesheet. The function never touches that second one. Version the preload and the two URLs stop matching. The preload is wasted, and a face declared font-display: optional misses first paint and keeps the fallback metrics.

A preload's query must match the stylesheet's query character for character

Bundles like the icon font ship their own hashed query inside src:. Repeat it exactly on the preload, or the browser downloads the font twice. Read the hash from the bundle's own stylesheet, not from another theme.

A page script in the head is a script that runs too early

Deferred scripts execute in document order. A page script moved into {block name=head} runs before the theme's core script. It cannot see the global object, and fails without an error. One exception: a library the core script itself consumes loads in the head, above it.

A url() inside CSS is resolved against the CSS file

The addressing function is for markup. A background image, font file or SVG mask referenced from inside a stylesheet resolves against that stylesheet, never through the function. Keep those references relative and keep the files beside the stylesheet that names them.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.